refactor: rename fiatAmount to fiatCents for clarity

The field was always stored in cents but the name was ambiguous.
Rename to fiatCents across state machine, store, and views.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-07 13:02:12 -05:00
commit 473834a363
6 changed files with 61 additions and 47 deletions

View file

@ -39,7 +39,7 @@ describe('ATM State Machine', () => {
actor.start()
const context = actor.getSnapshot().context
expect(context.fiatAmount).toBe(0)
expect(context.fiatCents).toBe(0)
expect(context.satsAmount).toBe(0)
expect(context.billsInserted).toEqual([])
expect(context.error).toBeNull()
@ -78,7 +78,7 @@ describe('ATM State Machine', () => {
const context = actor.getSnapshot().context
expect(context.billsInserted).toEqual([20, 10])
expect(context.fiatAmount).toBe(3000) // $30 in cents
expect(context.fiatCents).toBe(3000) // $30 in cents
})
it('should return to idle on CANCEL', async () => {
@ -141,14 +141,14 @@ describe('ATM State Machine', () => {
let state = actor.getSnapshot()
expect(state.context.cashOutSelection).toEqual([20, 20, 20])
expect(state.context.fiatAmount).toBe(6000) // $60 in cents
expect(state.context.fiatCents).toBe(6000) // $60 in cents
// Remove one
actor.send({ type: 'REMOVE_DENOMINATION', denomination: 20 })
state = actor.getSnapshot()
expect(state.context.cashOutSelection).toEqual([20, 20])
expect(state.context.fiatAmount).toBe(4000) // $40 in cents
expect(state.context.fiatCents).toBe(4000) // $40 in cents
})
it('should calculate sats amount from fiat with fee', async () => {
@ -164,8 +164,8 @@ describe('ATM State Machine', () => {
const state = actor.getSnapshot()
// $20 at 2500 sats/USD = 50,000 sats
// Plus 2% fee = 51,000 sats
expect(state.context.satsAmount).toBe(51000)
// Plus 7.77% cash-out fee = floor(50000 * 0.0777) = 3,885 → 53,885 sats
expect(state.context.satsAmount).toBe(53885)
})
it('should generate invoice after confirming amount', async () => {
@ -396,7 +396,7 @@ describe('ATM State Machine', () => {
actor.send({ type: 'RETRY' })
await new Promise((resolve) => setTimeout(resolve, 50))
// Should have retried and succeeded
// Rate fetch failed → retry goes to fetchingRate → succeeds → insertingBills
const state = actor.getSnapshot()
expect(state.value).toMatchObject({ cashIn: 'insertingBills' })
})

View file

@ -180,17 +180,16 @@ export function createATMMachine(
if (event.type !== 'BILL_INSERTED') return context.billsInserted
return [...context.billsInserted, event.denomination]
},
fiatAmount: ({ context, event }) => {
if (event.type !== 'BILL_INSERTED') return context.fiatAmount
// denomination is in dollars, fiatAmount is in cents
return context.fiatAmount + event.denomination * 100
fiatCents: ({ context, event }) => {
if (event.type !== 'BILL_INSERTED') return context.fiatCents
return context.fiatCents + event.denomination * 100
},
}),
calculateSats: assign({
satsAmount: ({ context }) => {
if (context.exchangeRate === 0) return 0
const fiatUnits = context.fiatAmount / 100 // cents to dollars
const grossSats = Math.floor(fiatUnits * context.exchangeRate)
const fiatDollars = context.fiatCents / 100
const grossSats = Math.floor(fiatDollars * context.exchangeRate)
const fee = Math.floor(grossSats * context.feePercent)
return grossSats - fee
},
@ -200,7 +199,7 @@ export function createATMMachine(
// Calculate bills to dispense for cash-out
// Simple algorithm: use largest denominations first
const denominations = [100, 50, 20, 10, 5, 1]
let remaining = context.fiatAmount / 100 // cents to dollars
let remaining = context.fiatCents / 100
const amounts: { denomination: number; count: number }[] = []
for (const denom of denominations) {
@ -278,7 +277,7 @@ export function createATMMachine(
},
}),
setAmount: assign({
fiatAmount: ({ event }) => {
fiatCents: ({ event }) => {
if (event.type !== 'SELECT_AMOUNT') return 0
return event.amount * 100 // dollars to cents
},
@ -296,9 +295,9 @@ export function createATMMachine(
if (event.type !== 'ADD_DENOMINATION') return context.cashOutSelection
return [...context.cashOutSelection, event.denomination]
},
fiatAmount: ({ context, event }) => {
if (event.type !== 'ADD_DENOMINATION') return context.fiatAmount
return context.fiatAmount + event.denomination * 100 // dollars to cents
fiatCents: ({ context, event }) => {
if (event.type !== 'ADD_DENOMINATION') return context.fiatCents
return context.fiatCents + event.denomination * 100
},
}),
removeDenomination: assign({
@ -310,21 +309,21 @@ export function createATMMachine(
newSelection.splice(idx, 1)
return newSelection
},
fiatAmount: ({ context, event }) => {
if (event.type !== 'REMOVE_DENOMINATION') return context.fiatAmount
if (!context.cashOutSelection.includes(event.denomination)) return context.fiatAmount
return context.fiatAmount - event.denomination * 100 // dollars to cents
fiatCents: ({ context, event }) => {
if (event.type !== 'REMOVE_DENOMINATION') return context.fiatCents
if (!context.cashOutSelection.includes(event.denomination)) return context.fiatCents
return context.fiatCents - event.denomination * 100
},
}),
clearCashOutSelection: assign({
cashOutSelection: () => [],
fiatAmount: () => 0,
fiatCents: () => 0,
satsAmount: () => 0,
}),
calculateSatsFromFiat: assign({
satsAmount: ({ context }) => {
if (context.exchangeRate === 0) return 0
const fiatDollars = context.fiatAmount / 100 // cents to dollars
const fiatDollars = context.fiatCents / 100
const grossSats = Math.floor(fiatDollars * context.exchangeRate)
// For cash-out, user pays the sats, so fee is added
const fee = Math.floor(grossSats * context.feePercent)
@ -362,7 +361,7 @@ export function createATMMachine(
},
guards: {
hasInsertedBills: ({ context }) => context.billsInserted.length > 0,
hasSufficientAmount: ({ context }) => context.fiatAmount >= 100, // $1 minimum
hasSufficientAmount: ({ context }) => context.fiatCents >= 100, // $1 minimum
hasExchangeRate: ({ context }) => context.exchangeRate > 0,
canRetry: ({ context }) => context.retryCount < 3,
hasUserNpub: ({ context }) => context.userNpub !== null,
@ -370,7 +369,7 @@ export function createATMMachine(
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 newFiatCents = context.fiatCents + event.denomination * 100
const grossSats = Math.floor((newFiatCents / 100) * context.exchangeRate)
const fee = Math.floor(grossSats * context.feePercent)
return grossSats - fee <= context.availableBalance
@ -543,11 +542,26 @@ export function createATMMachine(
},
error: {
on: {
RETRY: {
guard: 'canRetry',
target: 'generatingNdebit',
actions: 'incrementRetry',
},
RETRY: [
{
// Rate never loaded: retry from rate fetch
guard: ({ context }) => context.retryCount < 3 && context.exchangeRate === 0,
target: 'fetchingRate',
actions: 'incrementRetry',
},
{
// Ndebit failed before QR was shown: retry generation
guard: ({ context }) => context.retryCount < 3 && context.ndebitUri === null,
target: 'generatingNdebit',
actions: 'incrementRetry',
},
{
// Payment failed but QR/LNURL still valid: show QR again
guard: 'canRetry',
target: 'displayingQR',
actions: 'incrementRetry',
},
],
CANCEL: [
{
// No bills inserted: safe to cancel

View file

@ -34,7 +34,7 @@ export interface OfferRequestEvent {
export interface ATMContext {
// Transaction details
/** Fiat amount in cents */
fiatAmount: number
fiatCents: number
/** Satoshi amount */
satsAmount: number
/** Fiat currency code */
@ -141,7 +141,7 @@ export type ATMEvent =
/** Initial context values */
export const initialContext: ATMContext = {
fiatAmount: 0,
fiatCents: 0,
satsAmount: 0,
currency: 'USD',
exchangeRate: 0,