Two items from the Next Phase page, both created 2026-07-24. Point of Sale Navigation fixes the section's contents and order to Orders Management · Registers · Sessions · Discounts & Promotions · Refunds. Accessing Displays via Registers Page asks for Kitchen Display, Preparation Display and QR Menu to be reachable from the Registers page. No migration.
Three of the five nav rows already existed. The other two are real work: Discounts & Promotions moved out of Configurations, and Refunds is a new page that had no equivalent anywhere in the product.
The order is Narong's, not alphabetical, and the test asserts it as a sequence — a set-membership check would have passed on the old nav the moment the two new rows existed.

It now lives at /cafe/pos/discounts. /cafe/settings/discounts redirects, so existing links
and bookmarks still land — the same treatment /pos/registers got when the registers admin moved the
other way. Its old group, Discount & Loyalty, is gone entirely: this was its last row, since Loyalty
Mobile App Settings went top-level on 07-22.


The obvious implementation is "orders where state = refunded". That would be wrong twice over: it cannot show a
partial refund as its own event, and an order refunded more than once collapses into a single row.
The page reads cafe.order_refunds — the per-event table that already existed for the refund action —
so each refund is its own line with its own number, amount, reason and cashier.
Order Management shows one business date at a time, so a refund's order number links to
/cafe/orders?date=… for the day that order was rung up. Rows too old to carry a business date render as
plain text rather than a link that would silently land on today.


Those two screenshots are a genuine before/after. staging had zero refund events, so the ledger could only ever have
shown its empty state; the test refunds demo's one paid order through POST /cafe/api/cafe/orders/:id/refund
— the same endpoint the Order Management modal posts to — and then asserts the ledger lists it. A hand-inserted row
would only have proved the SELECT.
Until now the Kitchen Display was only reachable from inside an open register terminal, so setting up a kitchen screen meant opening a till first. The shop's displays now sit on the Registers page itself. The URL carries the shop's display secret, minted lazily on first render and stable afterwards, so a bookmarked kitchen tablet stays authenticated.


Preparation Display and QR Menu render disabled, not built. Both are marked NOT YET IMPLEMENTED on the Next Phase page — neither exists anywhere in the product. They're shown greyed so the shape of the feature is visible, the same convention the tier locks use. Building either is its own slice.

Refunds is gated on reports.view (manager-level), the same permission Sessions uses — not
orders.view. A POS User can already see Order Management, but that screen is scoped to a single
business date, whereas the refund ledger spans thirty days by default and offers "All". Gating on
orders.view would have quietly handed a POS User a wider financial window than any screen they have
today. There's a negative control for it: a user demoted to pos_user sees neither new row and is
redirected off both routes.
| Suite | Result |
|---|---|
| Local — nav, both pages, the redirect, displays across both tiers, permission boundary | 16/16 |
| Staging — real SSO on the deployed Worker, Pro and Starter | 12/12 |
| Staging smoke | 5/5 |
tsc --noEmit (nix-cafe) | clean |
Every claim carries a negative control: the nav is asserted as an ordered sequence, Discounts is checked gone from its old rail, Kitchen Display is checked both live on Pro and locked-with-no-href on Starter, and the date filter is proven to exclude in one direction (local fixtures older than the window are hidden) and include in the other (a same-day refund appears under the default). Expected counts and totals are read from the database at run time, not hardcoded, and the suite fails loudly if the fixture stops discriminating.
Worth recording, because all three are shapes that produce false confidence rather than false alarms:
new Date(business_date).toISOString() moved the expected day one west of the stored value. The app was right; the assertion was wrong. Now compares against ::text from the database.auth() memoises the resolved permission set per token, so the cookie minted before the role change kept serving the old permissions — and the route looked unguarded when it wasn't. Minting the cookie after the demotion made it deterministic.A fourth failure was the dev server itself degrading after repeated edits; a restart cleared it, and the suite then ran clean repeatedly.
nix-cafe staging b30214d (Worker 8528ba5b) · localization
main 2beb2a3 (37 new strings; km falls back to English until Narong fills it).
No migration. Two new routes. Parked for the scheduled prod promote.