feat(machine): limit bill insertion to available balance

Add billWithinBalance guard to state machine that rejects bills
exceeding the ATM's available sats. UI disables denomination buttons
and shows informational message when limit is reached.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-16 17:21:45 -05:00
commit 16159334a4
2 changed files with 45 additions and 2 deletions

View file

@ -114,6 +114,23 @@ function skipReceipt() {
atmStore.skipReceipt()
}
// Check if a bill denomination can still be accepted without exceeding available balance
function canAcceptBill(denomination: number): boolean {
const ctx = context.value
if (!ctx) return false
if (ctx.availableBalance <= 0 || ctx.exchangeRate === 0) return true
const newFiatCents = ctx.fiatAmount + denomination * 100
const grossSats = Math.floor((newFiatCents / 100) * ctx.exchangeRate)
const fee = Math.floor(grossSats * ctx.feePercent)
return grossSats - fee <= ctx.availableBalance
}
const balanceLimitReached = computed(() => {
if (!context.value || context.value.availableBalance <= 0) return false
// Check if even the smallest denomination ($1) would exceed
return !canAcceptBill(1)
})
// Simulate bill insertion (for development)
function simulateBill(denomination: number) {
atmStore.insertBill(denomination)
@ -210,10 +227,26 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</div>
</div>
<!-- Available balance indicator -->
<div v-if="context && context.availableBalance > 0" class="flex justify-between text-sm">
<span class="text-muted-foreground">Available:</span>
<span :class="balanceLimitReached ? 'text-foreground' : 'text-muted-foreground'">
{{ formatSats(context.availableBalance) }} sats
</span>
</div>
<!-- Bill Acceptor Visual -->
<div class="rounded-lg border-2 border-dashed border-white/20 p-6 text-center">
<div class="mb-2 text-4xl">💵 ➡️ 🏧</div>
<p class="text-sm text-muted-foreground">Insert bills into the acceptor</p>
<div v-if="balanceLimitReached">
<div class="mb-2 text-4xl">💰</div>
<p class="text-sm text-muted-foreground">
Maximum amount reached — press Done to continue
</p>
</div>
<div v-else>
<div class="mb-2 text-4xl">💵 ➡️ 🏧</div>
<p class="text-sm text-muted-foreground">Insert bills into the acceptor</p>
</div>
</div>
<!-- Dev: Simulate bill insertion -->
@ -227,6 +260,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
variant="outline"
size="sm"
class="h-8"
:disabled="!canAcceptBill(denom)"
@click="simulateBill(denom)"
>
${{ denom }}