fix(pricing): Calculate prices with multiple rule values (#13079)
This commit is contained in:
5
.changeset/dirty-boats-add.md
Normal file
5
.changeset/dirty-boats-add.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/pricing": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(pricing): Calculate prices with multiple rule values
|
||||||
@@ -473,7 +473,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
|
|||||||
{ context }
|
{ context }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
expect(calculatedPrice).toEqual([
|
expect(calculatedPrice).toEqual([
|
||||||
{
|
{
|
||||||
id: "price-set-PLN",
|
id: "price-set-PLN",
|
||||||
@@ -1233,11 +1232,27 @@ moduleIntegrationTestRunner<IPricingModuleService>({
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return best price list price first when price list conditions match", async () => {
|
it("should return cheapest price list price first when price list conditions match", async () => {
|
||||||
await createPriceLists(service)
|
|
||||||
await createPriceLists(
|
await createPriceLists(
|
||||||
service,
|
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) => {
|
defaultPriceListPrices.map((price) => {
|
||||||
return { ...price, amount: price.amount / 2 }
|
return { ...price, amount: price.amount / 2 }
|
||||||
@@ -1245,7 +1260,7 @@ moduleIntegrationTestRunner<IPricingModuleService>({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const priceSetsResult = await service.calculatePrices(
|
const priceSetsResult = await service.calculatePrices(
|
||||||
{ id: ["price-set-EUR", "price-set-PLN"] },
|
{ id: ["price-set-PLN"] },
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
currency_code: "PLN",
|
currency_code: "PLN",
|
||||||
@@ -1261,32 +1276,32 @@ moduleIntegrationTestRunner<IPricingModuleService>({
|
|||||||
id: "price-set-PLN",
|
id: "price-set-PLN",
|
||||||
is_calculated_price_price_list: true,
|
is_calculated_price_price_list: true,
|
||||||
is_calculated_price_tax_inclusive: false,
|
is_calculated_price_tax_inclusive: false,
|
||||||
calculated_amount: 232,
|
calculated_amount: 116,
|
||||||
raw_calculated_amount: {
|
raw_calculated_amount: {
|
||||||
value: "232",
|
value: "116",
|
||||||
precision: 20,
|
precision: 20,
|
||||||
},
|
},
|
||||||
is_original_price_price_list: false,
|
is_original_price_price_list: true,
|
||||||
is_original_price_tax_inclusive: false,
|
is_original_price_tax_inclusive: false,
|
||||||
original_amount: 400,
|
original_amount: 116,
|
||||||
raw_original_amount: {
|
raw_original_amount: {
|
||||||
value: "400",
|
value: "116",
|
||||||
precision: 20,
|
precision: 20,
|
||||||
},
|
},
|
||||||
currency_code: "PLN",
|
currency_code: "PLN",
|
||||||
calculated_price: {
|
calculated_price: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
price_list_id: expect.any(String),
|
price_list_id: expect.any(String),
|
||||||
price_list_type: "sale",
|
price_list_type: "override",
|
||||||
min_quantity: null,
|
min_quantity: null,
|
||||||
max_quantity: null,
|
max_quantity: null,
|
||||||
},
|
},
|
||||||
original_price: {
|
original_price: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
price_list_id: null,
|
price_list_id: expect.any(String),
|
||||||
price_list_type: null,
|
price_list_type: "override",
|
||||||
min_quantity: 1,
|
min_quantity: null,
|
||||||
max_quantity: 5,
|
max_quantity: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -1978,6 +1993,149 @@ moduleIntegrationTestRunner<IPricingModuleService>({
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 () => {
|
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"] }, [
|
await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [
|
||||||
...defaultPriceListPrices,
|
...defaultPriceListPrices,
|
||||||
|
|||||||
@@ -180,10 +180,10 @@ export class PricingRepository
|
|||||||
WHERE pr.price_id = price.id
|
WHERE pr.price_id = price.id
|
||||||
AND pr.deleted_at IS NULL
|
AND pr.deleted_at IS NULL
|
||||||
AND (
|
AND (
|
||||||
${flattenedContext
|
${flattenedContext
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return `
|
return `
|
||||||
(pr.attribute = ? AND (
|
(pr.attribute = ? AND (
|
||||||
(pr.operator = 'eq' AND pr.value = ?) OR
|
(pr.operator = 'eq' AND pr.value = ?) OR
|
||||||
(pr.operator = 'gt' AND ? > pr.value::numeric) OR
|
(pr.operator = 'gt' AND ? > pr.value::numeric) OR
|
||||||
@@ -192,16 +192,13 @@ export class PricingRepository
|
|||||||
(pr.operator = 'lte' AND ? <= pr.value::numeric)
|
(pr.operator = 'lte' AND ? <= pr.value::numeric)
|
||||||
))
|
))
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
const normalizeValue = Array.isArray(value)
|
const normalizeValue = Array.isArray(value) ? value : [value]
|
||||||
? value
|
const placeholders = normalizeValue.map(() => "?").join(",")
|
||||||
: [value]
|
return `(pr.attribute = ? AND pr.value IN (${placeholders}))`
|
||||||
const placeholders = normalizeValue.map(() => "?").join(",")
|
}
|
||||||
return `(pr.attribute = ? AND pr.value IN (${placeholders}))`
|
})
|
||||||
}
|
.join(" OR ")})
|
||||||
})
|
|
||||||
.join(" OR ")}
|
|
||||||
)
|
|
||||||
) = (
|
) = (
|
||||||
/* Get total rule count */
|
/* Get total rule count */
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@@ -232,11 +229,16 @@ export class PricingRepository
|
|||||||
WHERE plr.price_list_id = pl.id
|
WHERE plr.price_list_id = pl.id
|
||||||
AND plr.deleted_at IS NULL
|
AND plr.deleted_at IS NULL
|
||||||
AND (
|
AND (
|
||||||
${flattenedContext
|
${flattenedContext
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
return `(plr.attribute = ? AND plr.value @> ?)`
|
if (Array.isArray(value)) {
|
||||||
})
|
return value
|
||||||
.join(" OR ")}
|
.map((v) => `(plr.attribute = ? AND plr.value @> ?)`)
|
||||||
|
.join(" OR ")
|
||||||
|
}
|
||||||
|
return `(plr.attribute = ? AND plr.value @> ?)`
|
||||||
|
})
|
||||||
|
.join(" OR ")}
|
||||||
)
|
)
|
||||||
) = (
|
) = (
|
||||||
/* Get total rule count */
|
/* Get total rule count */
|
||||||
@@ -248,7 +250,8 @@ export class PricingRepository
|
|||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
flattenedContext.flatMap(([key, value]) => {
|
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
|
query
|
||||||
.orderByRaw("price.price_list_id IS NOT NULL DESC")
|
.orderByRaw("price.price_list_id IS NOT NULL DESC")
|
||||||
.orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC")
|
.orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC")
|
||||||
.orderBy("pl.id", "asc")
|
|
||||||
.orderBy("price.amount", "asc")
|
.orderBy("price.amount", "asc")
|
||||||
|
|
||||||
return await query
|
return await query
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ export default class PricingModuleService
|
|||||||
/**
|
/**
|
||||||
* When deciding which price to use we follow the following logic:
|
* 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 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 the price list is of type SALE, we use the lowest price between the price list price and the default price
|
||||||
*/
|
*/
|
||||||
if (priceListPrice) {
|
if (priceListPrice) {
|
||||||
@@ -369,6 +370,7 @@ export default class PricingModuleService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pricesSetPricesMap.set(key, { calculatedPrice, originalPrice })
|
pricesSetPricesMap.set(key, { calculatedPrice, originalPrice })
|
||||||
priceIds.push(
|
priceIds.push(
|
||||||
...(deduplicate(
|
...(deduplicate(
|
||||||
|
|||||||
Reference in New Issue
Block a user