diff --git a/.changeset/dirty-boats-add.md b/.changeset/dirty-boats-add.md new file mode 100644 index 0000000000..74bdf3fe65 --- /dev/null +++ b/.changeset/dirty-boats-add.md @@ -0,0 +1,5 @@ +--- +"@medusajs/pricing": minor +--- + +feat(pricing): Calculate prices with multiple rule values 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 2d3239e36a..8e441a5250 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 @@ -473,7 +473,6 @@ moduleIntegrationTestRunner({ { context } ) - expect(calculatedPrice).toEqual([ { id: "price-set-PLN", @@ -1233,11 +1232,27 @@ moduleIntegrationTestRunner({ ]) }) - it("should return best price list price first when price list conditions match", async () => { - await createPriceLists(service) + it("should return cheapest price list price first when price list conditions match", async () => { await createPriceLists( service, + { + title: "Test Price List One", + description: "test description", + type: PriceListType.OVERRIDE, + status: PriceListStatus.ACTIVE, + }, {}, + defaultPriceListPrices + ) + + await createPriceLists( + service, + { + title: "Test Price List Two", + description: "test description", + type: PriceListType.OVERRIDE, + status: PriceListStatus.ACTIVE, + }, {}, defaultPriceListPrices.map((price) => { return { ...price, amount: price.amount / 2 } @@ -1245,7 +1260,7 @@ moduleIntegrationTestRunner({ ) const priceSetsResult = await service.calculatePrices( - { id: ["price-set-EUR", "price-set-PLN"] }, + { id: ["price-set-PLN"] }, { context: { currency_code: "PLN", @@ -1261,32 +1276,32 @@ moduleIntegrationTestRunner({ id: "price-set-PLN", is_calculated_price_price_list: true, is_calculated_price_tax_inclusive: false, - calculated_amount: 232, + calculated_amount: 116, raw_calculated_amount: { - value: "232", + value: "116", precision: 20, }, - is_original_price_price_list: false, + is_original_price_price_list: true, is_original_price_tax_inclusive: false, - original_amount: 400, + original_amount: 116, raw_original_amount: { - value: "400", + value: "116", precision: 20, }, currency_code: "PLN", calculated_price: { id: expect.any(String), price_list_id: expect.any(String), - price_list_type: "sale", + price_list_type: "override", min_quantity: null, max_quantity: null, }, original_price: { id: expect.any(String), - price_list_id: null, - price_list_type: null, - min_quantity: 1, - max_quantity: 5, + price_list_id: expect.any(String), + price_list_type: "override", + min_quantity: null, + max_quantity: null, }, }, ]) @@ -1978,6 +1993,149 @@ moduleIntegrationTestRunner({ ]) }) + it("should return price list prices for multiple price lists with customer groups", async () => { + const [{ id }] = await createPriceLists( + service, + { type: "override" }, + { + ["customer.groups.id"]: ["vip-customer-group-id"], + }, + [ + { + amount: 600, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ] + ) + + const [{ id: idTwo }] = await createPriceLists( + service, + { type: "override" }, + { + ["customer.groups.id"]: ["vip-customer-group-id-1"], + }, + [ + { + amount: 400, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ] + ) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR"] }, + { + context: { + currency_code: "EUR", + // @ts-ignore + customer: { + groups: { + id: ["vip-customer-group-id", "vip-customer-group-id-1"], + }, + }, + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 400, + raw_calculated_amount: { + value: "400", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 400, + raw_original_amount: { + value: "400", + precision: 20, + }, + currency_code: "EUR", + calculated_price: { + id: expect.any(String), + price_list_id: idTwo, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: idTwo, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + ]) + }) + + it("should return price list prices when price list conditions match within prices", async () => { + await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [ + ...defaultPriceListPrices, + { + amount: 111, + currency_code: "PLN", + price_set_id: "price-set-PLN", + rules: { + region_id: "DE", + }, + }, + ]) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + region_id: "DE", + customer_group_id: "vip-customer-group-id", + company_id: "medusa-company-id", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 111, + raw_calculated_amount: { + value: "111", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 400, + raw_original_amount: { + value: "400", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "sale", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 5, + }, + }, + ]) + }) + it("should not return price list prices when price list conditions are met but price rules are not", async () => { await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [ ...defaultPriceListPrices, diff --git a/packages/modules/pricing/src/repositories/pricing.ts b/packages/modules/pricing/src/repositories/pricing.ts index 65f000d9a7..2d8ed67005 100644 --- a/packages/modules/pricing/src/repositories/pricing.ts +++ b/packages/modules/pricing/src/repositories/pricing.ts @@ -180,10 +180,10 @@ export class PricingRepository WHERE pr.price_id = price.id AND pr.deleted_at IS NULL AND ( - ${flattenedContext - .map(([key, value]) => { - if (typeof value === "number") { - return ` + ${flattenedContext + .map(([key, value]) => { + if (typeof value === "number") { + return ` (pr.attribute = ? AND ( (pr.operator = 'eq' AND pr.value = ?) OR (pr.operator = 'gt' AND ? > pr.value::numeric) OR @@ -192,16 +192,13 @@ export class PricingRepository (pr.operator = 'lte' AND ? <= pr.value::numeric) )) ` - } else { - const normalizeValue = Array.isArray(value) - ? value - : [value] - const placeholders = normalizeValue.map(() => "?").join(",") - return `(pr.attribute = ? AND pr.value IN (${placeholders}))` - } - }) - .join(" OR ")} - ) + } else { + const normalizeValue = Array.isArray(value) ? value : [value] + const placeholders = normalizeValue.map(() => "?").join(",") + return `(pr.attribute = ? AND pr.value IN (${placeholders}))` + } + }) + .join(" OR ")}) ) = ( /* Get total rule count */ SELECT COUNT(*) @@ -232,11 +229,16 @@ export class PricingRepository WHERE plr.price_list_id = pl.id AND plr.deleted_at IS NULL AND ( - ${flattenedContext - .map(([key, value]) => { - return `(plr.attribute = ? AND plr.value @> ?)` - }) - .join(" OR ")} + ${flattenedContext + .map(([key, value]) => { + if (Array.isArray(value)) { + return value + .map((v) => `(plr.attribute = ? AND plr.value @> ?)`) + .join(" OR ") + } + return `(plr.attribute = ? AND plr.value @> ?)` + }) + .join(" OR ")} ) ) = ( /* Get total rule count */ @@ -248,7 +250,8 @@ export class PricingRepository ) `, flattenedContext.flatMap(([key, value]) => { - return [key, JSON.stringify(Array.isArray(value) ? value : [value])] + const valueAsArray = Array.isArray(value) ? value : [value] + return valueAsArray.flatMap((v) => [key, JSON.stringify(v)]) }) ) @@ -275,7 +278,6 @@ export class PricingRepository query .orderByRaw("price.price_list_id IS NOT NULL DESC") .orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC") - .orderBy("pl.id", "asc") .orderBy("price.amount", "asc") return await query diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index 47673d4eb6..e015e177c7 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -343,6 +343,7 @@ export default class PricingModuleService /** * 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 there are multiple price list prices of type OVERRIDE, we use the one with the lowest amount. * - If the price list is of type SALE, we use the lowest price between the price list price and the default price */ if (priceListPrice) { @@ -369,6 +370,7 @@ export default class PricingModuleService } } + pricesSetPricesMap.set(key, { calculatedPrice, originalPrice }) priceIds.push( ...(deduplicate(