feat(shift): cash drawer balance carried across shifts + admin cash movements
New signed cash_movement event (admin-only): load/remove drawer float, signed + attributed. ShiftService folds cash payments + movements by time into a drawer balance; shift open auto-inherits the prior shift's expected closing drawer as its opening float; the Z-report reports opening/taken/added/removed/expected (= next shift's opening float). Card payments excluded (settle to bank). Routes: POST /api/cash-movement, drawer in GET /api/shift/current. ShiftControl shows the live drawer + admin load/remove form + Z-report drawer block. Wiki: shift.md.
This commit is contained in:
@@ -70,6 +70,54 @@ no variance gate, no manager override.
|
||||
close totals correct + signed + printed → close-again 409 → re-open works; readonly 403;
|
||||
verifyChain ok.
|
||||
|
||||
## Drawer balance — opening float, cash movements, carry-over (decided 2026-06-18)
|
||||
|
||||
The Z-report's payment totals answer "how much did this shift *take*?" — but a manned booth also has a
|
||||
**physical cash drawer** that carries across shifts. The drawer is tracked as a running balance over
|
||||
the signed chain, so each shift knows what it **inherited** and what it should **hand over**.
|
||||
|
||||
**The events:**
|
||||
- A new signed **`cash_movement`** event: the admin loads or removes drawer cash, `{ amountMinor
|
||||
(signed: + load, − removal), reason, operator }`. **Admin-only** (an operator takes payments but
|
||||
cannot move the float in/out). The opening-day load (+5000 ALL) and a mid-shift withdrawal (−5000)
|
||||
are both `cash_movement` events.
|
||||
- The existing `payment` events already add cash to the drawer (cash tender only; card never touches
|
||||
the drawer).
|
||||
|
||||
**The math — drawer is a fold over the chain BY TIME, not by operator** (a `cash_movement` is the
|
||||
admin's, not the shift operator's, so it can't key off `identity`):
|
||||
|
||||
```
|
||||
expectedDrawer(at) = Σ cash payments (tender=cash) up to `at`
|
||||
+ Σ cash_movement amounts up to `at`
|
||||
```
|
||||
|
||||
A shift's **opening float = expectedDrawer(shiftStart)** — i.e. everything that happened to the drawer
|
||||
before this shift's start mark. It is **auto-inherited from the chain** (no operator entry). The
|
||||
first shift ever opens at **0**; the admin's load makes it 5000.
|
||||
|
||||
**The Z-report at close** reports the full drawer picture for the shift window `[start, end]`:
|
||||
`openingFloat`, `cashTakenMinor` (cash payments in-window), `cashAddedMinor` / `cashRemovedMinor`
|
||||
(movements in-window), and `expectedDrawerMinor = openingFloat + cashTaken + cashAdded − cashRemoved`.
|
||||
That `expectedDrawer` is exactly the **next** shift's opening float — the carry-over.
|
||||
|
||||
**Worked example (the canonical scenario):**
|
||||
|
||||
| Step | Event | Drawer |
|
||||
| --- | --- | --- |
|
||||
| Opening day | admin `cash_movement` +5000 | 5000 |
|
||||
| Shift 1 takes 6500 cash | payments | 11500 |
|
||||
| Shift 1 closes | Z: open 5000, took 6500, expected **11500** | 11500 |
|
||||
| Shift 2 opens | opening float = **11500** (inherited) | 11500 |
|
||||
| admin `cash_movement` −5000 | withdrawal | 6500 |
|
||||
| Shift 2 takes 4500 cash | payments | 11000 |
|
||||
| Shift 2 closes | Z: open 11500, took 4500, removed 5000, expected **11000** | 11000 |
|
||||
| Shift 3 opens | opening float = **11000** | … |
|
||||
|
||||
Card payments are excluded from the drawer (they settle to the bank, not the till). The drawer figure
|
||||
is **expected**, not counted — the optional blind-count enhancement below would record the *variance*
|
||||
against it.
|
||||
|
||||
## Where the fraud control actually lives
|
||||
|
||||
Deliberately **not** in a shift-close ceremony. Because every payment is a **signed event in the
|
||||
@@ -86,6 +134,9 @@ reconciles the signed Z-report against the actual drawer and the bank/POS batch
|
||||
|
||||
## Open
|
||||
|
||||
- **Drawer carry-over (decided 2026-06-18, building):** opening float auto-inherits the prior shift's
|
||||
expected drawer; admin-only `cash_movement` events; Z-report reports the full drawer picture. See
|
||||
the Drawer balance section above.
|
||||
- **Shift ↔ session boundary:** a vehicle may enter under one shift and pay under another — the
|
||||
Z-report sums by **payment time** (when cash/card was taken), which is the operator who handled
|
||||
the money. Confirm that's the intended accountability (vs. by entry).
|
||||
|
||||
Reference in New Issue
Block a user