← All tests

POS Section Rework — Slice 1 (local)

2026-05-14 Gate 1 local. First slice of the POS Section Rework arc (Narong spec items 8 & 9). Session-scoped landing-summary data layer. The /cafe/pos landing cards already scoped sales by session_id — the real defect was that the summary query filtered state <> 'closed', so a register whose latest session had already closed showed a blank, all-zeros card. This slice reworks the data layer to take the register's latest session of any state and adds the closing-side metrics (ending cash/bank, cash sales, cash in/out, theoretical closing) every card now needs. Pure data-layer change — the card UI itself is Slice 2. No migration.

10/10 local DAO probe PASS. nix-cafe typechecks clean. No data migration. 6 files touched, 0 added.

The bug, precisely

Narong: "the overview Opening/Sold etc. is tied to Date not Session…"

Investigation found the cards were ALREADY session-scoped — the field
names soldToday / bankToday were just misleading. The actual defect:

  fetchSessionSummariesFromMirror  →  WHERE state <> 'closed'
  buildStarterLanding              →  WHERE state = 'open'

A register can hold many sessions over time (shifts — morning /
afternoon / evening, often several per day). When a register's NEWEST
session is already closed, both queries skipped it entirely → the card
fell back to state 'none' + all zeros, even though that closed shift
has real Opening / Sold / Closing numbers worth showing.

Slice 1 fix: take the latest session per register regardless of state,
and surface its closing-side numbers.

Metric definitions (Narong, 2026-05-14)

Theoretical Closing  =  Expected Balance
                     =  openingCash + cashSales + cashIn − cashOut
                        (system-computed, per session window)

Ending Balance       =  the actual Cash Count the cashier enters at
                        session close  (cafe.sessions.ending_cash)

Session window: scoping is by session_id FK, not a timestamp range —
cafe.orders, cafe.order_payments (via orders) and cafe.cash_movements
all reference the session row directly, so "everything in the session
window" is exactly "everything with this session_id".

Local DAO probe — 10/10

✓ Probe ran clean against local nix-db + cleaned up its fixture
✓ fetchLatestSessionSummaries returns ≤1 summary per active register
✓ Latest-session pick matches an independent JS fetch-all-and-pick
✓ Closed-latest register surfaces its session, not a blank 'none' card (the bug fix)
✓ Closed-canary metrics exactly match the known seeded fixture
✓ Canary metrics also match an independent recompute from the tables
✓ theoreticalClosing = openingCash + cashSales + cashIn − cashOut
✓ landingStateForSession maps cafe state → landing card enum
✓ buildStarterLanding shows a 'closed' card with real numbers (pre-Slice-1 it was 'none')
✓ fetchSessionSummariesFromMirror prefill carries every new field

Fixture: local lumiere-coffee has 360 closed sessions but all 4 open
sessions are newer, so no register naturally has a closed *latest*. The
probe self-seeds ONE deterministic closed session (fixed UUID, opened_at
= now() so it's the register's newest) with 2 paid orders + cash/card
payments + cash in/out movements, runs every assertion, then deletes the
session — the FK cascade removes the orders, payments and movements.
Pre-clean by the same fixed UUID makes a re-run idempotent.

Seeded fixture → helper output (all matched exactly):
  openingCash 100.00   endingCash 287.50   endingBank 120.00
  soldTotal 287.50 (2 orders)   cashSales 150.00   bankTotal 137.50
  cashIn 50.00   cashOut 12.50
  theoreticalClosing = 100 + 150 + 50 − 12.50 = 287.50

⚠ What Gate 2 covers

Local lumiere-coffee is Starter (no odoo_pos_config_id), so the Pro
mirror path fetchSessionSummariesFromMirror only gets a SHAPE check
locally (prefilled map carries every new field). The full Pro-path
validation runs at Gate 2 against get-coffee, whose registers have
real closed sessions, cash movements and Odoo ids:

  • a register whose latest session is closed shows that session's
    real Opening / Closing / Sold / Theoretical Closing
  • a register with multiple sessions in one day shows only the latest
  • the manager-live drawer + /cafe/pos landing render without regress
  • 51/51 regression sweep

Note: starter_shift.ts (getStarterExpectedCash / closeStarterShift)
computes the same opening + cashSales + cashIn − cashOut formula inline.
Slice 1 deliberately did NOT refactor it onto computeTheoreticalClosing
— that touches close-shift money math (the helper rounds to cents, the
inline version doesn't). Optional follow-up; kept out to keep Slice 1's
blast radius to the landing path only.

Files touched

nix-cafe (6 files, no migration):
  lib/db/cafe_sessions.ts     (+ fetchLatestSessionSummaries, computeTheoreticalClosing,
                                 landingStateForSession, SessionSummary type)
  lib/db/pos_configs.ts       (fetchSessionSummariesFromMirror reworked; type extended)
  lib/db/starter_landing.ts   (buildStarterLanding → shared helper)
  lib/db/manager_live.ts      (nixCashByConfig overlay removed; cardFor extended)
  lib/odoo/queries.ts         (PosConfigSummary extended + placeholder defaults)
  app/(authed)/pos/pos-landing.tsx  (LandingConfig type extended — UI consumes in Slice 2)