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.
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.
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.
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.
| Suite | Result | Notes |
|---|---|---|
| test-cash-carryover-prod.mjs (this fix) | 9/9 | End-to-end SSO + PIN-unlock + visual input value assertion + DB cleanup |
| test-launchpad-fix-prod.mjs | 8/8 | Outdoor SSO bridge fix — sanity |
| test-r7-prod.mjs | 14/14 | Solo 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.mjs | 6/6 | Page-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).
- 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.
loading…