Skip to content

Commit 3cc512e

Browse files
riqwanolivermrbl
andauthored
fix(utils): fix promotion case of each allocation not applying its total amount (medusajs#13199)
* fix(utils): fix promotion case of each allocation not applying its amount * chore: fixed tests --------- Co-authored-by: Oli Juhl <[email protected]>
1 parent 0128ed3 commit 3cc512e

File tree

4 files changed

+150
-12
lines changed

4 files changed

+150
-12
lines changed

.changeset/fresh-dragons-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/utils": patch
3+
---
4+
5+
fix(utils): fix promotion case of each allocation not applying its amount

integration-tests/http/__tests__/cart/store/cart.spec.ts

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,18 +3321,18 @@ medusaIntegrationTestRunner({
33213321
expect(updated.status).toEqual(200)
33223322
expect(updated.data.cart).toEqual(
33233323
expect.objectContaining({
3324-
discount_total: 105,
3325-
discount_subtotal: 100,
3326-
discount_tax_total: 5,
3324+
discount_total: 210,
3325+
discount_subtotal: 200,
3326+
discount_tax_total: 10,
33273327
original_total: 210,
3328-
total: 105, // 210 - 100 tax excl promotion + 5 promotion tax
3328+
total: 0, // 210 - 200 tax excl promotion + 10 promotion tax
33293329
items: expect.arrayContaining([
33303330
expect.objectContaining({
33313331
is_tax_inclusive: true,
33323332
adjustments: expect.arrayContaining([
33333333
expect.objectContaining({
33343334
code: taxInclPromotion.code,
3335-
amount: 105,
3335+
amount: 210,
33363336
is_tax_inclusive: true,
33373337
}),
33383338
]),
@@ -3739,6 +3739,107 @@ medusaIntegrationTestRunner({
37393739
)
37403740
})
37413741

3742+
it("should apply promotions to multiple quantity of the same product", async () => {
3743+
const product = (
3744+
await api.post(
3745+
`/admin/products`,
3746+
{
3747+
title: "Product for free",
3748+
description: "test",
3749+
options: [
3750+
{
3751+
title: "Size",
3752+
values: ["S"],
3753+
},
3754+
],
3755+
variants: [
3756+
{
3757+
title: "S / Black",
3758+
sku: "special-shirt",
3759+
options: {
3760+
Size: "S",
3761+
},
3762+
manage_inventory: false,
3763+
prices: [
3764+
{
3765+
amount: 100,
3766+
currency_code: "eur",
3767+
},
3768+
],
3769+
},
3770+
],
3771+
},
3772+
adminHeaders
3773+
)
3774+
).data.product
3775+
3776+
const sameProductPromotion = (
3777+
await api.post(
3778+
`/admin/promotions`,
3779+
{
3780+
code: "SAME_PRODUCT_PROMOTION",
3781+
type: PromotionType.STANDARD,
3782+
status: PromotionStatus.ACTIVE,
3783+
is_tax_inclusive: false,
3784+
is_automatic: true,
3785+
application_method: {
3786+
type: "fixed",
3787+
target_type: "items",
3788+
allocation: "each",
3789+
value: 100,
3790+
max_quantity: 5,
3791+
currency_code: "eur",
3792+
target_rules: [
3793+
{
3794+
attribute: "product_id",
3795+
operator: "in",
3796+
values: [product.id],
3797+
},
3798+
],
3799+
},
3800+
},
3801+
adminHeaders
3802+
)
3803+
).data.promotion
3804+
3805+
cart = (
3806+
await api.post(
3807+
`/store/carts`,
3808+
{
3809+
currency_code: "eur",
3810+
sales_channel_id: salesChannel.id,
3811+
region_id: noAutomaticRegion.id,
3812+
shipping_address: shippingAddressData,
3813+
items: [{ variant_id: product.variants[0].id, quantity: 2 }],
3814+
},
3815+
storeHeadersWithCustomer
3816+
)
3817+
).data.cart
3818+
3819+
expect(cart).toEqual(
3820+
expect.objectContaining({
3821+
discount_total: 200,
3822+
original_total: 200,
3823+
total: 0,
3824+
items: expect.arrayContaining([
3825+
expect.objectContaining({
3826+
adjustments: expect.arrayContaining([
3827+
expect.objectContaining({
3828+
code: sameProductPromotion.code,
3829+
amount: 200,
3830+
}),
3831+
]),
3832+
}),
3833+
]),
3834+
promotions: expect.arrayContaining([
3835+
expect.objectContaining({
3836+
code: sameProductPromotion.code,
3837+
}),
3838+
]),
3839+
})
3840+
)
3841+
})
3842+
37423843
describe("Percentage promotions", () => {
37433844
it("should apply a percentage promotion to a cart", async () => {
37443845
const percentagePromotion = (

packages/core/utils/src/totals/promotion/index.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ function getPromotionValueForPercentage(promotion, lineItemAmount) {
99
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemAmount)
1010
}
1111

12-
function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) {
12+
function getPromotionValueForFixed(
13+
promotion,
14+
lineItemAmount,
15+
lineItemsAmount,
16+
lineItem
17+
) {
1318
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
1419
const promotionValueForItem = MathBN.mult(
1520
MathBN.div(lineItemAmount, lineItemsAmount),
@@ -27,15 +32,37 @@ function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) {
2732

2833
return MathBN.mult(promotionValueForItem, MathBN.div(percentage, 100))
2934
}
30-
return promotion.value
35+
36+
// For each allocation, promotion is applied in the scope of the line item.
37+
// lineItemAmount will be the total applicable amount for the line item
38+
// maximumPromotionAmount is the maximum amount that can be applied to the line item
39+
// We need to return the minimum of the two
40+
const maximumQuantity = MathBN.min(
41+
lineItem.quantity,
42+
promotion.max_quantity ?? MathBN.convert(1)
43+
)
44+
45+
const maximumPromotionAmount = MathBN.mult(promotion.value, maximumQuantity)
46+
47+
return MathBN.min(maximumPromotionAmount, lineItemAmount)
3148
}
3249

33-
export function getPromotionValue(promotion, lineItemAmount, lineItemsAmount) {
50+
export function getPromotionValue(
51+
promotion,
52+
lineItemAmount,
53+
lineItemsAmount,
54+
lineItem
55+
) {
3456
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
3557
return getPromotionValueForPercentage(promotion, lineItemAmount)
3658
}
3759

38-
return getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount)
60+
return getPromotionValueForFixed(
61+
promotion,
62+
lineItemAmount,
63+
lineItemsAmount,
64+
lineItem
65+
)
3966
}
4067

4168
export function getApplicableQuantity(lineItem, maxQuantity) {
@@ -105,7 +132,8 @@ export function calculateAdjustmentAmountFromPromotion(
105132
const promotionValue = getPromotionValue(
106133
promotion,
107134
applicableAmount,
108-
lineItemsAmount
135+
lineItemsAmount,
136+
lineItem
109137
)
110138

111139
const returnValue = MathBN.min(promotionValue, applicableAmount)
@@ -139,14 +167,17 @@ export function calculateAdjustmentAmountFromPromotion(
139167
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
140168
promotion.applied_value
141169
)
170+
142171
const itemAmount = MathBN.div(
143172
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
144173
lineItem.quantity
145174
)
175+
146176
const maximumPromotionAmount = MathBN.mult(
147177
itemAmount,
148178
promotion.max_quantity ?? MathBN.convert(1)
149179
)
180+
150181
const applicableAmount = MathBN.min(
151182
remainingItemAmount,
152183
maximumPromotionAmount
@@ -159,7 +190,8 @@ export function calculateAdjustmentAmountFromPromotion(
159190
const promotionValue = getPromotionValue(
160191
promotion,
161192
applicableAmount,
162-
lineItemsAmount
193+
lineItemsAmount,
194+
lineItem
163195
)
164196

165197
const returnValue = MathBN.min(promotionValue, applicableAmount)

packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ moduleIntegrationTestRunner({
464464
type: "fixed",
465465
target_type: "items",
466466
allocation: "each",
467-
value: 500,
467+
value: 100,
468468
max_quantity: 5,
469469
target_rules: [
470470
{

0 commit comments

Comments
 (0)