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

@ -271,7 +271,7 @@ export const useAtmStore = defineStore('atm', () => {
persistTransaction({
txid: ctx.txid,
type: isCashInTx ? 'cash_in' : 'cash_out',
fiatCents: ctx.fiatAmount,
fiatCents: ctx.fiatCents,
sats: ctx.satsAmount,
bills,
})
@ -626,7 +626,7 @@ export const useAtmStore = defineStore('atm', () => {
}
// Calculate what the new sats amount would be
const newFiatCents = ctx.fiatAmount + denomination * 100
const newFiatCents = ctx.fiatCents + denomination * 100
const newFiatUnits = newFiatCents / 100
const grossSats = Math.floor(newFiatUnits * ctx.exchangeRate)
const fee = Math.floor(grossSats * ctx.feePercent)
@ -846,7 +846,7 @@ export const useAtmStore = defineStore('atm', () => {
}
// Calculate if this bill would exceed available balance
const newFiatCents = ctx.fiatAmount + denomination * 100
const newFiatCents = ctx.fiatCents + denomination * 100
const newFiatUnits = newFiatCents / 100
const grossSats = Math.floor(newFiatUnits * ctx.exchangeRate)
const fee = Math.floor(grossSats * ctx.feePercent)

View file

@ -119,7 +119,7 @@ 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 newFiatCents = ctx.fiatCents + denomination * 100
const grossSats = Math.floor((newFiatCents / 100) * ctx.exchangeRate)
const fee = Math.floor(grossSats * ctx.feePercent)
return grossSats - fee <= ctx.availableBalance
@ -214,7 +214,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<!-- Amount display -->
<div v-if="context">
<p class="text-3xl lg:text-[6vh] font-bold text-success leading-tight">
{{ atmStore.fiatSymbol }}{{ (context.fiatAmount / 100).toFixed(2) }}
{{ atmStore.fiatSymbol }}{{ (context.fiatCents / 100).toFixed(2) }}
</p>
<p class="text-xl lg:text-[3.5vh] font-semibold text-bitcoin">
{{ formatSats(context.satsAmount) }} sats
@ -322,7 +322,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
{{ formatSats(context?.satsAmount || 0) }} sats
</p>
<p class="text-lg lg:text-2xl text-muted-foreground">
{{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
{{ atmStore.fiatSymbol }}{{ ((context?.fiatCents || 0) / 100).toFixed(2) }}
</p>
<!-- Waiting indicator -->
@ -388,7 +388,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<h3 class="text-2xl lg:text-[3rem] font-bold text-destructive">Cash Already Accepted</h3>
<p class="max-w-lg text-center text-base lg:text-2xl text-muted-foreground">
Your {{ atmStore.fiatSymbol
}}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted and
}}{{ context ? (context.fiatCents / 100).toLocaleString() : '' }} has been accepted and
cannot be returned. Please scan the QR code to receive your Bitcoin.
</p>
<Button
@ -428,7 +428,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground">Inserted</span>
<span class="font-semibold"
>{{ atmStore.fiatSymbol }}{{ (context.fiatAmount / 100).toFixed(2) }}</span
>{{ atmStore.fiatSymbol }}{{ (context.fiatCents / 100).toFixed(2) }}</span
>
</div>
<div class="flex justify-between text-base lg:text-2xl">
@ -445,7 +445,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
>{{
formatSats(
Math.floor(
Math.floor((context.fiatAmount / 100) * context.exchangeRate) *
Math.floor((context.fiatCents / 100) * context.exchangeRate) *
context.feePercent
)
)

View file

@ -211,7 +211,7 @@ function formatFiat(cents: number): string {
<div class="text-center py-2 lg:py-4">
<p class="text-sm lg:text-xl text-muted-foreground">You will receive</p>
<p class="text-4xl lg:text-[7vh] font-bold text-success leading-tight">
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }}
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatCents) : '0.00' }}
</p>
<p class="mt-1 lg:mt-2 text-lg lg:text-2xl text-muted-foreground">
You pay:
@ -291,7 +291,7 @@ function formatFiat(cents: number): string {
<p class="text-lg lg:text-2xl text-muted-foreground">
to receive
<Badge class="bg-success text-success-foreground text-lg lg:text-2xl">
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }}
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatCents) : '0.00' }}
</Badge>
</p>
@ -363,7 +363,7 @@ function formatFiat(cents: number): string {
<p v-if="context" class="text-lg lg:text-2xl">
Please take your
<Badge class="bg-success text-success-foreground text-2xl"
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatCents) }}</Badge
>
</p>
<Button
@ -411,7 +411,7 @@ function formatFiat(cents: number): string {
formatSats(
Math.abs(
context.satsAmount -
Math.floor((context.fiatAmount / 100) * context.exchangeRate)
Math.floor((context.fiatCents / 100) * context.exchangeRate)
)
)
}}
@ -422,7 +422,7 @@ function formatFiat(cents: number): string {
<div class="flex justify-between text-xl lg:text-[1.8rem] font-bold">
<span class="text-success">Cash received</span>
<span class="text-success"
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</span
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatCents) }}</span
>
</div>
</div>

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,