← All tests

R9 — Orders view + per-line refund PROD

2026-05-09 on prod. Closes NIX-OS-81 (Open Orders) + NIX-OS-83 (Order Details in POS Register Session). Shipped together because they both target the same Orders surface in the lockable POS shell. Five sub-phases — schema/DAO for partial refund, refund API + Odoo sync queue, draft order-number reservation at park time, the full Odoo-POS-style Orders view rewrite (top tab strip + search + Active/Paid filter + paginator + per-line refund detail with on-screen qty numpad), and the “+” Add new order handoff.

16/16 R9 prod test green. End-to-end flow on get-coffee config 7: cashier creation, PIN unlock, open shift, ring an order, park a draft (gets reserved POS07-NNNN), switch to Orders view (new tab strip + filter render), Active filter shows the parked draft, switch to Paid filter, click the rung order, click a line, click Refund — then DB verify cafe.order_refunds row created with -R1 suffix, order state flipped to partial_refund, line.refunded_qty bumped, order_refund_lines populated.

Commits

de417d1 (backend) — feat(backend NIX-OS-81 NIX-OS-83): R9.1+R9.3 schema — partial refund tables + draft order_number
  migrations/20260509100000_cafe_partial_refunds.ts
  migrations/20260509110000_cafe_draft_orders_order_number.ts
  migrate.js (registry entries)

bdc7909 (cafe) — feat(cafe NIX-OS-81 NIX-OS-83): R9 orders view rewrite + per-line refund + draft order numbers
  17 files changed, 2110 insertions(+), 225 deletions(-)
  + lib/db/partial_refunds.ts, lib/db/order_refunds_push.ts
  + new orders-view.tsx (Odoo-POS-style layout)
  + lockable-shell.tsx wires draft handlers via window CustomEvent

3c80cef (cafe) — fix(cafe NIX-OS-81): refresh server props on Orders tab so freshly-parked drafts surface in Active filter
  Hooked router.refresh() in OpenPhase when view='orders' so the OrdersView's
  drafts/sessionOrders props are re-fetched server-side; without this, a draft
  parked on the Register tab wouldn't appear in Active until next manual reload.

c93c4bf (cafe) — fix(cafe NIX-OS-81): bypass Hyperdrive cache on listOrdersForSession so freshly-rung orders surface in Orders view immediately
  Wrap in db.transaction() — same pattern as listOrdersForTenant + listDeadLetterOrders.
  Without this, a freshly-rung order took ~60s (Hyperdrive SELECT cache TTL) to
  appear in the Paid filter.

R9 prod test (16/16)

#StepStatus
1Schema verify (3 tables/columns)pass
2Pre-flight: config 7 no open session + seq floorpass
3SSO login as get-coffee owner (narongix)pass
4Create test cashier → capture PINpass
5Open /cafe/pos/register/7 in popuppass
6PIN unlock + open shift with $0pass
7Ring an order: 2 products → pay cashpass
8Park a fresh cart → draft gets reserved order_numberpass
9Switch to Orders view → new tab strip visiblepass
10Active filter shows parked draft with reserved order_numberpass
11Paid filter → click the rung order → detail panelpass
12Select a line + click Refundpass
13DB: cafe.order_refunds row with -R1 suffixpass
14DB: order state flipped to refunded or partial_refundpass
15DB: order_lines.refunded_qty > 0pass
16DB: cafe.order_refund_lines populatedpass

Regression sweep

SuiteResultNotes
test-r9-prod.mjs (this ship)16/16Full flow visual + DB on get-coffee config 7
test-phase1-prod.mjs11/11SSO routing + subscription page
test-phase2-sso-outdoor-prod.mjs6/6SSO bridge across products
test-phase2-cafe-multishop-prod.mjs6/6Demo creds, parallel-safe
test-m1-prod.mjs10/10Shop scoping
test-r7-prod.mjs14/14Solo retry — first 2 attempts hit cold-worker dashboard render > 15s
test-launchpad-fix-prod.mjs8/8SSO bridge auto-create
test-r8-prod.mjs4/4Closed via the auth-cache + login-wait fixes below

93/93 effective after the two follow-ups landed (see “Follow-ups shipped same session” below).

Follow-ups shipped same session (3 extra commits)

b611bb8 (backend) — fix(backend R9 follow-up): drop too-strict (tenant, order_number)
                    unique on cafe.orders, replace with two partial uniques
  Migration 20260509120000_cafe_orders_business_date_unique.ts:
    + cafe.orders.business_date (DATE, nullable; backfilled from
      created_at AT TIME ZONE tenant.timezone for 145 post-R5.2 rows)
    - dropped (tenant_id, order_number) unique
    + partial unique (tenant_id, order_number) WHERE pos_config_id IS NULL
      (preserves legacy behavior for ~20.8k pre-R5.2 NULL-config rows)
    + partial unique (tenant_id, pos_config_id, business_date, sequence_no)
      WHERE pos_config_id IS NOT NULL (the actual conceptual invariant)
  No user-visible format change. Same-seq across different days now allowed.

