feat(schema): dispense outcome on settlements, dispense_reports, cash-out hold mirror (ADR-005)
m016: an append-only `dispense_reports` table (lamassu-server's cash_out_actions shape — one row per report the machine sent, so a retry, a late report and a remediation report stay distinct); dispense_confirmed / dispense_error / dispense_error_code / dispense_raw_code / dispense_error_class / dispense_reported_at / dispensed_fiat_cents on dca_settlements; cash_out_held_since / _reason / _code on dca_machines beside counts_uncertain_since. Settlement lifecycle gains awaiting_dispense (cash_out at insert — paid, waiting for the machine's report), partial_pending (some notes out, value short; held whole until the operator records the resolution) and cash_owed (nothing out; legs never run). dispense_unreported is derived by the worklist, not stored. resume_cash_out joins CASSETTE_OP_TYPES as a machine-wide op: position 0, no position on the wire, no bay fields. It rides the operator channel the machine already consumes; the machine honours it only if stamped after the hold began. A recount releases the hold too. crud: get_settlement_by_txid (the join the machine's extra.txid already provides), apply_dispense_outcome (copies the report onto the settlement and finally writes bills_json / cassettes_json with what actually came out), the dispense_reports accessors incl. adopting a report that arrived before its payment, set_machine_cash_out_hold, and the three new worklist buckets.
This commit is contained in:
parent
d09c51f277
commit
b8a5e6352a
3 changed files with 511 additions and 18 deletions
|
|
@ -941,3 +941,82 @@ async def m015_add_cassette_state_seq(db):
|
|||
await db.execute(
|
||||
"ALTER TABLE spirekeeper.cassette_configs ADD COLUMN state_seq INTEGER"
|
||||
)
|
||||
|
||||
|
||||
async def m016_dispense_outcome(db):
|
||||
"""The dispense outcome becomes a first-class fact (bitspire ADR-005 §2).
|
||||
|
||||
Until now a cash-out settlement was captured the instant the payment
|
||||
landed: `_handle_payment` spawned distribution in the same breath, so by
|
||||
the time a dispenser jammed two seconds later the legs were already paid
|
||||
and the dashboard honestly reported `processed`. The machine now reports
|
||||
every cash-out's outcome over a `report_dispense` RPC and the settlement
|
||||
waits for it (`awaiting_dispense`) before anything moves.
|
||||
|
||||
`dispense_reports` is append-only, one row per report the machine sent —
|
||||
lamassu-server's `cash_out_actions` shape — so a retry, a late report and
|
||||
a remediation report are all visible as distinct rows. `settlement_id` is
|
||||
NULL for a report whose payment this server never saw.
|
||||
|
||||
The settlement carries the three lamassu fields (dispense_confirmed,
|
||||
error, error_code) plus raw_code / error_class and the fiat value that
|
||||
actually left the machine, so the partial-dispense dialog can be
|
||||
pre-filled with the hardware's own number instead of a typed one.
|
||||
|
||||
The machine's cash-out hold is mirrored onto its registry row beside
|
||||
counts_uncertain_since: a latched machine refuses cash-out until an
|
||||
operator recounts or publishes `resume_cash_out`, and the dashboard needs
|
||||
to show that and offer the button.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS spirekeeper.dispense_reports (
|
||||
id TEXT PRIMARY KEY,
|
||||
machine_id TEXT NOT NULL,
|
||||
settlement_id TEXT,
|
||||
txid TEXT NOT NULL,
|
||||
payment_hash TEXT,
|
||||
dispense_confirmed BOOLEAN NOT NULL,
|
||||
error TEXT,
|
||||
error_code TEXT,
|
||||
raw_code TEXT,
|
||||
error_class TEXT,
|
||||
fiat_cents INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
bills_json TEXT NOT NULL,
|
||||
cassettes_json TEXT NOT NULL,
|
||||
counts_uncertain BOOLEAN NOT NULL DEFAULT false,
|
||||
remediates_txid TEXT,
|
||||
reported_at TIMESTAMP NOT NULL,
|
||||
received_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"CREATE INDEX IF NOT EXISTS dispense_reports_txid_idx "
|
||||
"ON dispense_reports (machine_id, txid)"
|
||||
)
|
||||
await db.execute(
|
||||
"CREATE INDEX IF NOT EXISTS dispense_reports_settlement_idx "
|
||||
"ON dispense_reports (settlement_id)"
|
||||
)
|
||||
for col, typ in (
|
||||
("dispense_confirmed", "BOOLEAN"),
|
||||
("dispense_error", "TEXT"),
|
||||
("dispense_error_code", "TEXT"),
|
||||
("dispense_raw_code", "TEXT"),
|
||||
("dispense_error_class", "TEXT"),
|
||||
("dispense_reported_at", "TIMESTAMP"),
|
||||
("dispensed_fiat_cents", "INTEGER"),
|
||||
):
|
||||
await db.execute(
|
||||
f"ALTER TABLE spirekeeper.dca_settlements ADD COLUMN {col} {typ}"
|
||||
)
|
||||
for col, typ in (
|
||||
("cash_out_held_since", "TIMESTAMP"),
|
||||
("cash_out_held_reason", "TEXT"),
|
||||
("cash_out_held_code", "TEXT"),
|
||||
):
|
||||
await db.execute(
|
||||
f"ALTER TABLE spirekeeper.dca_machines ADD COLUMN {col} {typ}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue