feat(v2): operator-side cassette inventory v1.1 + signer.nip44_* migration (#29) #30
1 changed files with 77 additions and 0 deletions
feat(v2): m008 flip cassette_configs PK to (machine_id, position) (#29 v1.1)
Some checks failed
ci.yml / feat(v2): m008 flip cassette_configs PK to (machine_id, position) (#29 v1.1) (pull_request) Failing after 0s
Some checks failed
ci.yml / feat(v2): m008 flip cassette_configs PK to (machine_id, position) (#29 v1.1) (pull_request) Failing after 0s
Coordinated v1.1 fix with the ATM side per coord-log 2026-05-30T18:30Z
+ 18:45Z. The m007 schema's denomination-PK was wrong:
- Operators need to swap cartridge denominations during refill (a $20
bay becomes a $50 bay) without re-provisioning. m007 made
denomination immutable per slot.
- Real machines have N cartridges of the same denomination for
cash-out throughput (e.g., four $20 cartridges on a single ATM to
avoid mid-day refill). m007 + denomination-PK rejected duplicates.
The flip:
- PK becomes (machine_id, position). Position is the fixed hardware
bay number; denomination + count are operator-editable per row.
- No UNIQUE constraint on denomination — multiple same-denom cassettes
are operationally valid.
- New nullable column state_denomination for v2 reverse-channel
reconciliation (operator-believed denomination per slot vs ATM-
reported denomination — diff highlighting in v2 UI).
SQLite doesn't support ALTER PRIMARY KEY directly; the migration does
the standard create-new / backfill-from-old / drop / rename dance.
Idempotent via state_denomination column-probe.
Backfill choice: in m007 the row's denomination was simultaneously the
operator-believed AND the ATM-reported value (only write path was the
bootstrap consumer copying state.db verbatim). At migration time
state_denomination = current denomination as a best-guess baseline;
the next bootstrap event re-populates the state_* columns
authoritatively per the v1.1 wire shape.
Wire shape, models, CRUD, transport, consumer, API, and UI all flip in
subsequent commits. PR #30 will grow from 9 → 14 commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit
df6e8e0a22
|
|
@ -575,3 +575,80 @@ async def m007_add_cassette_configs(db):
|
|||
PRIMARY KEY (machine_id, denomination)
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m008_flip_cassette_configs_pk_to_position(db):
|
||||
"""Flip cassette_configs PK from (machine_id, denomination) to
|
||||
(machine_id, position). The denomination-keyed shape from m007 was
|
||||
wrong: real machines have N cartridges of the same denomination
|
||||
(cash-out throughput requires multiple bays for one denom), and the
|
||||
operator needs to swap cartridge denominations during refill ($20
|
||||
bay becomes $50 bay) without a re-provisioning event.
|
||||
|
||||
Coordinated v1.1 fix with the ATM side per the 2026-05-30T18:30Z +
|
||||
18:45Z log entries:
|
||||
- Wire shape flips from {denominations: {<d>: {position, count}}}
|
||||
to {positions: {<p>: {denomination, count}}}
|
||||
- Position becomes the fixed row identity (hardware bay number);
|
||||
denomination + count are operator-editable per row
|
||||
- NO unique constraint on denomination (multiple same-denom cassettes
|
||||
are operationally valid)
|
||||
|
||||
Also adds `state_denomination` nullable column reserved for v2
|
||||
reverse-channel reconciliation (operator-believed denomination per
|
||||
slot vs ATM-reported denomination — diff highlighting in v2 UI).
|
||||
|
||||
SQLite doesn't support ALTER PRIMARY KEY directly; the migration
|
||||
does the standard create-copy-drop-rename dance. Idempotent via the
|
||||
column-probe trick used elsewhere in this file.
|
||||
"""
|
||||
try:
|
||||
# Probe: does the old PK shape still exist? If state_denomination
|
||||
# column already exists, m008 already ran — no-op.
|
||||
await db.fetchone(
|
||||
"SELECT state_denomination FROM satoshimachine.cassette_configs "
|
||||
"LIMIT 1"
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS satoshimachine.cassette_configs_new (
|
||||
machine_id TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
denomination INTEGER NOT NULL,
|
||||
count INTEGER NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
updated_by TEXT,
|
||||
state_denomination INTEGER,
|
||||
state_count INTEGER,
|
||||
state_at TIMESTAMP,
|
||||
state_event_id TEXT,
|
||||
PRIMARY KEY (machine_id, position)
|
||||
);
|
||||
""")
|
||||
|
||||
# Backfill from the old table — column-by-column copy. In the v1
|
||||
# m007 schema the row's `denomination` was simultaneously the
|
||||
# operator-believed denomination AND the ATM-reported denomination
|
||||
# (because the only write path was the bootstrap consumer copying
|
||||
# from the ATM's state.db). So state_denomination at migration time
|
||||
# = current denomination as a best-guess baseline; the next bootstrap
|
||||
# event re-populates the state_* columns authoritatively.
|
||||
await db.execute("""
|
||||
INSERT INTO satoshimachine.cassette_configs_new
|
||||
(machine_id, position, denomination, count,
|
||||
updated_at, updated_by,
|
||||
state_denomination, state_count, state_at, state_event_id)
|
||||
SELECT machine_id, position, denomination, count,
|
||||
updated_at, updated_by,
|
||||
denomination, state_count, state_at, state_event_id
|
||||
FROM satoshimachine.cassette_configs
|
||||
""")
|
||||
|
||||
await db.execute("DROP TABLE satoshimachine.cassette_configs")
|
||||
await db.execute(
|
||||
"ALTER TABLE satoshimachine.cassette_configs_new "
|
||||
"RENAME TO cassette_configs"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue