What
A sales line with no list price renders the same way whether the farm never had a comparable price or the order simply predates the feature. The domain already knows which it is. The screen does not, because the distinction never reaches the API.
The gap, read from the code
ListPriceBasis (src/Cluckwork.Domain/Sales/SalesOrder.cs:411) models four cases:
Recorded — a comparable list price was captured.
ProductUnpriced — the product had no default price at all.
NotComparable — the product's currency code or minor unit did not match the order's.
PreDating — "The row predates the column. Backfill only — never written by the application."
SalesOrderItem.ListUnitPriceMinorUnits's own doc comment (SalesOrder.cs:438-452) states the consequence outright: "Not on the JSON read API — the ListPriceBasis property below carries the distinction — and the screen renders all three alike, but the Admin-only CSV export carries the basis by name."
Confirmed: ListPriceBasis appears nowhere in src/Cluckwork.Api/Endpoints/Sales/SaleEndpoints.cs or web/src/api/cluckwork.ts. SalesOrderItemResponse carries the bare ListUnitPriceMinorUnits only. So the SPA has a long? and three reasons collapsed into one null.
The rendered result is sales:noListPrice ("No list price") on the line, sales:discountUnknown ("Unknown") in the Orders-list column, and sales:discountUnknownOrder ("No list price on any line — this order's discount cannot be worked out.") on the panel — for all of them.
Why the wording is actively misleading for one case
GLOSSARY.md:487 calls a null "no comparable list price, a real answer distinct from missing data". That is true of ProductUnpriced and NotComparable. It is false of PreDating, which IS missing data. The help text at web/src/i18n/en.ts:3496 doubles down: "Shown as 'No list price' when the product had none to compare" — which describes only the first case while the label covers all three.
So a reader of an order confirmed before 2026-09-09 is told the product had no price to compare against. The product may well have had one; the line just never captured it.
Worked example from a real farm
An order sold three tray products. The panel reports "No list price" on every line, and the catalogue today prices all three:
| Line |
Sold at |
Catalogue today |
| Large Tray |
₱205.00 |
₱215.00 |
| Extra Large Tray |
₱215.00 |
₱220.00 |
| Jumbo Tray |
₱235.00 |
₱240.00 |
This is the PreDating case. The order is readable as "we have no idea", when the accurate statement is "this order predates the list-price snapshot".
Not a request to backfill
The snapshot must stay unbackfilled. ListUnitPriceMinorUnits is the catalogue price as it stood when the line was added (GLOSSARY.md:481), and Update deliberately does not re-resolve it (INV-1). Filling old lines from today's catalogue would invent discounts that never happened — in the table above it would report ₱5–₱10 off on every line, which reads far more like a price rise since September than a negotiated discount, and that fabricated number would flow into the audit trail and the money reports.
The ask is to say which kind of nothing it is, not to manufacture a number.
Decide before writing code
1 — does the basis go on the wire, or does the SPA infer it? It cannot be inferred: ProductUnpriced and PreDating are indistinguishable from a bare null. So SalesOrderItemResponse gains the basis, as the enum MEMBER NAME rendered through web/src/i18n/enums.ts like DiscountReasonCode (#721), never raw. Note the domain comment says it is deliberately not on the read API today — that decision is what this issue reopens, so it needs an explicit rationale rather than a silent reversal.
2 — how many labels does the screen actually need? ProductUnpriced and NotComparable are both honestly "no comparable list price". Only PreDating is missing data. Two labels may be the whole fix; three is also defensible. Fewer strings is better, but the one that matters must be distinct.
3 — is it Admin-only? The basis is on the Admin-only CSV export today. The line table is SalesFlow. Putting it on screen widens who sees it; that is probably right, since it is a fact about the record rather than a money figure, but it is a decision and not an accident.
Repo rules this touches
Found while reading an old order on a live farm during #769 review. Pre-existing behaviour from #720; not a regression.
What
A sales line with no list price renders the same way whether the farm never had a comparable price or the order simply predates the feature. The domain already knows which it is. The screen does not, because the distinction never reaches the API.
The gap, read from the code
ListPriceBasis(src/Cluckwork.Domain/Sales/SalesOrder.cs:411) models four cases:Recorded— a comparable list price was captured.ProductUnpriced— the product had no default price at all.NotComparable— the product's currency code or minor unit did not match the order's.PreDating— "The row predates the column. Backfill only — never written by the application."SalesOrderItem.ListUnitPriceMinorUnits's own doc comment (SalesOrder.cs:438-452) states the consequence outright: "Not on the JSON read API — the ListPriceBasis property below carries the distinction — and the screen renders all three alike, but the Admin-only CSV export carries the basis by name."Confirmed:
ListPriceBasisappears nowhere insrc/Cluckwork.Api/Endpoints/Sales/SaleEndpoints.csorweb/src/api/cluckwork.ts.SalesOrderItemResponsecarries the bareListUnitPriceMinorUnitsonly. So the SPA has along?and three reasons collapsed into one null.The rendered result is
sales:noListPrice("No list price") on the line,sales:discountUnknown("Unknown") in the Orders-list column, andsales:discountUnknownOrder("No list price on any line — this order's discount cannot be worked out.") on the panel — for all of them.Why the wording is actively misleading for one case
GLOSSARY.md:487calls a null "no comparable list price, a real answer distinct from missing data". That is true ofProductUnpricedandNotComparable. It is false ofPreDating, which IS missing data. The help text atweb/src/i18n/en.ts:3496doubles down: "Shown as 'No list price' when the product had none to compare" — which describes only the first case while the label covers all three.So a reader of an order confirmed before 2026-09-09 is told the product had no price to compare against. The product may well have had one; the line just never captured it.
Worked example from a real farm
An order sold three tray products. The panel reports "No list price" on every line, and the catalogue today prices all three:
This is the
PreDatingcase. The order is readable as "we have no idea", when the accurate statement is "this order predates the list-price snapshot".Not a request to backfill
The snapshot must stay unbackfilled.
ListUnitPriceMinorUnitsis the catalogue price as it stood when the line was added (GLOSSARY.md:481), andUpdatedeliberately does not re-resolve it (INV-1). Filling old lines from today's catalogue would invent discounts that never happened — in the table above it would report ₱5–₱10 off on every line, which reads far more like a price rise since September than a negotiated discount, and that fabricated number would flow into the audit trail and the money reports.The ask is to say which kind of nothing it is, not to manufacture a number.
Decide before writing code
1 — does the basis go on the wire, or does the SPA infer it? It cannot be inferred:
ProductUnpricedandPreDatingare indistinguishable from a bare null. SoSalesOrderItemResponsegains the basis, as the enum MEMBER NAME rendered throughweb/src/i18n/enums.tslikeDiscountReasonCode(#721), never raw. Note the domain comment says it is deliberately not on the read API today — that decision is what this issue reopens, so it needs an explicit rationale rather than a silent reversal.2 — how many labels does the screen actually need?
ProductUnpricedandNotComparableare both honestly "no comparable list price". OnlyPreDatingis missing data. Two labels may be the whole fix; three is also defensible. Fewer strings is better, but the one that matters must be distinct.3 — is it Admin-only? The basis is on the Admin-only CSV export today. The line table is
SalesFlow. Putting it on screen widens who sees it; that is probably right, since it is a fact about the record rather than a money figure, but it is a decision and not an accident.Repo rules this touches
catalogParityfails the build. Per i18n: catalogParity compares key SETS, so a translation that drifts in MEANING passes every gate #688, help prose naming a control must use that control's label word in each locale.GLOSSARY.md:487's "a real answer distinct from missing data" anden.ts:3496's "when the product had none to compare" are both wrong forPreDatingand change with this.tools/simulation/ui/(in CI) andtools/simulation/k6/(not).Found while reading an old order on a live farm during #769 review. Pre-existing behaviour from #720; not a regression.