`order_items.order_qty` is the SUPPLIER-facing quantity — what prints
on the PO. Five call sites read it as the customer quantity, which is a
money bug in both directions:
- supplier MOQ (customer 4, we buy 10) → invoiced 10, over-billing 6
- shorted line (customer 100, supplier covers 40) → invoiced 40,
under-billing 60 until the operator splits the remainder
The invoicing screen never fetched `quantity` at all (zero occurrences
in page.tsx), so it could not bill the customer quantity even in
principle. Added to the PB field projection, which is load-bearing:
drop it and PB returns undefined and every line silently falls back to
order_qty. A page-level test now fails if the projection drifts.
`maxSurplusFor` measured the surplus as `collected - order_qty`, which
on an MOQ line is always 0 — the overflow was mathematically unbookable
and vanished from inventory by construction, not by operator error.
Four call sites built delivery-note lines by hand; two passed
`quantity` and two forgot it, so the same DN printed the customer's 4
via the service layer and the supplier's 10 via the API routes. All
four now share `buildDeliveryNoteLine`, so they cannot diverge again.
`order_qty` had no doc comment at all next to an 11-line one on
`quantity` — that absence is why five subsystems guessed differently.
Now documented, with a new `src/lib/order-item-qty.ts` holding
`customerQty` / `supplierQty` as the single definition.
driver-stops.ts and collection-confirm/po-card.tsx read `order_qty`
CORRECTLY (they check what the supplier delivered against what we
ordered from them) and carry comments saying so, so the next cleanup
doesn't "fix" them into breaking.
23 new tests. Full suite: no regressions — the 15 failures are
identical before and after (verified by stashing and re-running).
Audit: docs/superpowers/specs/2026-08-30-quantity-handling-audit.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.6 KiB
TypeScript
63 lines
2.6 KiB
TypeScript
/**
|
|
* The two quantities on an `order_items` row, and which one to use where.
|
|
*
|
|
* `order_items` carries two numbers that are equal on almost every row and
|
|
* mean very different things on the rows where they aren't:
|
|
*
|
|
* `quantity` — what the CUSTOMER asked for. Drives the invoice, the
|
|
* delivery note, and the "ordered" column on the picking
|
|
* slip. This is the number the customer agreed to pay for.
|
|
*
|
|
* `order_qty` — what WE order from the supplier for this line. Drives the
|
|
* PO PDF, the PO email and the PO total. The process page
|
|
* labels this field "what we tell the supplier".
|
|
*
|
|
* They diverge in two real situations:
|
|
*
|
|
* - **Supplier MOQ.** Customer wants 4, the supplier won't sell fewer than
|
|
* 10. `quantity = 4`, `order_qty = 10`. We bill 4 and book 6 into stock.
|
|
* - **Shorting.** Customer wants 100, supplier A can only supply 40. The
|
|
* operator sets `order_qty = 40` and splits the remaining 60 onto a new
|
|
* line for supplier B (`splitOrderItemRemainder`).
|
|
*
|
|
* Getting this wrong is a money bug in both directions: reading `order_qty`
|
|
* as the customer quantity over-bills on MOQ lines and under-bills on shorted
|
|
* lines. Reading `quantity` as the supplier quantity puts the wrong number on
|
|
* a purchase order.
|
|
*
|
|
* Background and the full list of miscounts this module was written to fix:
|
|
* `docs/superpowers/specs/2026-08-30-quantity-handling-audit.md`.
|
|
*/
|
|
|
|
/** The subset of an order item these helpers need. */
|
|
export type QtyBearingItem = {
|
|
quantity?: number | null;
|
|
order_qty?: number | null;
|
|
};
|
|
|
|
function finite(value: number | null | undefined): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
/**
|
|
* What the customer buys and is billed for.
|
|
*
|
|
* Falls back to `order_qty` only for legacy rows that never had a
|
|
* `quantity` — historically the two were written identically at creation,
|
|
* so the fallback is safe and keeps old orders invoicing correctly.
|
|
*/
|
|
export function customerQty(item: QtyBearingItem): number {
|
|
return finite(item.quantity) ?? finite(item.order_qty) ?? 0;
|
|
}
|
|
|
|
/**
|
|
* What we order from the supplier — the number that prints on the PO.
|
|
*
|
|
* A literal `0` is meaningful here and is returned as-is: the operator
|
|
* setting 0 means "order nothing from this supplier on this line". Only an
|
|
* unset `order_qty` falls back to the customer quantity, which is the
|
|
* common case where the operator never edited the supplier-side number.
|
|
*/
|
|
export function supplierQty(item: QtyBearingItem): number {
|
|
return finite(item.order_qty) ?? finite(item.quantity) ?? 0;
|
|
}
|