fix(state-machine): reject bills when available balance is unknown

Previously, if getAvailableBalance returned 0 or exchange rate was
missing, all bills were accepted — risking cash-in exceeding the
ATM's sats balance. Now rejects bills in that case to protect
customers from losing cash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-04-02 12:54:01 -04:00
commit 454154e528
2 changed files with 4 additions and 1 deletions

View file

@ -15,6 +15,7 @@ describe('ATM State Machine', () => {
dispensed: true,
} satisfies DispenseCashResult),
getExchangeRate: vi.fn().mockResolvedValue(2500), // 2500 sats per USD
getAvailableBalance: vi.fn().mockResolvedValue(1_000_000), // 1M sats available
// noffer cash-out services (legacy)
generateNoffer: vi.fn().mockResolvedValue('noffer1atmtest'),
sendOfferResponse: vi.fn().mockResolvedValue(undefined),

View file

@ -368,7 +368,9 @@ export function createATMMachine(
hasOfferRequest: ({ context }) => context.pendingOfferRequest !== null,
billWithinBalance: ({ context, event }) => {
if (event.type !== 'BILL_INSERTED') return false
if (context.availableBalance <= 0 || context.exchangeRate === 0) return true
// Reject bills if balance or rate is unknown — don't risk accepting
// cash we can't cover with sats
if (context.availableBalance <= 0 || context.exchangeRate === 0) return false
const newFiatCents = context.fiatCents + event.denomination * 100
const grossSats = Math.floor((newFiatCents / 100) * context.exchangeRate)
const fee = Math.floor(grossSats * context.feePercent)