recordTransaction() updated cassette counts with WHERE denomination = ?,
but the v9 migration made position the PK precisely so duplicate
denominations across bays are legal (and the HAL dispense path already
returns authoritative per-position results). On any machine with two
bays of the same denomination, a single dispense drained every matching
bay row — silently corrupting inventory, the operator cassette-state
publish, and out-of-money gating.
- cassettes branch: decrement by c.position
- mock-only fallback (no per-bay results): drain matching bays greedily
in position order, mirroring the dispenser's own fill order
- regression tests with a duplicate-denomination layout (3 of 5 fail
against the old code)
Found during the dev-branch architecture review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>