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:
Riqwan Thamir
2025-08-17 19:11:16 +02:00
committed by GitHub
parent 0128ed314c
commit 3cc512ef39
4 changed files with 150 additions and 12 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/utils": patch
---
fix(utils): fix promotion case of each allocation not applying its amount

View File

@@ -3321,18 +3321,18 @@ medusaIntegrationTestRunner({
expect(updated.status).toEqual(200)
expect(updated.data.cart).toEqual(
expect.objectContaining({
discount_total: 105,
discount_subtotal: 100,
discount_tax_total: 5,
discount_total: 210,
discount_subtotal: 200,
discount_tax_total: 10,
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([
expect.objectContaining({
is_tax_inclusive: true,
adjustments: expect.arrayContaining([
expect.objectContaining({
code: taxInclPromotion.code,
amount: 105,
amount: 210,
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", () => {
it("should apply a percentage promotion to a cart", async () => {
const percentagePromotion = (

View File

@@ -9,7 +9,12 @@ function getPromotionValueForPercentage(promotion, 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) {
const promotionValueForItem = MathBN.mult(
MathBN.div(lineItemAmount, lineItemsAmount),
@@ -27,15 +32,37 @@ function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) {
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) {
return getPromotionValueForPercentage(promotion, lineItemAmount)
}
return getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount)
return getPromotionValueForFixed(
promotion,
lineItemAmount,
lineItemsAmount,
lineItem
)
}
export function getApplicableQuantity(lineItem, maxQuantity) {
@@ -105,7 +132,8 @@ export function calculateAdjustmentAmountFromPromotion(
const promotionValue = getPromotionValue(
promotion,
applicableAmount,
lineItemsAmount
lineItemsAmount,
lineItem
)
const returnValue = MathBN.min(promotionValue, applicableAmount)
@@ -139,14 +167,17 @@ export function calculateAdjustmentAmountFromPromotion(
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
promotion.applied_value
)
const itemAmount = MathBN.div(
promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
lineItem.quantity
)
const maximumPromotionAmount = MathBN.mult(
itemAmount,
promotion.max_quantity ?? MathBN.convert(1)
)
const applicableAmount = MathBN.min(
remainingItemAmount,
maximumPromotionAmount
@@ -159,7 +190,8 @@ export function calculateAdjustmentAmountFromPromotion(
const promotionValue = getPromotionValue(
promotion,
applicableAmount,
lineItemsAmount
lineItemsAmount,
lineItem
)
const returnValue = MathBN.min(promotionValue, applicableAmount)

View File

@@ -464,7 +464,7 @@ moduleIntegrationTestRunner({
type: "fixed",
target_type: "items",
allocation: "each",
value: 500,
value: 100,
max_quantity: 5,
target_rules: [
{