fd64ac9 (cafe) — fix(cafe R9 follow-up): thread business_date into
                 createNativeOrder + cafeOrders schema for the new partial unique
  + lib/db/orders.ts: CreateOrderInput.businessDate, persisted on insert
  + lib/db/schema.ts: businessDate column on cafeOrders
  + app/api/cafe/orders/route.ts: thread businessDate (already computed
    above for the sequence step)

f6a4457 (cafe) — fix(cafe R8.2 follow-up): bypass Hyperdrive cache on tenantUser
                 SELECT in auth() so freshly-minted sids are seen immediately
  Wraps the tenantUsers SELECT in db.transaction() so the active_session_token
  read is not served from Hyperdrive's ~60s cache. Without this, a freshly-
  logged-in user (whose JWT carries the just-minted sid) hit the stale
  cached row with the prior sid → mismatch → /cafe/* 307'd to /auth/login →
  already authed → bounced to / (Commerce launchpad). Symptom was "fresh
  login can't access /cafe/team for ~60s." Affected r8 prod test repeatedly.

Plus test-r8-prod.mjs got two surgical edits:
  - loginSso bumped from waitForResponse(/tenant/auth/me) + 1500ms to a flat
    4000ms wait (matches the test-r9 + test-cash-carryover pattern that
    lands on /cafe/* cleanly).
  - Team-page click sequence now does waitForTimeout(2000) + waitFor visible
    + click, instead of relying on click()'s implicit wait that doesn't
    surface auth bounces clearly.

Result: r9 16/16 still green WITHOUT the seq-floor workaround (proves
the new partial unique allows same-seq across days as designed) and r8
4/4 green for the first time since the cold-worker churn from earlier
deploys started.

Architecture notes

partialRefundOrder(tenantId, orderId, lineRefunds?, reason, actor):
  Locks the order row FOR UPDATE.
  Validates: state in ('paid', 'partial_refund'), every selection is in
  the order, qty within (line.qty - line.refundedQty).
  Refund line value = (qty / line.qty) * (line.subtotal - line.discount).
  Bumps cafe.order_lines.refunded_qty per touched line.
  Recomputes parent state:
    all lines fully refunded  → 'refunded'
    any line partially refunded → 'partial_refund'
    nothing touched           → 'paid' (shouldn't happen post-validation)
  On full refund: sets cafe.orders.odoo_refund_synced_at = NOW() so the
  legacy listPendingRefunds queue ignores it (R9.2's new queue takes over).

R9.2 worker (cron route folds in alongside creates/refunds):
  listPendingPartialRefunds limits to refunds whose parent order has
  odoo_order_id IS NOT NULL (the original must be in Odoo before the
  refund counter-order can link via refunded_order_id). Backoff: 1m
  → 2m → 4m → 8m → 16m, capped at 16m, dead-letter at 5 retries.

R9.3 reservation flow:
  saveDraftAction calls advanceSequence + formatPosOrderNumber on the
  FIRST park (or any save where the existing row has order_number=NULL),
  then upsertDraft persists. Auto-saves on already-numbered drafts skip
  the mint. PayDialog forwards reservedOrderNumber + reservedSequenceNo
  + draftId; the orders POST route uses the reserved number instead of
  minting fresh, then deletes the draft.

R9.4 cross-component sync:
  RegisterShell holds the live cart; OrdersView reads server-rendered
  drafts. router.refresh() runs in OpenPhase when view flips to 'orders'
  so the latest data lands. Resume + Add-new use window CustomEvents
  (`nix-cafe:resume-draft`, `nix-cafe:add-new-order`) so the OrdersView
  can hand off to the still-mounted-after-tab-flip RegisterShell.
01Register opened
Cashier R9 Test … logged in. Top bar shows old layout (Display / Kitchen badge / Cash / Orders / Lock / Close shift) — R9 leaves these untouched.
02Order rung
Bread $2.50 + 50% Sugar $0 paid in cash. Success modal renders with order number and change.
03Draft parked
DraftsStrip chip (above the cart) renders the reserved POS07-NNNN instead of the legacy “Order N” placeholder.
04Orders view (Active, empty)
New layout matches Narong's Odoo-POS mockup. Tab strip [Register][Orders][+][Get Coffee TSP …], Search Orders input, Active filter, paginator, and the cart icon empty state on the right.
05Active filter populated
Parked draft surfaces here with date/time, register#, customer (—), total, and Ongoing chip. Click resumes via CustomEvent to RegisterShell.
06Paid detail + numpad
“Select the product(s) to refund” helper text at top. Lines on the left, action row + qty numpad on the right (Details/Invoice grayed for now), big red Refund button at the bottom.
07After Refund
Order state flips. DB confirms cafe.order_refunds row created with -R1 suffix, line.refunded_qty bumped, refund_lines populated.

Probe output

loading…