[Bug]: calculated_price.original_amount returns the base price instead of the customer's override when a Sale is stacked on an Override
Aperta il 31 mag 2026
Descrizione
What
When a customer group has both an override price list (their normal price) and a sale price list, calculatePrices returns the correct calculated_amount (the sale) but an original_amount equal to the base price, not the customer's override.
So a wholesale customer whose normal price is 34.96 is shown ~~49.95~~ 29.95 (a fake "−20.00 €" off a retail price they never pay). The honest reference is ~~34.96~~ 29.95. The storefront renders original_amount directly, and prepareLineItemData persists it onto the line item's compare_at_unit_price, so the wrong reference ends up in the stored order too, not just on the page. For EU stores this is a legal issue (PAngV / UWG misleading-price-reference).
Steps to reproduce
- Variant with base price
49.95(no rules). - Price list type
override, rulecustomer.groups.id = G, price34.96. - Price list type
sale, rulecustomer.groups.id = G, price29.95. calculatePrices({ id: [priceSetId] }, { context: { currency_code: "eur", "customer.groups.id": G } })
Actual: calculated_amount: 29.95, original_amount: 49.95 (base), is_original_price_price_list: false.
Expected: original_amount: 34.96 (the override).
Root cause
packages/modules/pricing/src/services/pricing-module.ts, calculatePrices() (~lines 406–445):
const priceListPrice = prices.find((p) => p.price_list_id) // single list price only
const defaultPrice = prices.find((p) => !p.price_list_id)
let originalPrice = defaultPrice // hardwired to base
switch (priceListPrice.price_list_type) {
case OVERRIDE: calculatedPrice = originalPrice = priceListPrice; break
case SALE: calculatedPrice = min(priceListPrice, defaultPrice)
// originalPrice is never reassigned, stays = base
}
Two problems compound:
originalPriceis alwaysdefaultPrice(base); the SALE branch never reconsiders it.prices.find((p) => p.price_list_id)keeps only one list price. The repository orders byamount ASC(repositories/pricing.ts~293–295), so when the override and sale share the same rule the cheaper sale wins thefind()and the override is dropped entirely.
Net: once a cheaper sale exists, the override can never be the original. (Related: #10877 added the min() to the SALE branch but did not consider a non-sale price that is itself an override.)
Suggested fix
There is exactly one real (non-sale) price per pricing context, which is what original_amount must be. Resolve it by running the existing selection over the candidate set with sale prices excluded:
const nonSale = prices.filter((p) => p.price_list_type !== "sale")
let originalPrice =
nonSale.find((p) => p.price_list_id) // the override, taken unconditionally (as OVERRIDE already is)
?? nonSale.find((p) => !p.price_list_id) // else base
i.e. symmetry: calculated resolves over all prices, original over all-except-sales. Localized to this block; no schema/migration/API change.
Why it's safe
original_amount does not affect the payable amount (unit_price → total is computed from the calculated price; no total reads original_amount). The fix is a no-op unless a sale is stacked on a group override, the exact case that is currently wrong. It only corrects the displayed/persisted reference price.
A possible objection: "a sitewide sale should anchor to retail for everyone." That is expressible through data, deactivate the override for the sale window, then base is the only active non-sale price and original is base. So there is no case where an active override + active sale should anchor to base.
Environment
@medusajs/pricing/@medusajs/medusa2.15.3, reproducible via the module service and the Store APIcalculated_price.