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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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