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:
parent
99ae5ab3de
commit
473834a363
6 changed files with 61 additions and 47 deletions
|
|
@ -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' })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue