feat(machine): value-confirmed dispense, cash-out hold, fault screens, counts-uncertain on zero-with-error (ADR-005 §3–§5)
HAL glue (electron/hal-service.ts and the renderer-side services/hal.ts): dispenseConfirmed is Σ(denomination × dispensed) === Σ(denomination × requested), computed on value. The driver's tagged error is carried through as errorCode / rawCode / errorClass / human; pre-dispense inventory refusals are errorClass 'inventory' so they route to outOfCash rather than the fault screen. The manual-dispense command result keeps its wire key `dispensed` (spirekeeper's poller reads it) and gains the new fields alongside. Cash-out hold: state-store persists it in meta as one JSON value beside countsUncertainSince, idempotent on set (the first fault's `since` is kept); IPC get/set/clear through preload. The store persists the hold the moment the machine sets it and restores it into the machine on boot. A recount clears it in the store (same gesture that clears counts- uncertain); operator-config also honours a new resume_cash_out op — not a cassette op, split off before applyOperatorCassetteOps, and honoured only when stamped after the hold began so a re-delivered old resume cannot clear a fresh fault. Either release calls back into the store, which sends CASH_OUT_RELEASED. The cassettes-state document carries cash_out_held_since / _reason / _code (additive, like counts_uncertain_since); the availability beacon reports cash_out false while held; the idle Sell button is disabled with the reason. Store watcher: dispenseFault and outOfCash both record dispense_error / partial (the customer has paid either way). A report of zero dispensed WITH a hardware error now sets countsUncertainSince instead of being trusted as zero — a note stopped in the transport completes neither counter (sintra 2026-10-09: bay read 66, held 65, one in the transport). Fault screen: both terminal states show "your payment went through", amount paid, per-denomination dispensed, the txid as QR and text, the payment hash (threaded from the settlement watch through PAYMENT_RECEIVED) and the time, with "keep this reference" and an acknowledge button. The raw dispenser code is not shown; it travels in the report.
This commit is contained in:
parent
3ff86de4ed
commit
888870d01a
12 changed files with 469 additions and 72 deletions
|
|
@ -27,6 +27,10 @@ import {
|
|||
getCountsUncertainSince,
|
||||
getLastStatePublishedAt,
|
||||
markCountsUncertain,
|
||||
getCashOutHold,
|
||||
setCashOutHold,
|
||||
clearCashOutHold,
|
||||
type CashOutHold,
|
||||
markStatePublished,
|
||||
resetStatePublishWatermark,
|
||||
resetForRepair,
|
||||
|
|
@ -565,6 +569,15 @@ ipcMain.handle('state:get-counts-uncertain-since', (): number | null => getCount
|
|||
ipcMain.handle('state:mark-counts-uncertain', (_event, unixTimestamp: number): void => {
|
||||
markCountsUncertain(unixTimestamp)
|
||||
})
|
||||
// Cash-out hold (ADR-005 §5)
|
||||
ipcMain.handle('state:get-cash-out-hold', (): CashOutHold | null => getCashOutHold())
|
||||
ipcMain.handle('state:set-cash-out-hold', (_event, hold: CashOutHold): CashOutHold => {
|
||||
if (!hold || typeof hold.reason !== 'string' || typeof hold.since !== 'number') {
|
||||
throw new Error('Invalid cash-out hold')
|
||||
}
|
||||
return setCashOutHold(hold)
|
||||
})
|
||||
ipcMain.handle('state:clear-cash-out-hold', (): boolean => clearCashOutHold())
|
||||
ipcMain.handle('state:mark-state-published', (_event, unixTimestamp: number): void => {
|
||||
markStatePublished(unixTimestamp)
|
||||
})
|
||||
|
|
@ -841,7 +854,7 @@ function startCommandPoller(): void {
|
|||
recordTransaction({
|
||||
txid,
|
||||
type: 'manual_dispense',
|
||||
status: result.dispensed ? 'complete' : 'dispense_error',
|
||||
status: result.dispenseConfirmed ? 'complete' : 'dispense_error',
|
||||
fiatCents: totalFiatCents,
|
||||
sats: 0,
|
||||
feeSats: 0,
|
||||
|
|
@ -860,7 +873,7 @@ function startCommandPoller(): void {
|
|||
|
||||
// Only remediate the original tx if ALL requested bills were dispensed
|
||||
let refRemediated = false
|
||||
if (parsed.ref_txid && result.dispensed) {
|
||||
if (parsed.ref_txid && result.dispenseConfirmed) {
|
||||
refRemediated = remediateTransaction(parsed.ref_txid, txid)
|
||||
}
|
||||
|
||||
|
|
@ -868,7 +881,13 @@ function startCommandPoller(): void {
|
|||
cmd.id,
|
||||
JSON.stringify({
|
||||
txid,
|
||||
dispensed: result.dispensed,
|
||||
// Wire key kept as `dispensed` — spirekeeper's command poller
|
||||
// reads it. Value is the ADR-005 value-equality confirmation.
|
||||
dispensed: result.dispenseConfirmed,
|
||||
dispense_confirmed: result.dispenseConfirmed,
|
||||
error_code: result.errorCode,
|
||||
raw_code: result.rawCode,
|
||||
error_class: result.errorClass,
|
||||
ref_remediated: refRemediated,
|
||||
error: result.error,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue