Repository navigation
[Bug]: Price-list updates carrying explicit price IDs are silently dropped when quantity fields are omitted #17289
Description
Package.json file
Relevant dependencies (sanitized excerpt; reported module versions are also 2.21.2):
{
"dependencies": {
"@medusajs/framework": "2.21.2",
"@medusajs/medusa": "2.21.2"
}
}@medusajs/pricing and @medusajs/core-flows: 2.21.2.
Node.js version
v22.22.1
Database and its version
PostgreSQL 17.11
Operating system name and version
macOS 26.6.2
Browser name
N/A — server-side script.
What happended?
batchPriceListPricesWorkflow / updatePriceListPrices silently drops one of two updates even though each update carries a distinct, explicit price id.
Given two prices in the same price list and price set, both in EUR but with different quantity bands, updating them together with { id, variant_id, amount } changes only the second price. The first keeps its old amount, and the workflow reports success. The Admin update validator accepts this shape: currency and quantity fields are optional.
This differs from closed #11103, which concerned region rules and was resolved for that caller by supplying the distinguishing rules. This report isolates explicit-ID updates to quantity-band prices: callers already identify both target rows and should not have to resend unchanged identity fields. It still reproduces on 2.21.2, after the merged pricing-normalization changes in PR #13899.
Source analysis
All links below target the v2.21.2 TypeScript source:
hashPriceincludes only the currency, price-set, price-list, quantity and rule fields present on the input; it does not includeid.normalizePriceskeyspricesToUpsertby that hash, not by explicit ID. The two partial updates hash identically, so the second replaces the first in the map. A hash match with an existing price can also replace the supplied ID.updatePriceListPrices_normalizes the inputs before passing the collapsed array toupsertWithReplace.- The workflow step forwards each explicit ID and adds its variant's price-set ID; it does not fill omitted quantity fields before calling the module.
The entire pricing-module.ts file is byte-for-byte identical on develop at 0b97a96ea0d32ff3f4b18dbeeeeba9e763600a15 and v2.21.2. This is a source comparison, not a new runtime test of develop.
Workaround: resend currency_code, min_quantity, max_quantity and any applicable rules with each update so the price hashes remain distinct.
The separate rollback-compensation defect is already tracked by open PR #17062 and is outside this report.
Expected behavior
Both explicitly identified prices are updated: quantity band 1–9 becomes 78, and quantity band 10+ becomes 74. Omitting unchanged optional identity fields must not cause one distinct ID to overwrite another update before persistence.
Actual behavior
On 2.21.2, the script printed [[1, 80], [10, 74]] across 3 runs on a fresh database: the workflow succeeded, but band 1–9 remained 80 and only band 10+ changed to 74. For the untouched band 1–9 price, updated_at remained equal to created_at. Setting the price list to type "override" produced the same result. In the original reproduction, supplying currency and quantity boundaries with both updates correctly changed both prices.
Unrelated search-index subscriber errors were also logged under medusa exec; they are noted separately from the successful price-update workflow and its observed persisted result.
Link to reproduction repo
No standalone public reproduction repository. The complete script below runs in a standard Medusa 2.21.2 application with PostgreSQL, configured and migrated normally.
Save as src/scripts/repro-price-list-id-updates.ts, then run:
npx medusa exec ./src/scripts/repro-price-list-id-updates.tsUse a disposable application/database. The script creates only synthetic data and leaves the product and price list for inspection. The observed output below was reproduced across 3 runs on a fresh database, including the same result with price-list type "override".
import type { ExecArgs, IPricingModuleService } from "@medusajs/framework/types"
import { Modules, PriceListStatus } from "@medusajs/framework/utils"
import {
batchPriceListPricesWorkflow,
createPriceListsWorkflow,
createProductsWorkflow,
} from "@medusajs/medusa/core-flows"
import { randomUUID } from "node:crypto"
export default async function ({ container }: ExecArgs) {
const pricing = container.resolve<IPricingModuleService>(Modules.PRICING)
const suffix = randomUUID()
const { result: [product] } = await createProductsWorkflow(container).run({
input: {
products: [{
title: `Price update repro ${suffix}`,
options: [{ title: "Default", values: ["Default"] }],
variants: [{
title: "Default",
sku: `price-update-repro-${suffix}`,
manage_inventory: false,
options: { Default: "Default" },
prices: [{ currency_code: "eur", amount: 100 }],
}],
}],
},
})
const variantId = product.variants?.[0]?.id
if (!variantId) throw new Error("Repro variant was not created")
const { result: [list] } = await createPriceListsWorkflow(container).run({
input: {
price_lists_data: [{
title: `Price update repro ${suffix}`,
description: "Synthetic explicit-ID update reproduction",
status: PriceListStatus.ACTIVE,
prices: [
{ variant_id: variantId, currency_code: "eur", amount: 80,
min_quantity: 1, max_quantity: 9 },
{ variant_id: variantId, currency_code: "eur", amount: 76,
min_quantity: 10 },
],
}],
},
})
const readPrices = async () =>
(await pricing.listPrices({ price_list_id: [list.id] }))
.sort((a, b) => Number(a.min_quantity) - Number(b.min_quantity))
const [band1, band2] = await readPrices()
if (!band1 || !band2) throw new Error("Expected two quantity-band prices")
await batchPriceListPricesWorkflow(container).run({
input: {
data: {
id: list.id,
create: [],
delete: [],
update: [
{ id: band1.id, variant_id: variantId, amount: 78 },
{ id: band2.id, variant_id: variantId, amount: 74 },
],
},
},
})
console.log((await readPrices()).map((p) => [Number(p.min_quantity), Number(p.amount)]))
// Expected: [[1, 78], [10, 74]]
// Observed on 2.21.2: [[1, 80], [10, 74]] — no workflow error.
}Suggested fix: when an input carries an explicit id, preserve and key the update by that ID instead of deduplicating it by optional identity fields. Reject conflicting updates to the same target rather than silently retaining only one.
Activity
medusa-os-bot commentedon Oct 15, 2026
Thanks for reporting this! We've labeled it as a bug and will investigate.
Confirmed bug in the pricing module's normalization logic. When batchPriceListPricesWorkflow / updatePriceListPrices receives multiple price updates that each carry an explicit id but omit optional identity fields (currency_code, min_quantity, max_quantity), the internal hashPrice function produces identical hashes for all such entries (since id is not part of the hash). The normalizePrices function then stores entries in a map keyed by this hash, so the second entry silently overwrites the first. Only the last update survives; the rest are dropped without error. Root cause: hashPrice at pricing-module.ts around line 1838 and pricesToUpsert.set(entryHash, entry) at around line 990. Workaround: include currency_code, min_quantity, and max_quantity in each update so the hashes remain distinct. Fix direction: when an input carries an explicit id, key the update by id instead of the computed hash. The fix is contained but touches a sensitive core pricing…
medusa-os-bot commentedon Oct 22, 2026
Thanks for the report. This looks like a small, well-scoped fix, so we're attempting one automatically.
In
normalizePrices(packages/modules/pricing/src/services/pricing-module.ts:990), price updates are stored in a map keyed by the output ofhashPrice, which does not include the priceidfield (hashPrice at line 1838 covers only currency, price-set, price-list, min/max quantity, and rules). When two updates share the same values for those fields — or those fields are omitted — both produce the same hash and the second entry silently overwrites the first. Only one of the two updates reaches the database. Fix: when an input entry carries an explicitid, use thatidas the map key inpricesToUpsertinstead of the computed hash, so two distinct identified prices cannot collide.A pull request that references this issue will follow. A maintainer reviews every change before it's merged. If we can't produce a safe fix, the issue is handed to the team instead.
medusa-os-bot commentedon Oct 22, 2026
We tried to put together an automated fix for this issue, but couldn't produce one that is small and safe enough to open as a pull request.
The automated fix run did not complete.
The issue has been escalated to the team, and a maintainer will follow up here. There's nothing you need to do in the meantime.
fix(pricing): don't drop price list updates that carry explicit ids (m…