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:
1 parent
1f8fab3636
commit
c870a7a1ba
3 files changed
+330
-9
No files matched your search
@@ -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)
|
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal) {
|
function getPromotionValueForFixed(promotion, itemTotal, allItemsTotal) {
|
||||||
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
||||||
return MathBN.mult(
|
const promotionValueForItem = MathBN.mult(
|
||||||
MathBN.div(lineItemTotal, lineItemsTotal),
|
MathBN.div(itemTotal, allItemsTotal),
|
||||||
promotion.value
|
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
|
return promotion.value
|
||||||
@@ -45,12 +59,84 @@ export function calculateAdjustmentAmountFromPromotion(
|
|||||||
promotion,
|
promotion,
|
||||||
lineItemsTotal: BigNumberInput = 0
|
lineItemsTotal: BigNumberInput = 0
|
||||||
) {
|
) {
|
||||||
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
|
/*
|
||||||
const lineItemTotal = MathBN.mult(getLineItemUnitPrice(lineItem), quantity)
|
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.
|
||||||
const applicableTotal = MathBN.sub(lineItemTotal, promotion.applied_value)
|
|
||||||
|
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)) {
|
if (MathBN.lte(applicableTotal, 0)) {
|
||||||
return applicableTotal
|
return MathBN.convert(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const promotionValue = getPromotionValue(
|
const promotionValue = getPromotionValue(
|
||||||
|
|||||||
+231
-2
@@ -736,7 +736,7 @@ moduleIntegrationTestRunner({
|
|||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 2,
|
amount: 5,
|
||||||
code: "PROMOTION_TEST_2",
|
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 () => {
|
it("should not compute actions when applicable total is 0", async () => {
|
||||||
await createDefaultPromotion(service, {
|
await createDefaultPromotion(service, {
|
||||||
rules: [
|
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 () => {
|
it("should not compute actions when applicable total is 0", async () => {
|
||||||
await createDefaultPromotion(service, {
|
await createDefaultPromotion(service, {
|
||||||
rules: [
|
rules: [
|
||||||
@@ -3937,7 +4166,7 @@ moduleIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
} as any)
|
||||||
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual([
|
expect(JSON.parse(JSON.stringify(result))).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user