Skip to content

Commit

Permalink
fix(pricing): PriceLists of type Sale should not override lower prices
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperkristensen committed Jan 8, 2025
1 parent 46f987e commit 7345e6d
Show file tree
Hide file tree
Showing 4 changed files with 329 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-buttons-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/pricing": patch
---

fix(pricing): PriceLists of type Sale should not override lower prices
166 changes: 166 additions & 0 deletions integration-tests/http/__tests__/product/store/product.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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&region_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(
Expand Down Expand Up @@ -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&region_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", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,129 @@ moduleIntegrationTestRunner<IPricingModuleService>({
])
})

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 })

Expand Down
39 changes: 35 additions & 4 deletions packages/modules/pricing/src/services/pricing-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
InjectTransactionManager,
isPresent,
isString,
MathBN,
MedusaContext,
MedusaError,
ModulesSdkUtils,
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
}

Expand Down

0 comments on commit 7345e6d

Please sign in to comment.