From bd6d9777c50d69115a20334a103a8ab9997b259d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:23:06 +0200 Subject: [PATCH] fix(promotion, types): non discountable items check (#12644) * fix(promotions): check if item is discountable * fix: return earl yonly if non discountable * fix: update test * chore: add integration test --- .changeset/proud-shirts-leave.md | 6 ++ .../http/__tests__/cart/store/cart.spec.ts | 90 +++++++++++++++++++ .../cart/store/add-promotions-to-cart.spec.ts | 65 ++++++++++++++ .../src/promotion/common/compute-actions.ts | 5 ++ .../src/utils/compute-actions/line-items.ts | 10 ++- 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 .changeset/proud-shirts-leave.md diff --git a/.changeset/proud-shirts-leave.md b/.changeset/proud-shirts-leave.md new file mode 100644 index 0000000000..33f8382bf6 --- /dev/null +++ b/.changeset/proud-shirts-leave.md @@ -0,0 +1,6 @@ +--- +"@medusajs/promotion": patch +"@medusajs/types": patch +--- + +fix(promotion, types): non discountable items check diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 1817473f61..96571e9f0d 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -2621,6 +2621,96 @@ medusaIntegrationTestRunner({ ) }) + it("should only apply promotion on discountable items", async () => { + const notDiscountableProduct = ( + await api.post( + "/admin/products", + { + title: "Medusa T-Shirt not discountable", + handle: "t-shirt-not-discountable", + discountable: false, + options: [ + { + title: "Size", + values: ["S"], + }, + ], + variants: [ + { + title: "S", + sku: "s-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 1000, + currency_code: "usd", + }, + ], + }, + ], + + shipping_profile_id: shippingProfile.id, + }, + adminHeaders + ) + ).data.product + + const cartData = { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + items: [ + { variant_id: product.variants[0].id, quantity: 1 }, + { + variant_id: notDiscountableProduct.variants[0].id, + quantity: 1, + }, + ], + promo_codes: [promotion.code], + } + + const cart = ( + await api.post( + `/store/carts?fields=+items.is_discountable,+items.total,+items.discount_total`, + cartData, + storeHeaders + ) + ).data.cart + + expect(cart).toEqual( + expect.objectContaining({ + discount_subtotal: 100, + items: expect.arrayContaining([ + expect.objectContaining({ + variant_id: product.variants[0].id, + is_discountable: true, + unit_price: 1500, + total: 1395, + discount_total: 100, + adjustments: [ + expect.objectContaining({ + promotion_id: promotion.id, + amount: 100, + }), + ], + }), + expect.objectContaining({ + variant_id: notDiscountableProduct.variants[0].id, + is_discountable: false, + total: 1000, + unit_price: 1000, + discount_total: 0, + adjustments: [], + }), + ]), + }) + ) + }) + it("should remove promotion adjustments when promotion is deleted", async () => { let cartBeforeRemovingPromotion = ( await api.get(`/store/carts/${cart.id}`, storeHeaders) diff --git a/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts b/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts index 866f93689d..33d5f482ca 100644 --- a/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts @@ -162,6 +162,71 @@ medusaIntegrationTestRunner({ ) }) + it("should add line item adjustments only for discountable items", async () => { + const createdPromotion = + await promotionModuleService.createPromotions({ + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: 1000, + apply_to_quantity: 1, + currency_code: "usd", + }, + }) + + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + items: [ + { + id: "item-1", + unit_price: 2000, + quantity: 1, + title: "Test item", + product_id: "prod_mat", + } as any, + { + id: "item-2", + unit_price: 1000, + quantity: 1, + title: "Test item", + product_id: "prod_tshirt", + is_discountable: false, + } as any, + ], + }) + + const created = await api.post( + `/store/carts/${cart.id}/promotions`, + { promo_codes: [createdPromotion.code] }, + storeHeaders + ) + + expect(created.status).toEqual(200) + expect(created.data.cart).toEqual( + expect.objectContaining({ + id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + id: "item-1", + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: createdPromotion.code, + amount: 1000, + }), + ]), + }), + expect.objectContaining({ + adjustments: [], + }), + ]), + }) + ) + }) + it("should add shipping method adjustments to a cart based on promotions", async () => { const [appliedPromotion] = await promotionModuleService.createPromotions([ diff --git a/packages/core/types/src/promotion/common/compute-actions.ts b/packages/core/types/src/promotion/common/compute-actions.ts index 065bc2506b..c49e306d5d 100644 --- a/packages/core/types/src/promotion/common/compute-actions.ts +++ b/packages/core/types/src/promotion/common/compute-actions.ts @@ -180,6 +180,11 @@ export interface ComputeActionItemLine extends Record { */ subtotal: BigNumberInput + /** + * Whether the line item is discountable. + */ + is_discountable: boolean + /** * The adjustments applied before on the line item. */ 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 4119db1c20..1bb6d96f49 100644 --- a/packages/modules/promotion/src/utils/compute-actions/line-items.ts +++ b/packages/modules/promotion/src/utils/compute-actions/line-items.ts @@ -201,7 +201,15 @@ function getValidItemsForPromotion( } return items.filter((item) => { - if (!item || !("subtotal" in item) || MathBN.lte(item.subtotal, 0)) { + if (!item) { + return false + } + + if ("is_discountable" in item && !item.is_discountable) { + return false + } + + if (!("subtotal" in item) || MathBN.lte(item.subtotal, 0)) { return false }