fix(types,utils,promotion): Move from total to original_total to resolve edge case for adjustments calculation (#13106)

* Move from total to original_total to resolve edge case in adjustment calculation

* Added changeset

* Added test case for correction

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
scherddel
2025-08-01 12:52:04 +02:00
committed by GitHub
parent b37a87c355
commit 9766570827
5 changed files with 156 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/promotion": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
Moved calculation logic from total to original_total to ensure consistent base values

View File

@@ -3463,6 +3463,143 @@ medusaIntegrationTestRunner({
}) })
) )
}) })
it("should verify that reapplying the same promotion code after the cart total has been reduced to zero does not incorrectly remove existing adjustments", async () => {
const taxInclPromotion = (
await api.post(
`/admin/promotions`,
{
code: "PROMOTION_TAX_INCLUSIVE",
type: PromotionType.STANDARD,
status: PromotionStatus.ACTIVE,
is_tax_inclusive: true,
application_method: {
type: "fixed",
target_type: "items",
allocation: "across",
currency_code: "usd",
value: 50,
},
},
adminHeaders
)
).data.promotion
const product = (
await api.post(
`/admin/products`,
{
title: "Product for free",
description: "test",
options: [
{
title: "Size",
values: ["S", "M", "L", "XL"],
},
],
variants: [
{
title: "S / Black",
sku: "special-shirt",
options: {
Size: "S",
},
manage_inventory: false,
prices: [
{
amount: 50,
currency_code: "usd",
},
],
},
],
},
adminHeaders
)
).data.product
cart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
shipping_address: shippingAddressData,
},
storeHeadersWithCustomer
)
).data.cart
cart = (
await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 1,
},
storeHeaders
)
).data.cart
let updated = await api.post(
`/store/carts/${cart.id}`,
{
promo_codes: [taxInclPromotion.code],
},
storeHeaders
)
expect(updated.status).toEqual(200)
expect(updated.data.cart).toEqual(
expect.objectContaining({
discount_total: 50,
original_total: 50,
total: 0,
items: expect.arrayContaining([
expect.objectContaining({
is_tax_inclusive: true,
adjustments: expect.arrayContaining([
expect.objectContaining({
code: taxInclPromotion.code,
amount: 50,
is_tax_inclusive: true,
}),
]),
}),
]),
})
)
let updatedAgain = await api.post(
`/store/carts/${cart.id}`,
{
promo_codes: [taxInclPromotion.code],
},
storeHeaders
)
expect(updatedAgain.status).toEqual(200)
expect(updatedAgain.data.cart).toEqual(
expect.objectContaining({
discount_total: 50,
original_total: 50,
total: 0,
items: expect.arrayContaining([
expect.objectContaining({
is_tax_inclusive: true,
adjustments: expect.arrayContaining([
expect.objectContaining({
code: taxInclPromotion.code,
amount: 50,
is_tax_inclusive: true,
}),
]),
}),
]),
})
)
})
}) })
describe("POST /store/carts/:id/customer", () => { describe("POST /store/carts/:id/customer", () => {

View File

@@ -188,7 +188,7 @@ export interface ComputeActionItemLine extends Record<string, unknown> {
/** /**
* The total of the line item. * The total of the line item.
*/ */
total: BigNumberInput original_total: BigNumberInput
/** /**
* Whether the line item is discountable. * Whether the line item is discountable.
@@ -218,7 +218,7 @@ export interface ComputeActionShippingLine extends Record<string, unknown> {
/** /**
* The total of the shipping method. * The total of the shipping method.
*/ */
total: BigNumberInput original_total: BigNumberInput
/** /**
* The adjustments applied before on the shipping method. * The adjustments applied before on the shipping method.

View File

@@ -53,8 +53,8 @@ function getLineItemSubtotal(lineItem) {
return MathBN.div(lineItem.subtotal, lineItem.quantity) return MathBN.div(lineItem.subtotal, lineItem.quantity)
} }
function getLineItemTotal(lineItem) { function getLineItemOriginalTotal(lineItem) {
return MathBN.div(lineItem.total, lineItem.quantity) return MathBN.div(lineItem.original_total, lineItem.quantity)
} }
export function calculateAdjustmentAmountFromPromotion( export function calculateAdjustmentAmountFromPromotion(
@@ -95,7 +95,7 @@ export function calculateAdjustmentAmountFromPromotion(
const lineItemAmount = MathBN.mult( const lineItemAmount = MathBN.mult(
promotion.is_tax_inclusive promotion.is_tax_inclusive
? getLineItemTotal(lineItem) ? getLineItemOriginalTotal(lineItem)
: getLineItemSubtotal(lineItem), : getLineItemSubtotal(lineItem),
quantity quantity
) )
@@ -134,11 +134,11 @@ export function calculateAdjustmentAmountFromPromotion(
*/ */
const remainingItemAmount = MathBN.sub( const remainingItemAmount = MathBN.sub(
promotion.is_tax_inclusive ? lineItem.total : lineItem.subtotal, promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
promotion.applied_value promotion.applied_value
) )
const itemAmount = MathBN.div( const itemAmount = MathBN.div(
promotion.is_tax_inclusive ? lineItem.total : lineItem.subtotal, promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal,
lineItem.quantity lineItem.quantity
) )
const maximumPromotionAmount = MathBN.mult( const maximumPromotionAmount = MathBN.mult(

View File

@@ -110,7 +110,7 @@ function applyPromotionToItems(
MathBN.sub( MathBN.sub(
MathBN.add( MathBN.add(
acc, acc,
promotion.is_tax_inclusive ? item.total : item.subtotal promotion.is_tax_inclusive ? item.original_total : item.subtotal
), ),
appliedPromotionsMap.get(item.id) ?? 0 appliedPromotionsMap.get(item.id) ?? 0
), ),
@@ -124,7 +124,10 @@ function applyPromotionToItems(
for (const item of applicableItems) { for (const item of applicableItems) {
if ( if (
MathBN.lte(promotion.is_tax_inclusive ? item.total : item.subtotal, 0) MathBN.lte(
promotion.is_tax_inclusive ? item.original_total : item.subtotal,
0
)
) { ) {
continue continue
} }