feat(ops): atm-reconcile — check cassette ledgers against recorded history

The cassettes table is a running total, so it can be re-derived: an
absolute truth point (a recount, or an empty) plus the refills and
dispenses since. A derived count that disagrees with the stored one is
evidence of something the ledger never saw.

Reconciliation deliberately refuses to start from a refill. A refill is
a delta, and applying deltas on top of a wrong number just carries the
error forward — which is how sintra's 20-EUR bay ran 10 notes high for
weeks while its 50-EUR bay, zeroed by an `empty` before refilling,
reconciled exactly. A bay with no baseline is reported as
unreconcilable rather than silently assumed good.

Also surfaces the two things that make a count untrustworthy: the
counts-uncertain flag, and any transaction still sitting in
dispense_error / partial.

The SQL uses scalar subqueries rather than joins on purpose — joining
transaction_bills to cassettes fans out across bays, and a LEFT JOIN
whose rows are all excluded by the baseline cutoff collapses to NULL
and poisons the arithmetic downstream (the first draft read "expected:
blank" for exactly that reason).

Exits non-zero on any gap or missing baseline so it can be run as a
check after a test session.

Refs #122
This commit is contained in:
Padreug 2026-10-09 09:40:41 +02:00
commit ee28275bf3
2 changed files with 173 additions and 0 deletions

View file

@ -376,6 +376,7 @@ in
# ATM operations
sqlite
(writeShellScriptBin "atm-transactions" (builtins.readFile ./atm-transactions.sh))
(writeShellScriptBin "atm-reconcile" (builtins.readFile ./atm-reconcile.sh))
];
# Enable SSH for remote administration