fix(utils): fix promotion case of each allocation not applying its total amount (#13199)
* fix(utils): fix promotion case of each allocation not applying its amount * chore: fixed tests --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Oli Juhl
parent
0128ed314c
commit
3cc512ef39
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(utils): fix promotion case of each allocation not applying its amount
|
||||||
@@ -3321,18 +3321,18 @@ medusaIntegrationTestRunner({
|
|||||||
expect(updated.status).toEqual(200)
|
expect(updated.status).toEqual(200)
|
||||||
expect(updated.data.cart).toEqual(
|
expect(updated.data.cart).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
discount_total: 105,
|
discount_total: 210,
|
||||||
discount_subtotal: 100,
|
discount_subtotal: 200,
|
||||||
discount_tax_total: 5,
|
discount_tax_total: 10,
|
||||||
original_total: 210,
|
original_total: 210,
|
||||||
total: 105, // 210 - 100 tax excl promotion + 5 promotion tax
|
total: 0, // 210 - 200 tax excl promotion + 10 promotion tax
|
||||||
items: expect.arrayContaining([
|
items: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
is_tax_inclusive: true,
|
is_tax_inclusive: true,
|
||||||
adjustments: expect.arrayContaining([
|
adjustments: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
code: taxInclPromotion.code,
|
code: taxInclPromotion.code,
|
||||||
amount: 105,
|
amount: 210,
|
||||||
is_tax_inclusive: true,
|
is_tax_inclusive: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@@ -3739,6 +3739,107 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should apply promotions to multiple quantity of the same product", async () => {
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products`,
|
||||||
|
{
|
||||||
|
title: "Product for free",
|
||||||
|
description: "test",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
sku: "special-shirt",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 100,
|
||||||
|
currency_code: "eur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const sameProductPromotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "SAME_PRODUCT_PROMOTION",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "each",
|
||||||
|
value: 100,
|
||||||
|
max_quantity: 5,
|
||||||
|
currency_code: "eur",
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product_id",
|
||||||
|
operator: "in",
|
||||||
|
values: [product.id],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: noAutomaticRegion.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
items: [{ variant_id: product.variants[0].id, quantity: 2 }],
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
expect(cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
discount_total: 200,
|
||||||
|
original_total: 200,
|
||||||
|
total: 0,
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: sameProductPromotion.code,
|
||||||
|
amount: 200,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
promotions: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: sameProductPromotion.code,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
describe("Percentage promotions", () => {
|
describe("Percentage promotions", () => {
|
||||||
it("should apply a percentage promotion to a cart", async () => {
|
it("should apply a percentage promotion to a cart", async () => {
|
||||||
const percentagePromotion = (
|
const percentagePromotion = (
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ function getPromotionValueForPercentage(promotion, lineItemAmount) {
|
|||||||
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemAmount)
|
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) {
|
function getPromotionValueForFixed(
|
||||||
|
promotion,
|
||||||
|
lineItemAmount,
|
||||||
|
lineItemsAmount,
|
||||||
|
lineItem
|
||||||
|
) {
|
||||||
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
||||||
const promotionValueForItem = MathBN.mult(
|
const promotionValueForItem = MathBN.mult(
|
||||||
MathBN.div(lineItemAmount, lineItemsAmount),
|
MathBN.div(lineItemAmount, lineItemsAmount),
|
||||||
@@ -27,15 +32,37 @@ function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) {
|
|||||||
|
|
||||||
return MathBN.mult(promotionValueForItem, MathBN.div(percentage, 100))
|
return MathBN.mult(promotionValueForItem, MathBN.div(percentage, 100))
|
||||||
}
|
}
|
||||||
return promotion.value
|
|
||||||
|
// For each allocation, promotion is applied in the scope of the line item.
|
||||||
|
// lineItemAmount will be the total applicable amount for the line item
|
||||||
|
// maximumPromotionAmount is the maximum amount that can be applied to the line item
|
||||||
|
// We need to return the minimum of the two
|
||||||
|
const maximumQuantity = MathBN.min(
|
||||||
|
lineItem.quantity,
|
||||||
|
promotion.max_quantity ?? MathBN.convert(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
const maximumPromotionAmount = MathBN.mult(promotion.value, maximumQuantity)
|
||||||
|
|
||||||
|
return MathBN.min(maximumPromotionAmount, lineItemAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPromotionValue(promotion, lineItemAmount, lineItemsAmount) {
|
export function getPromotionValue(
|
||||||
|
promotion,
|
||||||
|
lineItemAmount,
|
||||||
|
lineItemsAmount,
|
||||||
|
lineItem
|
||||||
|
) {
|
||||||
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
|
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
|
||||||
return getPromotionValueForPercentage(promotion, lineItemAmount)
|
return getPromotionValueForPercentage(promotion, lineItemAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount)
|
return getPromotionValueForFixed(
|
||||||
|
promotion,
|
||||||
|
lineItemAmount,
|
||||||
|
lineItemsAmount,
|
||||||
|
lineItem
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApplicableQuantity(lineItem, maxQuantity) {
|
export function getApplicableQuantity(lineItem, maxQuantity) {
|
||||||
@@ -105,7 +132,8 @@ export function calculateAdjustmentAmountFromPromotion(
|
|||||||
const promotionValue = getPromotionValue(
|
const promotionValue = getPromotionValue(
|
||||||
promotion,
|
promotion,
|
||||||
applicableAmount,
|
applicableAmount,
|
||||||
lineItemsAmount
|
lineItemsAmount,
|
||||||
|
lineItem
|
||||||
)
|
)
|
||||||
|
|
||||||
const returnValue = MathBN.min(promotionValue, applicableAmount)
|
const returnValue = MathBN.min(promotionValue, applicableAmount)
|
||||||
@@ -139,14 +167,17 @@ export function calculateAdjustmentAmountFromPromotion(
|
|||||||
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
|
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
|
||||||
promotion.applied_value
|
promotion.applied_value
|
||||||
)
|
)
|
||||||
|
|
||||||
const itemAmount = MathBN.div(
|
const itemAmount = MathBN.div(
|
||||||
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
|
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
|
||||||
lineItem.quantity
|
lineItem.quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
const maximumPromotionAmount = MathBN.mult(
|
const maximumPromotionAmount = MathBN.mult(
|
||||||
itemAmount,
|
itemAmount,
|
||||||
promotion.max_quantity ?? MathBN.convert(1)
|
promotion.max_quantity ?? MathBN.convert(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
const applicableAmount = MathBN.min(
|
const applicableAmount = MathBN.min(
|
||||||
remainingItemAmount,
|
remainingItemAmount,
|
||||||
maximumPromotionAmount
|
maximumPromotionAmount
|
||||||
@@ -159,7 +190,8 @@ export function calculateAdjustmentAmountFromPromotion(
|
|||||||
const promotionValue = getPromotionValue(
|
const promotionValue = getPromotionValue(
|
||||||
promotion,
|
promotion,
|
||||||
applicableAmount,
|
applicableAmount,
|
||||||
lineItemsAmount
|
lineItemsAmount,
|
||||||
|
lineItem
|
||||||
)
|
)
|
||||||
|
|
||||||
const returnValue = MathBN.min(promotionValue, applicableAmount)
|
const returnValue = MathBN.min(promotionValue, applicableAmount)
|
||||||
|
|||||||
+1
-1
@@ -464,7 +464,7 @@ moduleIntegrationTestRunner({
|
|||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "each",
|
allocation: "each",
|
||||||
value: 500,
|
value: 100,
|
||||||
max_quantity: 5,
|
max_quantity: 5,
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user