fix(utils,promotion): fix case where mutliple percentage promotions werent applied (#11992)

what:

- fix case where mutliple percentage promotions werent applied
This commit is contained in:
Riqwan Thamir
2025-03-26 22:46:50 +00:00
committed by GitHub
parent 1f8fab3636
commit c870a7a1ba
3 changed files with 330 additions and 9 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/promotion": patch
"@medusajs/utils": patch
---
fix(promotion,utils): fix case where multiple percentage promotions werent applied
@@ -9,12 +9,26 @@ function getPromotionValueForPercentage(promotion, lineItemTotal) {
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemTotal)
}
function getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal) {
function getPromotionValueForFixed(promotion, itemTotal, allItemsTotal) {
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
return MathBN.mult(
MathBN.div(lineItemTotal, lineItemsTotal),
const promotionValueForItem = MathBN.mult(
MathBN.div(itemTotal, allItemsTotal),
promotion.value
)
if (MathBN.lte(promotionValueForItem, itemTotal)) {
return promotionValueForItem
}
const percentage = MathBN.div(
MathBN.mult(itemTotal, 100),
promotionValueForItem
)
return MathBN.mult(
promotionValueForItem,
MathBN.div(percentage, 100)
).precision(4)
}
return promotion.value
@@ -45,12 +59,84 @@ export function calculateAdjustmentAmountFromPromotion(
promotion,
lineItemsTotal: BigNumberInput = 0
) {
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
const lineItemTotal = MathBN.mult(getLineItemUnitPrice(lineItem), quantity)
const applicableTotal = MathBN.sub(lineItemTotal, promotion.applied_value)
/*
For a promotion with an across allocation, we consider not only the line item total, but also the total of all other line items in the order.
We then distribute the promotion value proportionally across the line items based on the total of each line item.
For example, if the promotion is 100$, and the order total is 400$, and the items are:
item1: 250$
item2: 150$
total: 400$
The promotion value for the line items would be:
item1: 62.5$
item2: 37.5$
total: 100$
For the next 100$ promotion, we remove the applied promotions value from the line item total and redistribute the promotion value across the line items based on the updated totals.
Example:
item1: (250 - 62.5) = 187.5
item2: (150 - 37.5) = 112.5
total: 300
The promotion value for the line items would be:
item1: $62.5
item2: $37.5
total: 100$
*/
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
const lineItemTotal = MathBN.mult(getLineItemUnitPrice(lineItem), quantity)
const applicableTotal = MathBN.sub(lineItemTotal, promotion.applied_value)
if (MathBN.lte(applicableTotal, 0)) {
return applicableTotal
}
const promotionValue = getPromotionValue(
promotion,
applicableTotal,
lineItemsTotal
)
return MathBN.min(promotionValue, applicableTotal)
}
/*
For a promotion with an EACH allocation, we calculate the promotion value on the line item as a whole.
Example:
item1: {
subtotal: 200$,
unit_price: 50$,
quantity: 4,
}
When applying promotions, we need to consider 2 values:
1. What is the maximum promotion value?
2. What is the maximum promotion we can apply on the line item?
After applying each promotion, we reduce the maximum promotion that you can add to the line item by the value of the promotions applied.
We then apply whichever is lower.
*/
const remainingItemTotal = MathBN.sub(
lineItem.subtotal,
promotion.applied_value
)
const unitPrice = MathBN.div(lineItem.subtotal, lineItem.quantity)
const maximumPromotionTotal = MathBN.mult(
unitPrice,
promotion.max_quantity ?? MathBN.convert(1)
)
const applicableTotal = MathBN.min(remainingItemTotal, maximumPromotionTotal)
if (MathBN.lte(applicableTotal, 0)) {
return applicableTotal
return MathBN.convert(0)
}
const promotionValue = getPromotionValue(
@@ -736,7 +736,7 @@ moduleIntegrationTestRunner({
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 2,
amount: 5,
code: "PROMOTION_TEST_2",
},
{
@@ -748,6 +748,81 @@ moduleIntegrationTestRunner({
])
})
it("should compute actions when 2 percentage promotions are applied to the same item", async () => {
await createDefaultPromotion(service, {
code: "PROMO_PERCENTAGE_1",
rules: [],
application_method: {
type: ApplicationMethodType.PERCENTAGE,
target_type: "items",
allocation: "each",
value: 100,
max_quantity: 1,
target_rules: [
{
attribute: "product.id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
})
await createDefaultPromotion(service, {
code: "PROMO_PERCENTAGE_2",
rules: [],
application_method: {
type: ApplicationMethodType.PERCENTAGE,
target_type: "items",
allocation: "each",
value: 100,
max_quantity: 1,
target_rules: [
{
attribute: "product.id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
})
const result = await service.computeActions(
["PROMO_PERCENTAGE_1", "PROMO_PERCENTAGE_2"],
{
currency_code: "usd",
items: [
{
id: "item_cotton_tshirt",
quantity: 4,
subtotal: 200,
product_category: {
id: "catg_cotton",
},
product: {
id: "prod_tshirt",
},
},
],
}
)
expect(JSON.parse(JSON.stringify(result))).toEqual([
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 50,
code: "PROMO_PERCENTAGE_1",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 50,
code: "PROMO_PERCENTAGE_2",
},
])
})
it("should not compute actions when applicable total is 0", async () => {
await createDefaultPromotion(service, {
rules: [
@@ -1695,6 +1770,160 @@ moduleIntegrationTestRunner({
])
})
it("should compute actions when 2 percentage promotions are applied to the same item", async () => {
await createDefaultPromotion(service, {
code: "PROMO_PERCENTAGE_1",
rules: [],
application_method: {
type: ApplicationMethodType.PERCENTAGE,
target_type: "items",
allocation: "across",
value: 50,
target_rules: [
{
attribute: "product.id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
})
await createDefaultPromotion(service, {
code: "PROMO_PERCENTAGE_2",
rules: [],
application_method: {
type: ApplicationMethodType.PERCENTAGE,
target_type: "items",
allocation: "across",
value: 50,
target_rules: [
{
attribute: "product.id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
})
const result = await service.computeActions(
["PROMO_PERCENTAGE_1", "PROMO_PERCENTAGE_2"],
{
currency_code: "usd",
items: [
{
id: "item_cotton_tshirt",
quantity: 4,
subtotal: 300,
product: {
id: "prod_tshirt",
},
},
{
id: "item_wool_tshirt",
quantity: 4,
subtotal: 100,
product: {
id: "prod_tshirt",
},
},
],
}
)
expect(JSON.parse(JSON.stringify(result))).toEqual([
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 150,
code: "PROMO_PERCENTAGE_1",
},
{
action: "addItemAdjustment",
item_id: "item_wool_tshirt",
amount: 50,
code: "PROMO_PERCENTAGE_1",
},
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 75,
code: "PROMO_PERCENTAGE_2",
},
{
action: "addItemAdjustment",
item_id: "item_wool_tshirt",
amount: 25,
code: "PROMO_PERCENTAGE_2",
},
])
await createDefaultPromotion(service, {
code: "PROMO_PERCENTAGE_3",
rules: [],
application_method: {
type: ApplicationMethodType.PERCENTAGE,
target_type: "items",
allocation: "across",
value: 100,
target_rules: [
{
attribute: "product.id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
})
const result2 = await service.computeActions(
[
"PROMO_PERCENTAGE_1",
"PROMO_PERCENTAGE_2",
"PROMO_PERCENTAGE_3",
],
{
currency_code: "usd",
items: [
{
id: "item_cotton_tshirt",
quantity: 4,
subtotal: 300,
product: {
id: "prod_tshirt",
},
},
{
id: "item_wool_tshirt",
quantity: 4,
subtotal: 100,
product: {
id: "prod_tshirt",
},
},
],
}
)
// Should only apply the most valuable promotion (100% off - PROMO_PERCENTAGE_3) that covers
// the entire total of the line item
expect(JSON.parse(JSON.stringify(result2))).toEqual([
{
action: "addItemAdjustment",
item_id: "item_cotton_tshirt",
amount: 300,
code: "PROMO_PERCENTAGE_3",
},
{
action: "addItemAdjustment",
item_id: "item_wool_tshirt",
amount: 100,
code: "PROMO_PERCENTAGE_3",
},
])
})
it("should not compute actions when applicable total is 0", async () => {
await createDefaultPromotion(service, {
rules: [
@@ -3937,7 +4166,7 @@ moduleIntegrationTestRunner({
},
},
],
})
} as any)
expect(JSON.parse(JSON.stringify(result))).toEqual([
{