The payments side of our POS (StoreFlux, a headless commerce backend) got most of the write-up attention so far. The shift and cash-drawer accounting underneath it is smaller in scope but had its own interesting constraints, so here's how that part actually works.
The requirement: a real variance number, not a vibe
A cashier opens a shift with a starting cash count. Through the shift they take cash payments, maybe pay cash out for something (a supplier delivery, petty expenses), maybe put more cash in. At close, they count the drawer again. The number that matters is: what should be in there, given everything that happened, versus what's actually in there. That gap is the variance, and it has to be a real computed number a manager can look at, not something eyeballed.
Shifts are event-sourced, same as the rest of the till
Everything at the register already flows through an outbox of client-generated, idempotent events (sales, refunds, product edits — covered in an earlier post). Shifts fit the same model instead of getting special-cased: shift.opened (carries the starting float), cash.moved (a paid-in or paid-out, with a reason), and shift.closed (carries the counted-at-close amount). The shift's id is literally the register's client-generated guid for that shift — no separate id-minting round trip needed before the register can start tagging sales with which shift they belong to.
Every sale and refund that happens during an open shift carries that shift id. That's what makes the expected-cash calculation possible after the fact: starting float, plus cash collected from sales in that shift, minus cash refunded, plus or minus any manual cash movements, equals what should be in the drawer. Compare that to the counted amount from shift.closed and the variance falls out.
Doing that math in two places, correctly, is the actual hard part
Same story as the money math elsewhere in this system: the Rust register computes an expected-cash number locally so a cashier can see it the moment they close a shift, with zero network round trip. The C# server computes the same number independently once the events land, because the server is the one source of truth once everything's synced — a register's local math is a preview, not the final answer. Those two computations have to agree, or a manager ends up staring at two different "the drawer should have X in it" numbers from the same shift and trusting neither.
This is handled with the same parity-testing discipline as the rest of the sync layer: replay a shift's full event sequence through both implementations and diff the result. It's not glamorous, but it's the only way I trust two independently-written pieces of arithmetic to actually match under real event orderings, including the annoying cases — multiple cash-outs in one shift, a refund that happens to land in a different shift than its original sale because it was processed offline and back-dated to the sale's actual time.
Offline refunds and which shift they belong to
That last case deserves its own mention because it was a genuine bug we caught, not a hypothetical. A refund processed while offline gets dated to when it actually happened, not when it finally syncs — otherwise a refund given during Tuesday's shift could get silently counted against Wednesday's drawer because that's when the connection came back. Getting this right means the variance calculation for a shift has to be based on "what happened during this shift's real time window," not "what the server received while this shift's status was open." Those are subtly different, and conflating them is exactly the kind of bug that doesn't show up in testing until you specifically simulate being offline across a shift boundary.
What a manager actually sees
None of this is exposed as raw event data, obviously — the owner-facing side is just an endpoint that returns a shift with its expected cash, counted cash, and variance already computed, plus the list of cash movements with whatever reason the cashier entered. The accounting happens once, server-side, from the event history; nothing is recomputed differently depending on who's asking.
Still open: an X-report style printable summary at shift close, and automatic variance-threshold alerts (right now a manager has to go looking at the dashboard rather than getting proactively flagged when a shift closes meaningfully short or over). Those are next.
storeflux.store/#pos if you want to see where this sits in the product. Curious whether anyone else doing register/shift software has hit the same "two engines must agree on money" problem and solved it differently than parity testing.

