fix(access): instrument Complete, and say when a card can't sell #100
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
|
// 'declined' — presented and refused, or failed after presentation. The
|
||||||
// boltcards server bumps the SUN counter on the first GET, so
|
// boltcards server bumps the SUN counter on the first GET, so
|
||||||
// treat the voucher as spent even when the failure was ours.
|
// 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
|
// 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
|
// screen (raw lnurlw, spent by this call) or the session opened at entry
|
||||||
// (hit-keyed steps, no p/c).
|
// (hit-keyed steps, no p/c).
|
||||||
|
|
@ -715,6 +719,15 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
const invoice = context.value?.invoice
|
const invoice = context.value?.invoice
|
||||||
if (!invoice) return 'skipped'
|
if (!invoice) return 'skipped'
|
||||||
if (boltCardProcessing.value) return 'skipped' // one pull at a time
|
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
|
boltCardProcessing.value = true
|
||||||
nfcStatus.value = { state: 'processing', message: 'Reading card…' }
|
nfcStatus.value = { state: 'processing', message: 'Reading card…' }
|
||||||
try {
|
try {
|
||||||
|
|
@ -722,13 +735,12 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
const api = window.electronAPI!
|
const api = window.electronAPI!
|
||||||
const res =
|
const res =
|
||||||
'session' in source
|
'session' in source
|
||||||
? source.session.withdraw
|
? await api.withdrawWithSession({
|
||||||
? await api.withdrawWithSession({
|
// Non-null: a withheld step is pre-flighted above.
|
||||||
withdraw: plainWithdrawStep(source.session.withdraw),
|
withdraw: plainWithdrawStep(source.session.withdraw!),
|
||||||
bolt11: invoice,
|
bolt11: invoice,
|
||||||
amountMsat,
|
amountMsat,
|
||||||
})
|
})
|
||||||
: { ok: false, reason: source.session.withdrawBlockedReason ?? 'card cannot pay now' }
|
|
||||||
: await api.lnurlWithdraw({ lnurlw: source.lnurlw, bolt11: invoice, amountMsat })
|
: await api.lnurlWithdraw({ lnurlw: source.lnurlw, bolt11: invoice, amountMsat })
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
nfcStatus.value = { state: 'accepted', message: 'Card accepted — confirming payment…' }
|
nfcStatus.value = { state: 'accepted', message: 'Card accepted — confirming payment…' }
|
||||||
|
|
@ -819,7 +831,10 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
console.info(
|
console.info(
|
||||||
`[ATM] Card session ${opened.ok ? 'opened' : 'refused'} in ${Date.now() - t0} ms` +
|
`[ATM] Card session ${opened.ok ? 'opened' : 'refused'} in ${Date.now() - t0} ms` +
|
||||||
(opened.ok
|
(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) {
|
if (!opened.ok) {
|
||||||
|
|
@ -878,10 +893,19 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
async function completeWithCard() {
|
async function completeWithCard() {
|
||||||
const card = loadedBoltCard.value
|
const card = loadedBoltCard.value
|
||||||
if (!card) return
|
if (!card) return
|
||||||
|
const t0 = Date.now()
|
||||||
let outcome: BoltCardOutcome = 'skipped'
|
let outcome: BoltCardOutcome = 'skipped'
|
||||||
if (isCashOut.value) outcome = await handleBoltCardTap({ session: card })
|
if (isCashOut.value) outcome = await handleBoltCardTap({ session: card })
|
||||||
else if (isCashIn.value) outcome = await handleBoltCardReceive({ 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
|
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
|
loadedBoltCard.value = null
|
||||||
if (outcome === 'declined') {
|
if (outcome === 'declined') {
|
||||||
const reason = nfcStatus.value?.message ?? 'Card 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"
|
class="flex w-full max-w-md flex-col items-center gap-2 pt-2"
|
||||||
>
|
>
|
||||||
<CardChip class="w-full" />
|
<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
|
<Button
|
||||||
|
v-else
|
||||||
class="w-full bg-success text-success-foreground"
|
class="w-full bg-success text-success-foreground"
|
||||||
size="kiosk-lg"
|
size="kiosk-lg"
|
||||||
:disabled="atmStore.boltCardProcessing"
|
:disabled="atmStore.boltCardProcessing"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue