← All tests

NIX-OS-84 — Cash carry-over PROD

2026-05-08 on prod. When a cashier opens a new shift, Beginning cash now defaults to the previous close's ending_cash on the same register (still editable). Eliminates the "type yesterday's number" friction on shift handover. Single backend-free fix; no migration; ~107 LOC across 7 files.

Visual proof on prod: get-coffee config 7 had a closed session with ending_cash=$117.00. The PreShift screen now displays "117.00" pre-filled in the input + helper text "Carried over from the last shift's close ($117.00). Edit if today's drawer count differs." Verified via real SSO + PIN-unlock flow on the lockable register. Test does not open a new shift — cancels back to lock so prod state stays clean.
Hotfix landed in this Gate 2 cycle (2 commits). First commit bb0fb8c shipped the carry-over plumbing. Prod test caught a real DAO bug — config 7's MOST RECENT closed session has ending_cash=NULL (a force-close from 18 min after the $117 close), and the original DAO ordered by closed_at DESC regardless of NULL → returned null → input stayed blank. The local DB happened to have only non-null closes for the test register, so the bug only surfaced in prod. Fix 94067a5 adds isNotNull(ending_cash) to the WHERE so the carry-over falls back to the last RECONCILED close instead of blanking. Lesson: prod data shapes are richer than local seeds; visual prod tests with real fixtures catch DAO edge cases that synthetic local data misses.
PreShift screen showing $117.00 carried over from previous close
PreShift screen on get-coffee config 7 — input pre-filled with $117.00, helper text reflects carry-over.

Commits

bb0fb8c (cafe) — feat(cafe NIX-OS-84): cash carry-over — open-shift form pre-fills with last closed session ending_cash on the same register
94067a5 (cafe) — fix(cafe NIX-OS-84): skip closed sessions with NULL ending_cash so carry-over falls back to last reconciled close instead of blanking

Files (7):
  lib/db/cafe_sessions.ts
    + UserDAO.getLastClosedSessionEndingCash(tenantId, posConfigId)
      Hyperdrive-cache wrapped via db.transaction. Filters
      ending_cash IS NOT NULL so a force-close doesn't shadow a real close.

  app/(authed)/pos/_components/lock-screens.tsx
    + PreShiftScreen accepts defaultBeginningCash?: number | null
    + useState(defaultBeginningCash != null ? toFixed(2) : "")
    + helper text flips between "Carried over from the last shift's close..."
      and the existing "Suggested if unsure: $0.00..." copy

  app/(pos-fullscreen)/pos/register/[configId]/{lockable-shell,page}.tsx
    + Both Pro path + StarterLockablePath: when phase==='preshift',
      fetch + pass defaultBeginningCash to the shell.

  app/(authed)/pos/_components/starter-lockable-shell.tsx
    + threaded defaultBeginningCash through to PreShiftScreen.

  app/(authed)/pos/starter-register-page.tsx
    + Starter in-app form path: fetch + pass to OpenShiftForm.

  app/(authed)/pos/_components/open-shift-form.tsx
    + defaultBeginningCash?: number | null prop on the in-app variant.

Regression sweep — sequential per R8.2 rule

SuiteResultNotes
test-cash-carryover-prod.mjs (this fix)9/9End-to-end SSO + PIN-unlock + visual input value assertion + DB cleanup
test-launchpad-fix-prod.mjs8/8Outdoor SSO bridge fix — sanity
test-r7-prod.mjs14/14Solo retry — first run hit a cold-worker timeout on /dashboard's heavier queries (KPIs + Ranking + Recent Orders); /reports + Starter all green on first pass. Worker warm → 14/14 on retry.
test-phase2-cafe-multishop-prod.mjs6/6Page-level ShopSelector cleanup still good

37/37 effective. Run sequentially per the new workflow rule (R8.2 single-session enforcement kicks parallel narongix-shared logins out of each other's sessions).

Failure modes (all silent fallback to blank input)

- No prior close on this register → null → field stays blank. Same as today.
- All prior closes have NULL ending_cash (cashier force-closed without count)
  → null → blank. NULL filter ensures we don't return a meaningless null.
- DB error during fetch → caught + logged → null → blank. No exception bubbles.
- Cashier wants a different number → field is editable, normal validation runs.

The Hyperdrive db.transaction() wrap matches getOpenNixSessionForConfig so
close-shift → reload → re-open-shift sees the fresh ending_cash, not a
60s-stale cached read.

Probe + sweep output

loading…