From 9766570827ebf50d49d8daf956deecce6666a8cc Mon Sep 17 00:00:00 2001 From: scherddel Date: Fri, 1 Aug 2025 12:52:04 +0200 Subject: [PATCH] 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> --- .changeset/sour-horses-beam.md | 7 + .../http/__tests__/cart/store/cart.spec.ts | 137 ++++++++++++++++++ .../src/promotion/common/compute-actions.ts | 4 +- .../core/utils/src/totals/promotion/index.ts | 10 +- .../src/utils/compute-actions/line-items.ts | 7 +- 5 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 .changeset/sour-horses-beam.md diff --git a/.changeset/sour-horses-beam.md b/.changeset/sour-horses-beam.md new file mode 100644 index 0000000000..4d5ebc7978 --- /dev/null +++ b/.changeset/sour-horses-beam.md @@ -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 diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 8e9a0fbede..ecee90b83d 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -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", () => { diff --git a/packages/core/types/src/promotion/common/compute-actions.ts b/packages/core/types/src/promotion/common/compute-actions.ts index 74dce1493d..da40d48a62 100644 --- a/packages/core/types/src/promotion/common/compute-actions.ts +++ b/packages/core/types/src/promotion/common/compute-actions.ts @@ -188,7 +188,7 @@ export interface ComputeActionItemLine extends Record { /** * The total of the line item. */ - total: BigNumberInput + original_total: BigNumberInput /** * Whether the line item is discountable. @@ -218,7 +218,7 @@ export interface ComputeActionShippingLine extends Record { /** * The total of the shipping method. */ - total: BigNumberInput + original_total: BigNumberInput /** * The adjustments applied before on the shipping method. diff --git a/packages/core/utils/src/totals/promotion/index.ts b/packages/core/utils/src/totals/promotion/index.ts index 2033c53502..86baefda83 100644 --- a/packages/core/utils/src/totals/promotion/index.ts +++ b/packages/core/utils/src/totals/promotion/index.ts @@ -53,8 +53,8 @@ function getLineItemSubtotal(lineItem) { return MathBN.div(lineItem.subtotal, lineItem.quantity) } -function getLineItemTotal(lineItem) { - return MathBN.div(lineItem.total, lineItem.quantity) +function getLineItemOriginalTotal(lineItem) { + return MathBN.div(lineItem.original_total, lineItem.quantity) } export function calculateAdjustmentAmountFromPromotion( @@ -95,7 +95,7 @@ export function calculateAdjustmentAmountFromPromotion( const lineItemAmount = MathBN.mult( promotion.is_tax_inclusive - ? getLineItemTotal(lineItem) + ? getLineItemOriginalTotal(lineItem) : getLineItemSubtotal(lineItem), quantity ) @@ -134,11 +134,11 @@ export function calculateAdjustmentAmountFromPromotion( */ const remainingItemAmount = MathBN.sub( - promotion.is_tax_inclusive ? lineItem.total : lineItem.subtotal, + promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal, promotion.applied_value ) const itemAmount = MathBN.div( - promotion.is_tax_inclusive ? lineItem.total : lineItem.subtotal, + promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal, lineItem.quantity ) const maximumPromotionAmount = MathBN.mult( diff --git a/packages/modules/promotion/src/utils/compute-actions/line-items.ts b/packages/modules/promotion/src/utils/compute-actions/line-items.ts index e2f270fcc9..1c1dcc89db 100644 --- a/packages/modules/promotion/src/utils/compute-actions/line-items.ts +++ b/packages/modules/promotion/src/utils/compute-actions/line-items.ts @@ -110,7 +110,7 @@ function applyPromotionToItems( MathBN.sub( MathBN.add( acc, - promotion.is_tax_inclusive ? item.total : item.subtotal + promotion.is_tax_inclusive ? item.original_total : item.subtotal ), appliedPromotionsMap.get(item.id) ?? 0 ), @@ -124,7 +124,10 @@ function applyPromotionToItems( for (const item of applicableItems) { 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 }