feat(machine): record failed dispenses and per-cassette tracking

Failed dispenses (sats debited, cash not dispensed) were invisible —
transactions only recorded on 'complete'. Now records on 'dispenseError'
with status ('dispense_error'|'partial'|'complete'), error message, and
per-cassette detail.

Also fixes a bug in both HAL services where dispense results were mapped
by amounts-array index instead of cassette position, causing swapped
denomination counts when cassette order differs from request order.

Schema v3→v4: adds status/error columns to transactions, new
cassette_bills table for per-cassette provisioned/dispensed/rejected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-22 17:53:10 -04:00
commit a21865ff20
10 changed files with 287 additions and 53 deletions

View file

@ -51,6 +51,7 @@ export {
type PaymentStatus,
type PaymentMethod,
type DispenseCashResult,
type CassetteBillResult,
initialContext,
} from './types.js'

View file

@ -5,14 +5,32 @@
/** Payment status */
export type PaymentStatus = 'pending' | 'paid' | 'failed' | null
/** Per-cassette dispense result (position-aware) */
export interface CassetteBillResult {
/** Cassette name, e.g. "cassette1", "cassette2" */
name: string
/** 0-indexed physical position */
position: number
/** Bill denomination in this cassette */
denomination: number
/** Bills requested from this cassette */
provisioned: number
/** Bills actually dispensed */
dispensed: number
/** Bills rejected/jammed */
rejected: number
}
/** Result of a dispense operation (always resolves, never throws) */
export interface DispenseCashResult {
/** Per-cassette results (what was actually dispensed) */
/** Per-denomination results (what was actually dispensed) */
bills: { denomination: number; dispensed: number; rejected: number }[]
/** Whether the full requested amount was dispensed */
dispensed: boolean
/** Error message if dispense failed or was partial */
error?: string
/** Per-cassette detail (position-aware, produced by HAL) */
cassettes?: CassetteBillResult[]
}
/** Payment methods supported */