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:
@@ -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(
|
||||
|
||||
+231
-2
@@ -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([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user