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:
7
.changeset/sour-horses-beam.md
Normal file
7
.changeset/sour-horses-beam.md
Normal 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
|
||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user