You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Sales: the Orders list cannot show which orders are unpaid #769
The Orders list cannot answer "which orders has nobody paid yet". Add an Outstanding column and
an unpaid filter, so a farm can find the orders owing money instead of opening them one at a time.
The gap, read from the code
The Orders list columns are Reference, Date, Customer, Status, Discount, Total, History
(web/src/routes/SalesPage.tsx:1693). SalesOrderResponse carries TotalMinorUnits and no paid or
outstanding figure (SaleEndpoints.cs:349).
Status cannot stand in for it.SalesOrderStatus is Draft, Confirmed, Shipped, Invoiced, Cancelled, Voided. There is no Paid and nothing in that enum moves when money arrives, so a fully
settled order and one that has paid nothing both read Confirmed.
The data exists, just nowhere useful for this question:
GET /sales/{orderId}/payments returns PaidMinorUnits and OutstandingMinorUnits — per order,
so answering it for a list of 100 means opening 100 orders.
GET /customers/balances returns per-customer confirmed total, paid and outstanding, and CustomersPage.tsx already renders it.
So "which CUSTOMER owes me money" is answerable today. "Which ORDERS are unpaid" is not.
Not a regression, and not an unmet criterion
#89 scoped exactly two SPA surfaces — "Sales order panel: payments section … outstanding line" and "Customers page: outstanding balance column" — and shipped both. The Orders list was never in it. specs/product/GLOSSARY.md:734 documents the same boundary in the present tense: outstanding is "Shown on the order's payments panel and the Customers page (admins)." The documentation is
accurate; the capability is simply absent. This is unfilled work, not drift.
Nothing in specs/product/specs.md requests it, and Phase 1.5's epic #15 carries no payment items.
Decide before writing code
1 — who sees it. This is the blocking question. #89 made money admin-only end to end and /customers/balances refuses Workers outright, but the Orders list is AuthPolicies.SalesFlow
(Owner, Manager, Sales and Worker). So this is not additive: it puts money on a screen that
currently shows none, for roles that currently see none.
Three coherent answers, and picking none of them ships an access decision by accident:
Sales tier sees it, Worker does not. Matches the glossary's own split — "Recording and viewing
payments is the Sales tier (Owner/Manager/Sales); voiding a payment is corrective". Probably the
right answer, and it makes the column useful to the people chasing the money.
2 — column, filter, or both. A per-row badge alone does not solve the stated need: it marks
what is already on screen but does not let you find unpaid orders in a long list. The filter is the
half that answers the question. The list is paged (DefaultPageSize = 100, MaxPageSize = 500, SaleEndpoints.cs:21-22), so an unpaid filter must be a server-side predicate, never a client
filter over the current page — otherwise it silently means "unpaid among the 100 you happen to be
looking at".
3 — what "unpaid" means. Partial payments are explicitly the normal case per #89. So the filter
needs a stated definition: nothing paid, or anything still outstanding. "Outstanding > 0" is the
useful one for chasing money, but it is a different set from "paid nothing", and the label must say
which. Draft orders have no outstanding concept at all — payments attach to Confirmed orders only.
The constraint that shapes the implementation
GLOSSARY.md:734 is emphatic that these are "server-side sums, never client-aggregated pages".
So the per-order outstanding must be computed in the same query as the page, as a join or lateral
aggregate over non-voided payments — not N follow-up queries, and not summed in the browser. Voided
payments must not count, and the sum must exclude them the same way ListCustomerBalancesAsync already does; reuse that shape rather than re-deriving it.
Acceptance
The Orders list shows each confirmed order's outstanding amount, for the roles decided in (1).
A filter returns only orders matching the agreed definition of unpaid, evaluated server-side across
the whole result set rather than the current page.
A fully paid order is visually distinct from one that has paid nothing, and from a partial payment.
Voided payments do not reduce the outstanding figure.
Draft, Cancelled and Voided orders render no outstanding figure rather than a misleading zero.
i18n — column header, filter label and any badge in en/es/tl, or catalogParity fails the
build. A badge key must start with a capital in all three locales (badgeCase.test.ts).
Glossary + Help — GLOSSARY.md:734's "Shown on the order's payments panel and the Customers
page" becomes wrong the moment this ships, and the SPA Help page needs the same update.
What
The Orders list cannot answer "which orders has nobody paid yet". Add an Outstanding column and
an unpaid filter, so a farm can find the orders owing money instead of opening them one at a time.
The gap, read from the code
The Orders list columns are Reference, Date, Customer, Status, Discount, Total, History
(
web/src/routes/SalesPage.tsx:1693).SalesOrderResponsecarriesTotalMinorUnitsand no paid oroutstanding figure (
SaleEndpoints.cs:349).Statuscannot stand in for it.SalesOrderStatusisDraft, Confirmed, Shipped, Invoiced, Cancelled, Voided. There is noPaidand nothing in that enum moves when money arrives, so a fullysettled order and one that has paid nothing both read
Confirmed.The data exists, just nowhere useful for this question:
GET /sales/{orderId}/paymentsreturnsPaidMinorUnitsandOutstandingMinorUnits— per order,so answering it for a list of 100 means opening 100 orders.
GET /customers/balancesreturns per-customer confirmed total, paid and outstanding, andCustomersPage.tsxalready renders it.So "which CUSTOMER owes me money" is answerable today. "Which ORDERS are unpaid" is not.
Not a regression, and not an unmet criterion
#89 scoped exactly two SPA surfaces — "Sales order panel: payments section … outstanding line" and
"Customers page: outstanding balance column" — and shipped both. The Orders list was never in it.
specs/product/GLOSSARY.md:734documents the same boundary in the present tense: outstanding is"Shown on the order's payments panel and the Customers page (admins)." The documentation is
accurate; the capability is simply absent. This is unfilled work, not drift.
Nothing in
specs/product/specs.mdrequests it, and Phase 1.5's epic #15 carries no payment items.Decide before writing code
1 — who sees it. This is the blocking question. #89 made money admin-only end to end and
/customers/balancesrefuses Workers outright, but the Orders list isAuthPolicies.SalesFlow(Owner, Manager, Sales and Worker). So this is not additive: it puts money on a screen that
currently shows none, for roles that currently see none.
Three coherent answers, and picking none of them ships an access decision by accident:
F21: Customer payments + balances — order-attached payments, outstanding per order/customer (admin-only) #89's tier exactly. A Sales user still cannot see what they are owed.
payments is the Sales tier (Owner/Manager/Sales); voiding a payment is corrective". Probably the
right answer, and it makes the column useful to the people chasing the money.
ShowFarmWideSaleAllocationNotice/yourMaxDiscountPercentpattern from Configurable Worker sales allocation scope by flock assignment #612 and Sales: per-farm discount ceiling, with Owner/Manager approval above it #727. Moresurface; only worth it if a Worker genuinely needs to know an order is unsettled.
2 — column, filter, or both. A per-row badge alone does not solve the stated need: it marks
what is already on screen but does not let you find unpaid orders in a long list. The filter is the
half that answers the question. The list is paged (
DefaultPageSize = 100,MaxPageSize = 500,SaleEndpoints.cs:21-22), so an unpaid filter must be a server-side predicate, never a clientfilter over the current page — otherwise it silently means "unpaid among the 100 you happen to be
looking at".
3 — what "unpaid" means. Partial payments are explicitly the normal case per #89. So the filter
needs a stated definition: nothing paid, or anything still outstanding. "Outstanding > 0" is the
useful one for chasing money, but it is a different set from "paid nothing", and the label must say
which. Draft orders have no outstanding concept at all — payments attach to Confirmed orders only.
The constraint that shapes the implementation
GLOSSARY.md:734is emphatic that these are "server-side sums, never client-aggregated pages".So the per-order outstanding must be computed in the same query as the page, as a join or lateral
aggregate over non-voided payments — not N follow-up queries, and not summed in the browser. Voided
payments must not count, and the sum must exclude them the same way
ListCustomerBalancesAsyncalready does; reuse that shape rather than re-deriving it.Acceptance
the whole result set rather than the current page.
renders, per F21: Customer payments + balances — order-attached payments, outstanding per order/customer (admin-only) #89's own split.
Repo rules this touches
check the Playwright specs under
tools/simulation/ui/, which DO run in CI on anysrc/**orweb/**change (AGENTS.md #394 says the Playwright specs are not in CI; they have been since 2026-08-08 #767), andtools/simulation/k6/, which does not.catalogParityfails thebuild. A badge key must start with a capital in all three locales (
badgeCase.test.ts).GLOSSARY.md:734's "Shown on the order's payments panel and the Customerspage" becomes wrong the moment this ships, and the SPA Help page needs the same update.
at the head under review.
Found while reviewing the Sales screen after #727.