medusajs/medusa

[Bug]: calculated_price.original_amount returns the base price instead of the customer's override when a Sale is stacked on an Override

Open

Aperta il 31 mag 2026

Vedi su GitHub
 (1 commento) (0 reazioni) (0 assegnatari)TypeScript (22.539 star) (2090 fork)batch import
good first issuetype: bug

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

  1. Variant with base price 49.95 (no rules).
  2. Price list type override, rule customer.groups.id = G, price 34.96.
  3. Price list type sale, rule customer.groups.id = G, price 29.95.
  4. 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:

  1. originalPrice is always defaultPrice (base); the SALE branch never reconsiders it.
  2. prices.find((p) => p.price_list_id) keeps only one list price. The repository orders by amount ASC (repositories/pricing.ts ~293–295), so when the override and sale share the same rule the cheaper sale wins the find() 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_pricetotal 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/medusa 2.15.3, reproducible via the module service and the Store API calculated_price.

Guida contributor