Skip to content

Commit 02dd83f

Browse files
authored
fix(pricing): Calculate prices with multiple rule values (medusajs#13079)
1 parent 8616248 commit 02dd83f

File tree

4 files changed

+202
-35
lines changed

4 files changed

+202
-35
lines changed

.changeset/dirty-boats-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/pricing": minor
3+
---
4+
5+
feat(pricing): Calculate prices with multiple rule values

packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts

Lines changed: 172 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
473473
{ context }
474474
)
475475

476-
477476
expect(calculatedPrice).toEqual([
478477
{
479478
id: "price-set-PLN",
@@ -1233,19 +1232,35 @@ moduleIntegrationTestRunner<IPricingModuleService>({
12331232
])
12341233
})
12351234

1236-
it("should return best price list price first when price list conditions match", async () => {
1237-
await createPriceLists(service)
1235+
it("should return cheapest price list price first when price list conditions match", async () => {
12381236
await createPriceLists(
12391237
service,
1238+
{
1239+
title: "Test Price List One",
1240+
description: "test description",
1241+
type: PriceListType.OVERRIDE,
1242+
status: PriceListStatus.ACTIVE,
1243+
},
12401244
{},
1245+
defaultPriceListPrices
1246+
)
1247+
1248+
await createPriceLists(
1249+
service,
1250+
{
1251+
title: "Test Price List Two",
1252+
description: "test description",
1253+
type: PriceListType.OVERRIDE,
1254+
status: PriceListStatus.ACTIVE,
1255+
},
12411256
{},
12421257
defaultPriceListPrices.map((price) => {
12431258
return { ...price, amount: price.amount / 2 }
12441259
})
12451260
)
12461261

12471262
const priceSetsResult = await service.calculatePrices(
1248-
{ id: ["price-set-EUR", "price-set-PLN"] },
1263+
{ id: ["price-set-PLN"] },
12491264
{
12501265
context: {
12511266
currency_code: "PLN",
@@ -1261,32 +1276,32 @@ moduleIntegrationTestRunner<IPricingModuleService>({
12611276
id: "price-set-PLN",
12621277
is_calculated_price_price_list: true,
12631278
is_calculated_price_tax_inclusive: false,
1264-
calculated_amount: 232,
1279+
calculated_amount: 116,
12651280
raw_calculated_amount: {
1266-
value: "232",
1281+
value: "116",
12671282
precision: 20,
12681283
},
1269-
is_original_price_price_list: false,
1284+
is_original_price_price_list: true,
12701285
is_original_price_tax_inclusive: false,
1271-
original_amount: 400,
1286+
original_amount: 116,
12721287
raw_original_amount: {
1273-
value: "400",
1288+
value: "116",
12741289
precision: 20,
12751290
},
12761291
currency_code: "PLN",
12771292
calculated_price: {
12781293
id: expect.any(String),
12791294
price_list_id: expect.any(String),
1280-
price_list_type: "sale",
1295+
price_list_type: "override",
12811296
min_quantity: null,
12821297
max_quantity: null,
12831298
},
12841299
original_price: {
12851300
id: expect.any(String),
1286-
price_list_id: null,
1287-
price_list_type: null,
1288-
min_quantity: 1,
1289-
max_quantity: 5,
1301+
price_list_id: expect.any(String),
1302+
price_list_type: "override",
1303+
min_quantity: null,
1304+
max_quantity: null,
12901305
},
12911306
},
12921307
])
@@ -1978,6 +1993,149 @@ moduleIntegrationTestRunner<IPricingModuleService>({
19781993
])
19791994
})
19801995

1996+
it("should return price list prices for multiple price lists with customer groups", async () => {
1997+
const [{ id }] = await createPriceLists(
1998+
service,
1999+
{ type: "override" },
2000+
{
2001+
["customer.groups.id"]: ["vip-customer-group-id"],
2002+
},
2003+
[
2004+
{
2005+
amount: 600,
2006+
currency_code: "EUR",
2007+
price_set_id: "price-set-EUR",
2008+
},
2009+
]
2010+
)
2011+
2012+
const [{ id: idTwo }] = await createPriceLists(
2013+
service,
2014+
{ type: "override" },
2015+
{
2016+
["customer.groups.id"]: ["vip-customer-group-id-1"],
2017+
},
2018+
[
2019+
{
2020+
amount: 400,
2021+
currency_code: "EUR",
2022+
price_set_id: "price-set-EUR",
2023+
},
2024+
]
2025+
)
2026+
2027+
const priceSetsResult = await service.calculatePrices(
2028+
{ id: ["price-set-EUR"] },
2029+
{
2030+
context: {
2031+
currency_code: "EUR",
2032+
// @ts-ignore
2033+
customer: {
2034+
groups: {
2035+
id: ["vip-customer-group-id", "vip-customer-group-id-1"],
2036+
},
2037+
},
2038+
},
2039+
}
2040+
)
2041+
2042+
expect(priceSetsResult).toEqual([
2043+
{
2044+
id: "price-set-EUR",
2045+
is_calculated_price_price_list: true,
2046+
is_calculated_price_tax_inclusive: false,
2047+
calculated_amount: 400,
2048+
raw_calculated_amount: {
2049+
value: "400",
2050+
precision: 20,
2051+
},
2052+
is_original_price_price_list: true,
2053+
is_original_price_tax_inclusive: false,
2054+
original_amount: 400,
2055+
raw_original_amount: {
2056+
value: "400",
2057+
precision: 20,
2058+
},
2059+
currency_code: "EUR",
2060+
calculated_price: {
2061+
id: expect.any(String),
2062+
price_list_id: idTwo,
2063+
price_list_type: "override",
2064+
min_quantity: null,
2065+
max_quantity: null,
2066+
},
2067+
original_price: {
2068+
id: expect.any(String),
2069+
price_list_id: idTwo,
2070+
price_list_type: "override",
2071+
min_quantity: null,
2072+
max_quantity: null,
2073+
},
2074+
},
2075+
])
2076+
})
2077+
2078+
it("should return price list prices when price list conditions match within prices", async () => {
2079+
await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [
2080+
...defaultPriceListPrices,
2081+
{
2082+
amount: 111,
2083+
currency_code: "PLN",
2084+
price_set_id: "price-set-PLN",
2085+
rules: {
2086+
region_id: "DE",
2087+
},
2088+
},
2089+
])
2090+
2091+
const priceSetsResult = await service.calculatePrices(
2092+
{ id: ["price-set-EUR", "price-set-PLN"] },
2093+
{
2094+
context: {
2095+
currency_code: "PLN",
2096+
region_id: "DE",
2097+
customer_group_id: "vip-customer-group-id",
2098+
company_id: "medusa-company-id",
2099+
},
2100+
}
2101+
)
2102+
2103+
expect(priceSetsResult).toEqual([
2104+
{
2105+
id: "price-set-PLN",
2106+
is_calculated_price_price_list: true,
2107+
is_calculated_price_tax_inclusive: false,
2108+
calculated_amount: 111,
2109+
raw_calculated_amount: {
2110+
value: "111",
2111+
precision: 20,
2112+
},
2113+
is_original_price_price_list: false,
2114+
is_original_price_tax_inclusive: false,
2115+
original_amount: 400,
2116+
raw_original_amount: {
2117+
value: "400",
2118+
precision: 20,
2119+
},
2120+
currency_code: "PLN",
2121+
calculated_price: {
2122+
id: expect.any(String),
2123+
price_list_id: expect.any(String),
2124+
price_list_type: "sale",
2125+
min_quantity: null,
2126+
max_quantity: null,
2127+
},
2128+
original_price: {
2129+
id: expect.any(String),
2130+
price_list_id: null,
2131+
price_list_type: null,
2132+
min_quantity: 1,
2133+
max_quantity: 5,
2134+
},
2135+
},
2136+
])
2137+
})
2138+
19812139
it("should not return price list prices when price list conditions are met but price rules are not", async () => {
19822140
await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [
19832141
...defaultPriceListPrices,

packages/modules/pricing/src/repositories/pricing.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ export class PricingRepository
180180
WHERE pr.price_id = price.id
181181
AND pr.deleted_at IS NULL
182182
AND (
183-
${flattenedContext
184-
.map(([key, value]) => {
185-
if (typeof value === "number") {
186-
return `
183+
${flattenedContext
184+
.map(([key, value]) => {
185+
if (typeof value === "number") {
186+
return `
187187
(pr.attribute = ? AND (
188188
(pr.operator = 'eq' AND pr.value = ?) OR
189189
(pr.operator = 'gt' AND ? > pr.value::numeric) OR
@@ -192,16 +192,13 @@ export class PricingRepository
192192
(pr.operator = 'lte' AND ? <= pr.value::numeric)
193193
))
194194
`
195-
} else {
196-
const normalizeValue = Array.isArray(value)
197-
? value
198-
: [value]
199-
const placeholders = normalizeValue.map(() => "?").join(",")
200-
return `(pr.attribute = ? AND pr.value IN (${placeholders}))`
201-
}
202-
})
203-
.join(" OR ")}
204-
)
195+
} else {
196+
const normalizeValue = Array.isArray(value) ? value : [value]
197+
const placeholders = normalizeValue.map(() => "?").join(",")
198+
return `(pr.attribute = ? AND pr.value IN (${placeholders}))`
199+
}
200+
})
201+
.join(" OR ")})
205202
) = (
206203
/* Get total rule count */
207204
SELECT COUNT(*)
@@ -232,11 +229,16 @@ export class PricingRepository
232229
WHERE plr.price_list_id = pl.id
233230
AND plr.deleted_at IS NULL
234231
AND (
235-
${flattenedContext
236-
.map(([key, value]) => {
237-
return `(plr.attribute = ? AND plr.value @> ?)`
238-
})
239-
.join(" OR ")}
232+
${flattenedContext
233+
.map(([key, value]) => {
234+
if (Array.isArray(value)) {
235+
return value
236+
.map((v) => `(plr.attribute = ? AND plr.value @> ?)`)
237+
.join(" OR ")
238+
}
239+
return `(plr.attribute = ? AND plr.value @> ?)`
240+
})
241+
.join(" OR ")}
240242
)
241243
) = (
242244
/* Get total rule count */
@@ -248,7 +250,8 @@ export class PricingRepository
248250
)
249251
`,
250252
flattenedContext.flatMap(([key, value]) => {
251-
return [key, JSON.stringify(Array.isArray(value) ? value : [value])]
253+
const valueAsArray = Array.isArray(value) ? value : [value]
254+
return valueAsArray.flatMap((v) => [key, JSON.stringify(v)])
252255
})
253256
)
254257

@@ -275,7 +278,6 @@ export class PricingRepository
275278
query
276279
.orderByRaw("price.price_list_id IS NOT NULL DESC")
277280
.orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC")
278-
.orderBy("pl.id", "asc")
279281
.orderBy("price.amount", "asc")
280282

281283
return await query

packages/modules/pricing/src/services/pricing-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export default class PricingModuleService
343343
/**
344344
* When deciding which price to use we follow the following logic:
345345
* - If the price list is of type OVERRIDE, we always use the price list price.
346+
* - If there are multiple price list prices of type OVERRIDE, we use the one with the lowest amount.
346347
* - If the price list is of type SALE, we use the lowest price between the price list price and the default price
347348
*/
348349
if (priceListPrice) {
@@ -369,6 +370,7 @@ export default class PricingModuleService
369370
}
370371
}
371372

373+
372374
pricesSetPricesMap.set(key, { calculatedPrice, originalPrice })
373375
priceIds.push(
374376
...(deduplicate(

0 commit comments

Comments
 (0)