diff --git a/apps/machine/src/views/CashInView.vue b/apps/machine/src/views/CashInView.vue index 48c6d02..d907f15 100644 --- a/apps/machine/src/views/CashInView.vue +++ b/apps/machine/src/views/CashInView.vue @@ -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 + +
+ Available: + + {{ formatSats(context.availableBalance) }} sats + +
+
-
💵 ➡️ 🏧
-

Insert bills into the acceptor

+
+
💰
+

+ Maximum amount reached — press Done to continue +

+
+
+
💵 ➡️ 🏧
+

Insert bills into the acceptor

+
@@ -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 }} diff --git a/packages/state-machine/src/machine.ts b/packages/state-machine/src/machine.ts index 2e0d918..e3260a5 100644 --- a/packages/state-machine/src/machine.ts +++ b/packages/state-machine/src/machine.ts @@ -331,6 +331,14 @@ export function createATMMachine(services: Partial = {}) { canRetry: ({ context }) => context.retryCount < 3, hasUserNpub: ({ context }) => context.userNpub !== null, hasOfferRequest: ({ context }) => context.pendingOfferRequest !== null, + billWithinBalance: ({ context, event }) => { + if (event.type !== 'BILL_INSERTED') return false + if (context.availableBalance <= 0 || context.exchangeRate === 0) return true + const newFiatCents = context.fiatAmount + event.denomination * 100 + const grossSats = Math.floor((newFiatCents / 100) * context.exchangeRate) + const fee = Math.floor(grossSats * context.feePercent) + return grossSats - fee <= context.availableBalance + }, // Cash-out guards hasSelectedAmount: ({ context }) => context.cashOutSelection.length > 0, canAddDenomination: ({ context, event }) => { @@ -392,6 +400,7 @@ export function createATMMachine(services: Partial = {}) { insertingBills: { on: { BILL_INSERTED: { + guard: 'billWithinBalance', actions: ['addBill', 'calculateSats'], }, BILL_REJECTED: {