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:
parent
3ab03d05ac
commit
a21865ff20
10 changed files with 287 additions and 53 deletions
|
|
@ -38,6 +38,14 @@ export interface DispenseResult {
|
|||
bills: { denomination: number; dispensed: number; rejected: number }[]
|
||||
dispensed: boolean
|
||||
error?: string
|
||||
cassettes?: {
|
||||
name: string
|
||||
position: number
|
||||
denomination: number
|
||||
provisioned: number
|
||||
dispensed: number
|
||||
rejected: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface HalInstance {
|
||||
|
|
@ -175,26 +183,37 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
|
|||
|
||||
const result = await dispenser.dispense(notes)
|
||||
|
||||
// Update inventory
|
||||
for (let i = 0; i < result.value.length; i++) {
|
||||
const denom = cassetteDenominations[i]
|
||||
if (denom !== undefined && inventory[denom] !== undefined) {
|
||||
inventory[denom] -= result.value[i]?.dispensed ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
// Build per-denomination result
|
||||
const bills = amounts.map((a, i) => ({
|
||||
denomination: a.denomination,
|
||||
// Build per-cassette results (position-aware, matches result.value ordering)
|
||||
const cassetteResults = cassetteDenominations.map((denom, i) => ({
|
||||
name: `cassette${i + 1}`,
|
||||
position: i,
|
||||
denomination: denom,
|
||||
provisioned: notes[i] ?? 0,
|
||||
dispensed: result.value[i]?.dispensed ?? 0,
|
||||
rejected: result.value[i]?.rejected ?? 0,
|
||||
}))
|
||||
|
||||
// Update inventory based on what was ACTUALLY dispensed (per-cassette)
|
||||
for (const c of cassetteResults) {
|
||||
if (c.dispensed > 0 && inventory[c.denomination] !== undefined) {
|
||||
inventory[c.denomination] -= c.dispensed
|
||||
}
|
||||
}
|
||||
|
||||
// Build per-denomination result (backward compatible, only requested denoms)
|
||||
const bills = cassetteResults
|
||||
.filter((c) => c.provisioned > 0)
|
||||
.map((c) => ({
|
||||
denomination: c.denomination,
|
||||
dispensed: c.dispensed,
|
||||
rejected: c.rejected,
|
||||
}))
|
||||
|
||||
const totalRequested = amounts.reduce((s, a) => s + a.count, 0)
|
||||
const totalDispensed = bills.reduce((s, b) => s + b.dispensed, 0)
|
||||
|
||||
if (result.error) {
|
||||
return { bills, dispensed: false, error: result.error.message }
|
||||
return { bills, cassettes: cassetteResults, dispensed: false, error: result.error.message }
|
||||
}
|
||||
|
||||
// Wait for customer to take bills
|
||||
|
|
@ -203,7 +222,7 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
|
|||
console.log('[HAL] Bills removed by customer')
|
||||
}
|
||||
|
||||
return { bills, dispensed: totalRequested === totalDispensed }
|
||||
return { bills, cassettes: cassetteResults, dispensed: totalRequested === totalDispensed }
|
||||
},
|
||||
|
||||
getInventory: () => ({ ...inventory }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue