diff --git a/apps/machine/src/stores/atm.ts b/apps/machine/src/stores/atm.ts index c868322..f9af8cb 100644 --- a/apps/machine/src/stores/atm.ts +++ b/apps/machine/src/stores/atm.ts @@ -343,6 +343,12 @@ export const useAtmStore = defineStore('atm', () => { // The card balance is hidden by default on the public screen; the holder // reveals it with the eye toggle. Resets on re-lock. const cardBalanceRevealed = ref(false) + // Set when a card accepted a cash-out pull and the machine then left the + // invoice screen without ever seeing the payment settle. The customer's + // wallet paid and no cash came out, so this must be visible and carry the + // reference an operator can reconcile against — never a silent return to + // the amount screen. Cleared when the next transaction starts. + const settlementError = ref<{ txid: string | null; message: string } | null>(null) // When the card server names a currency but didn't price the balance (its // rate cache was cold — it never blocks the unlock on a rate lookup), price // it here from the ATM's own rate source, in that currency. @@ -663,6 +669,26 @@ export const useAtmStore = defineStore('atm', () => { (prevNestedState === 'displayingInvoice' && currentNested !== 'displayingInvoice') || (prevNestedState === 'displayingQR' && currentNested !== 'displayingQR') if (leftBoltCardScreen) { + // The card accepted the pull (that's what leaves processing latched + // with an 'accepted' status) but we left the invoice screen for + // somewhere other than the dispenser: the payment was taken and no + // cash followed. Surface it with the txid instead of dropping the + // customer back on the amount screen as if nothing had happened. + if ( + prevNestedState === 'displayingInvoice' && + currentNested !== 'dispensingCash' && + boltCardProcessing.value && + nfcStatus.value?.state === 'accepted' + ) { + const txid = context.value?.txid ?? null + console.error( + `[ATM] Settlement never confirmed after the card accepted the pull — txid=${txid}` + ) + settlementError.value = { + txid, + message: 'Your payment was accepted but the cash was not dispensed.', + } + } boltCardProcessing.value = false nfcStatus.value = null } @@ -1786,10 +1812,12 @@ export const useAtmStore = defineStore('atm', () => { // Convenience methods for common events function selectCashIn() { + settlementError.value = null send({ type: 'SELECT_CASH_IN' }) } function selectCashOut() { + settlementError.value = null send({ type: 'SELECT_CASH_OUT' }) } @@ -1942,6 +1970,7 @@ export const useAtmStore = defineStore('atm', () => { simulateBoltCardEntry, // Access control (ADR-003) + settlementError, accessControl, isLocked, grantAccess, diff --git a/apps/machine/src/views/CashOutView.vue b/apps/machine/src/views/CashOutView.vue index 50980ec..50a43a2 100644 --- a/apps/machine/src/views/CashOutView.vue +++ b/apps/machine/src/views/CashOutView.vue @@ -178,6 +178,24 @@ function formatFiat(cents: number): string { key="selectingAmount" class="flex flex-1 flex-col justify-center px-4 lg:px-[8vw] py-4 lg:py-6 gap-4 lg:gap-6" > + +
+ {{ atmStore.settlementError.message }} +
++ Please contact the operator + and quote + {{ atmStore.settlementError.txid }} . +
+