fix(cash-out): settlement watch missed one-tap payments #99
2 changed files with 47 additions and 0 deletions
fix(machine): surface a payment taken with no cash dispensed
When a card accepted a cash-out pull and settlement never confirmed, the machine returned to the amount screen as though nothing had happened — the customer's wallet had paid and there was nothing on screen or in the journal to say so. Latch that transition, log it with the txid, and show a red notice naming the reference an operator can reconcile against. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
commit
67573008ee
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<!-- A payment was taken and no cash came out. Say so plainly, with
|
||||
the reference an operator needs; do not let the customer wander
|
||||
back into the amount grid believing nothing happened. -->
|
||||
<div
|
||||
v-if="atmStore.settlementError"
|
||||
class="rounded-lg border-2 border-destructive bg-destructive/10 px-4 py-3 text-center lg:px-6 lg:py-4"
|
||||
>
|
||||
<p class="text-base font-bold text-destructive lg:text-2xl">
|
||||
{{ atmStore.settlementError.message }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground lg:text-lg">
|
||||
Please contact the operator<template v-if="atmStore.settlementError.txid">
|
||||
and quote
|
||||
<span class="font-mono-code">{{ atmStore.settlementError.txid }}</span> </template
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Denomination grid -->
|
||||
<div class="grid grid-cols-2 gap-3 lg:gap-6">
|
||||
<div
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue