feat(machine): apply the settle_transaction operator op

ADR-005 §6: the operator paid the customer by hand and recorded it in
spirekeeper. The op carries the txid and the note; the machine flips its
own dispense_error/partial row to remediated via remediateTransaction,
which only touches rows still in an error state, so re-delivery is a
no-op. Closes the machine side of the ledger for an owed-cash sale
without dispensing anything.
This commit is contained in:
Padreug 2026-10-10 22:27:39 +02:00
commit 651c43d7b0

View file

@ -65,7 +65,21 @@ type CassetteOp = {
* machine" gesture and already clears counts-uncertain.
*/
type ResumeCashOutOp = { id: string; at: number; type: 'resume_cash_out' }
type OperatorOp = CassetteOp | ResumeCashOutOp
/**
* ADR-005 §6: the operator paid the customer by hand, off-machine, for a
* cash-out this machine recorded as dispense_error / partial. Flips that row
* to `remediated` with the note as provenance so both ledgers close on one
* act. Idempotent by nature — remediateTransaction only touches rows still
* in an error state — so re-delivery is harmless.
*/
type SettleTransactionOp = {
id: string
at: number
type: 'settle_transaction'
txid: string
note?: string
}
type OperatorOp = CassetteOp | ResumeCashOutOp | SettleTransactionOp
/** Accept operator events stamped up to this many seconds in the future. */
const MAX_FUTURE_SKEW_S = 60
@ -249,7 +263,22 @@ async function handleOperatorConfigEvent(
const resumeOps = allOps.filter(
(o): o is ResumeCashOutOp => !!o && o.type === 'resume_cash_out'
)
const ops = allOps.filter((o): o is CassetteOp => !!o && o.type !== 'resume_cash_out')
const settleOps = allOps.filter(
(o): o is SettleTransactionOp =>
!!o && o.type === 'settle_transaction' && typeof (o as SettleTransactionOp).txid === 'string'
)
for (const op of settleOps) {
const provenance = `settled-off-machine:${op.id}${op.note ? `:${op.note}` : ''}`
const changed = await api.remediateTransaction(op.txid, provenance)
console.log(
`[OperatorConfig] settle_transaction ${op.id} for ${op.txid}: ` +
(changed ? 'row marked remediated' : 'no row in an error state (already closed, or unknown)')
)
}
const ops = allOps.filter(
(o): o is CassetteOp =>
!!o && o.type !== 'resume_cash_out' && o.type !== 'settle_transaction'
)
if (holdBefore && resumeOps.some((o) => typeof o.at === 'number' && o.at > holdBefore.since)) {
await api.clearCashOutHold()
console.log(