From 7345e6dde3da011a46b1d36a3c34e606d090b3f2 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:09:54 +0100 Subject: [PATCH 1/3] fix(pricing): PriceLists of type Sale should not override lower prices --- .changeset/quick-buttons-raise.md | 5 + .../__tests__/product/store/product.spec.ts | 166 ++++++++++++++++++ .../pricing-module/calculate-price.spec.ts | 123 +++++++++++++ .../pricing/src/services/pricing-module.ts | 39 +++- 4 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 .changeset/quick-buttons-raise.md diff --git a/.changeset/quick-buttons-raise.md b/.changeset/quick-buttons-raise.md new file mode 100644 index 0000000000000..6388a558a3344 --- /dev/null +++ b/.changeset/quick-buttons-raise.md @@ -0,0 +1,5 @@ +--- +"@medusajs/pricing": patch +--- + +fix(pricing): PriceLists of type Sale should not override lower prices diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index 503adfb9961d9..248ab71826099 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -1172,6 +1172,89 @@ medusaIntegrationTestRunner({ expect(response.data.products).toEqual(expectation) }) + it("should list products with prices with a default price when the price list price is higher and the price list is of type SALE", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 3500, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { "customer.groups.id": [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: false, + is_calculated_price_tax_inclusive: false, + calculated_amount: 3000, + raw_calculated_amount: { + value: "3000", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 3000, + raw_original_amount: { + value: "3000", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }), + ]) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.products).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual(expectation) + }) + it("should list products with prices with a override price list price", async () => { const priceList = ( await api.post( @@ -1254,6 +1337,89 @@ medusaIntegrationTestRunner({ expect(response.status).toEqual(200) expect(response.data.products).toEqual(expectation) }) + + it("should list products with prices with a override price list price even if the price list price is higher than the default price", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 35000, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { "customer.groups.id": [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 35000, + raw_calculated_amount: { + value: "35000", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 35000, + raw_original_amount: { + value: "35000", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }), + ]) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.products).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual(expectation) + }) }) describe("with inventory items", () => { diff --git a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts index c2e6c4269991a..998f3a04dc64f 100644 --- a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts +++ b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -1167,6 +1167,129 @@ moduleIntegrationTestRunner({ ]) }) + it("should return default prices when the price list price is higher than the default price when the price list is of type SALE", async () => { + await createPriceLists(service, undefined, undefined, [ + { + amount: 2500, + currency_code: "PLN", + price_set_id: "price-set-PLN", + }, + { + amount: 2500, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ]) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: false, + is_calculated_price_tax_inclusive: false, + calculated_amount: 1000, + raw_calculated_amount: { + value: "1000", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 1000, + raw_original_amount: { + value: "1000", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 10, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 10, + }, + }, + ]) + }) + + it("should return price list prices even if the price list price is higher than the default price when the price list is of type OVERRIDE", async () => { + await createPriceLists( + service, + { type: PriceListType.OVERRIDE }, + {}, + [ + { + amount: 2500, + currency_code: "PLN", + price_set_id: "price-set-PLN", + }, + { + amount: 2500, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ] + ) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 2500, + raw_calculated_amount: { + value: "2500", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 2500, + raw_original_amount: { + value: "2500", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + ]) + }) + it("should return price list prices when price list conditions match for override", async () => { await createPriceLists(service, { type: PriceListType.OVERRIDE }) diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index cf0d3eabab05d..bcc437b352ce8 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -31,6 +31,7 @@ import { InjectTransactionManager, isPresent, isString, + MathBN, MedusaContext, MedusaError, ModulesSdkUtils, @@ -50,10 +51,10 @@ import { PriceSet, } from "@models" +import { Collection } from "@mikro-orm/core" import { ServiceTypes } from "@types" import { eventBuilders, validatePriceListDates } from "@utils" import { joinerConfig } from "../joiner-config" -import { Collection } from "@mikro-orm/core" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -288,11 +289,41 @@ export default class PricingModuleService let originalPrice: PricingTypes.CalculatedPriceSetDTO | undefined = defaultPrice + /** + * When deciding which price to use we follow the following logic: + * - If the price list is of type OVERRIDE, we always use the price list price. + * - If the price list is of type SALE, we use the lowest price between the price list price and the default price + */ if (priceListPrice) { - calculatedPrice = priceListPrice + switch (priceListPrice.price_list_type) { + case PriceListType.OVERRIDE: + calculatedPrice = priceListPrice + originalPrice = priceListPrice + break + case PriceListType.SALE: { + let lowestPrice = priceListPrice + + const defaultPriceAmount = defaultPrice?.amount + ? MathBN.convert(defaultPrice.amount) + : null + const priceListPriceAmount = priceListPrice.amount + ? MathBN.convert(priceListPrice.amount) + : null + + if ( + defaultPriceAmount && + priceListPriceAmount && + defaultPrice && + priceListPrice + ) { + lowestPrice = priceListPriceAmount.lte(defaultPriceAmount) + ? priceListPrice + : defaultPrice + } - if (priceListPrice.price_list_type === PriceListType.OVERRIDE) { - originalPrice = priceListPrice + calculatedPrice = lowestPrice + break + } } } From d4b8aaa217bc867dbfa49e61d4b67a8c6479490c Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:10:38 +0100 Subject: [PATCH 2/3] update changeset --- .changeset/quick-buttons-raise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quick-buttons-raise.md b/.changeset/quick-buttons-raise.md index 6388a558a3344..8cb128226fb2a 100644 --- a/.changeset/quick-buttons-raise.md +++ b/.changeset/quick-buttons-raise.md @@ -2,4 +2,4 @@ "@medusajs/pricing": patch --- -fix(pricing): PriceLists of type Sale should not override lower prices +fix(pricing): PriceLists of type Sale no longer override default prices when the price list price is higher than the default price. From 1a296d0af50c4d55cf3f2e1926cc2b4bc1ab167a Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:30:02 +0100 Subject: [PATCH 3/3] rm conversion --- .../pricing/src/services/pricing-module.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index bcc437b352ce8..0b5d7ef3c68df 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -303,20 +303,11 @@ export default class PricingModuleService case PriceListType.SALE: { let lowestPrice = priceListPrice - const defaultPriceAmount = defaultPrice?.amount - ? MathBN.convert(defaultPrice.amount) - : null - const priceListPriceAmount = priceListPrice.amount - ? MathBN.convert(priceListPrice.amount) - : null - - if ( - defaultPriceAmount && - priceListPriceAmount && - defaultPrice && - priceListPrice - ) { - lowestPrice = priceListPriceAmount.lte(defaultPriceAmount) + if (defaultPrice?.amount && priceListPrice.amount) { + lowestPrice = MathBN.lte( + priceListPrice.amount, + defaultPrice.amount + ) ? priceListPrice : defaultPrice }