Skip to content

Sales: an old order's missing list price reads as "the product had no price" #773

Description

@mforce

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions