fix(access): instrument Complete, and say when a card can't sell
Pressing Complete on a session that fails leaves nothing in the journal: the decline path has no logging, so a failed sell is indistinguishable from a button that never fired. Add the telemetry that was missing — completeWithCard logs outcome, duration and reason, and the entry line now says whether selling is available at all. Two real defects alongside it. A session whose withdraw step the card server withheld (daily limit spent, card disabled) still rendered a Complete Sale button that could only ever fail; it now shows the server's reason instead. And the decline path advised 'tap your card to try again' even for refusals a re-tap cannot lift, so 'blocked' is now its own outcome: nothing was consumed, the session stays loaded, and no re-tap is suggested. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
67763d6e8d
commit
61d5bf0231
2 changed files with 46 additions and 9 deletions
|
|
@ -301,7 +301,11 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// 'declined' — presented and refused, or failed after presentation. The
|
||||
// boltcards server bumps the SUN counter on the first GET, so
|
||||
// treat the voucher as spent even when the failure was ours.
|
||||
type BoltCardOutcome = 'skipped' | 'accepted' | 'declined'
|
||||
// 'blocked' — refused BEFORE anything was presented, because the card
|
||||
// server withheld that step when the session opened (e.g. the
|
||||
// card's daily limit is already spent). Nothing was consumed
|
||||
// and no re-tap can lift it, so the session stays loaded.
|
||||
type BoltCardOutcome = 'skipped' | 'accepted' | 'declined' | 'blocked'
|
||||
// Where a Complete gets its voucher: a card tapped right now on the cash
|
||||
// screen (raw lnurlw, spent by this call) or the session opened at entry
|
||||
// (hit-keyed steps, no p/c).
|
||||
|
|
@ -715,6 +719,15 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
const invoice = context.value?.invoice
|
||||
if (!invoice) return 'skipped'
|
||||
if (boltCardProcessing.value) return 'skipped' // one pull at a time
|
||||
// The card server withheld the withdraw step when this session opened, so
|
||||
// there is nothing to present. Say why instead of attempting a payment.
|
||||
if ('session' in source && !source.session.withdraw) {
|
||||
nfcStatus.value = {
|
||||
state: 'declined',
|
||||
message: source.session.withdrawBlockedReason ?? 'This card cannot sell right now',
|
||||
}
|
||||
return 'blocked'
|
||||
}
|
||||
boltCardProcessing.value = true
|
||||
nfcStatus.value = { state: 'processing', message: 'Reading card…' }
|
||||
try {
|
||||
|
|
@ -722,13 +735,12 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
const api = window.electronAPI!
|
||||
const res =
|
||||
'session' in source
|
||||
? source.session.withdraw
|
||||
? await api.withdrawWithSession({
|
||||
withdraw: plainWithdrawStep(source.session.withdraw),
|
||||
bolt11: invoice,
|
||||
amountMsat,
|
||||
})
|
||||
: { ok: false, reason: source.session.withdrawBlockedReason ?? 'card cannot pay now' }
|
||||
? await api.withdrawWithSession({
|
||||
// Non-null: a withheld step is pre-flighted above.
|
||||
withdraw: plainWithdrawStep(source.session.withdraw!),
|
||||
bolt11: invoice,
|
||||
amountMsat,
|
||||
})
|
||||
: await api.lnurlWithdraw({ lnurlw: source.lnurlw, bolt11: invoice, amountMsat })
|
||||
if (res.ok) {
|
||||
nfcStatus.value = { state: 'accepted', message: 'Card accepted — confirming payment…' }
|
||||
|
|
@ -819,7 +831,10 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
console.info(
|
||||
`[ATM] Card session ${opened.ok ? 'opened' : 'refused'} in ${Date.now() - t0} ms` +
|
||||
(opened.ok
|
||||
? ` (fiat ${opened.session.fiat === null ? 'not ' : ''}priced by card server)`
|
||||
? ` (fiat ${opened.session.fiat === null ? 'not ' : ''}priced by card server; sell ` +
|
||||
(opened.session.withdraw
|
||||
? 'available)'
|
||||
: `BLOCKED: ${opened.session.withdrawBlockedReason ?? 'withdraw step withheld'})`)
|
||||
: '')
|
||||
)
|
||||
if (!opened.ok) {
|
||||
|
|
@ -878,10 +893,19 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
async function completeWithCard() {
|
||||
const card = loadedBoltCard.value
|
||||
if (!card) return
|
||||
const t0 = Date.now()
|
||||
let outcome: BoltCardOutcome = 'skipped'
|
||||
if (isCashOut.value) outcome = await handleBoltCardTap({ session: card })
|
||||
else if (isCashIn.value) outcome = await handleBoltCardReceive({ session: card })
|
||||
console.info(
|
||||
`[ATM] Complete with card: ${outcome} in ${Date.now() - t0} ms` +
|
||||
(outcome === 'accepted' ? '' : ` — ${nfcStatus.value?.message ?? 'no reason given'}`)
|
||||
)
|
||||
if (outcome === 'skipped') return
|
||||
// Blocked is the card server's standing answer for this visit: nothing was
|
||||
// consumed, and re-tapping cannot lift it. Keep the session loaded so the
|
||||
// chip still shows whose card it is, and skip the re-tap advice below.
|
||||
if (outcome === 'blocked') return
|
||||
loadedBoltCard.value = null
|
||||
if (outcome === 'declined') {
|
||||
const reason = nfcStatus.value?.message ?? 'Card declined'
|
||||
|
|
|
|||
|
|
@ -353,7 +353,20 @@ function formatFiat(cents: number): string {
|
|||
class="flex w-full max-w-md flex-col items-center gap-2 pt-2"
|
||||
>
|
||||
<CardChip class="w-full" />
|
||||
<!-- The card server withheld this session's withdraw step (e.g.
|
||||
the card's daily limit is spent). Nothing the customer does
|
||||
here can lift it, so say so rather than offering a button
|
||||
that always fails. The QR remains as the way to get paid. -->
|
||||
<p
|
||||
v-if="!atmStore.loadedBoltCard.withdraw"
|
||||
class="w-full text-center text-sm font-medium text-destructive lg:text-lg"
|
||||
>
|
||||
{{
|
||||
atmStore.loadedBoltCard.withdrawBlockedReason ?? 'This card cannot sell right now'
|
||||
}}
|
||||
</p>
|
||||
<Button
|
||||
v-else
|
||||
class="w-full bg-success text-success-foreground"
|
||||
size="kiosk-lg"
|
||||
:disabled="atmStore.boltCardProcessing"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue