Skip to content

Sales: the Orders list cannot show which orders are unpaid #769

Closed

Description

@mforce

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). 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:

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.
  • A role that may not see money sees no amount — and the API refuses it regardless of what the SPA
    renders, per F21: Customer payments + balances — order-attached payments, outstanding per order/customer (admin-only) #89's own split.

Repo rules this touches

Found while reviewing the Sales screen after #727.

Activity

  1. mforce commented on Sep 11, 2026

    @mforce
    OwnerAuthor

    Decisions, 2026-09-11

    Two of the three open questions are answered by the owner.

    "Unpaid" means outstanding > 0, not "paid nothing". A partially-paid order is unpaid for this
    purpose — the point is finding money still owed, and a 90%-settled order still owes something.
    Partial payments are the normal case per #89, so the "paid nothing" reading would have hidden most of
    what the filter exists to surface.

    Workers do not see it. The column and the filter are the Sales tier — Owner, Manager and
    Sales — matching the glossary's own split ("Recording and viewing payments is the Sales tier")
    rather than #89's stricter admin-only balances endpoint. Workers see the Orders list without any
    outstanding column, and the API must refuse them the figure regardless of what the SPA renders.

    Note this makes the list response role-dependent, which it is not today. The established pattern
    is a per-caller derived field computed server-side (yourMaxDiscountPercent on #727,
    ShowFarmWideSaleAllocationNotice on #612) rather than widening the endpoint and filtering in the
    browser. A Worker's response should not carry the number at all.

    One thing I am deciding rather than asking

    The unpaid filter is its own control, not a value in the Status dropdown. Adding "Unpaid" beside
    Draft / Confirmed / Cancelled / Voided would conflate payment state with order state — the exact
    conflation that makes the current screen unable to answer this question, since Confirmed already
    means both "settled" and "owes everything". They are orthogonal: you want to ask for confirmed AND
    unpaid. Two controls, combinable.

    Consequences for the acceptance criteria above

    • The filter returns confirmed orders whose non-voided payments sum to less than the order total.
    • Draft, Cancelled and Voided orders are never returned by it — payments attach to Confirmed orders
      only, so "unpaid" is undefined for them, and a zero there would read as settled.
    • A fully paid order is excluded; a partially paid one is included and shows what remains.
  2. added
    area:apiAPI/endpoint layer
    size:MA day or two; migration or a multi-state UI
    on Sep 11, 2026
  3. added 20 commits that reference this issue on Sep 12, 2026
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

    area:apiAPI/endpoint layerarea:frontendReact/Vite web clientenhancementNew feature or requestepic-1.5size:MA day or two; migration or a multi-state UI

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions