diff --git a/app/analysis/page.tsx b/app/analysis/page.tsx index e78e70b..f30a64f 100644 --- a/app/analysis/page.tsx +++ b/app/analysis/page.tsx @@ -20,7 +20,13 @@ import { Input } from '@/components/ui/input'; import { Panel } from '@/components/ui/panel'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { getCompanyAnalysis } from '@/lib/api'; -import { asNumber, formatCompactCurrency, formatCurrency, formatPercent } from '@/lib/format'; +import { + asNumber, + formatCurrency, + formatCurrencyByScale, + formatPercent, + type NumberScaleUnit +} from '@/lib/format'; import type { CompanyAnalysis } from '@/lib/types'; type FinancialPeriodFilter = 'quarterlyAndFiscalYearEnd' | 'quarterlyOnly' | 'fiscalYearEndOnly'; @@ -41,6 +47,12 @@ const FINANCIAL_PERIOD_FILTER_OPTIONS: Array<{ value: FinancialPeriodFilter; lab { value: 'fiscalYearEndOnly', label: 'Fiscal Year End only' } ]; +const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [ + { value: 'thousands', label: 'Thousands (K)' }, + { value: 'millions', label: 'Millions (M)' }, + { value: 'billions', label: 'Billions (B)' } +]; + function formatShortDate(value: string) { return format(new Date(value), 'MMM yyyy'); } @@ -75,6 +87,17 @@ function includesFinancialPeriod(filingType: '10-K' | '10-Q', filter: FinancialP return true; } +function asScaledFinancialCurrency( + value: number | null | undefined, + scale: NumberScaleUnit +) { + if (value === null || value === undefined) { + return 'n/a'; + } + + return formatCurrencyByScale(value, scale); +} + export default function AnalysisPage() { return ( Loading analysis desk...}> @@ -93,6 +116,7 @@ function AnalysisPageContent() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [financialPeriodFilter, setFinancialPeriodFilter] = useState('quarterlyAndFiscalYearEnd'); + const [financialValueScale, setFinancialValueScale] = useState('millions'); useEffect(() => { const fromQuery = searchParams.get('ticker'); @@ -163,6 +187,10 @@ function AnalysisPageContent() { return (analysis?.filings ?? []).filter((filing) => isFinancialSnapshotForm(filing.filing_type)); }, [analysis?.filings]); + const selectedFinancialScaleLabel = useMemo(() => { + return FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === financialValueScale)?.label ?? 'Millions (M)'; + }, [financialValueScale]); + if (isPending || !isAuthenticated) { return
Loading analysis desk...
; } @@ -262,20 +290,35 @@ function AnalysisPageContent() { - {FINANCIAL_PERIOD_FILTER_OPTIONS.map((option) => ( - - ))} +
+
+ {FINANCIAL_PERIOD_FILTER_OPTIONS.map((option) => ( + + ))} +
+
+ {FINANCIAL_VALUE_SCALE_OPTIONS.map((option) => ( + + ))} +
)} > @@ -303,11 +346,11 @@ function AnalysisPageContent() { {formatLongDate(point.filingDate)} {point.periodLabel} {point.filingType} - {point.revenue === null ? 'n/a' : formatCompactCurrency(point.revenue)} + {asScaledFinancialCurrency(point.revenue, financialValueScale)} = 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}> - {point.netIncome === null ? 'n/a' : formatCompactCurrency(point.netIncome)} + {asScaledFinancialCurrency(point.netIncome, financialValueScale)} - {point.assets === null ? 'n/a' : formatCompactCurrency(point.assets)} + {asScaledFinancialCurrency(point.assets, financialValueScale)} {point.netMargin === null ? 'n/a' : formatPercent(point.netMargin)} ))} @@ -318,7 +361,10 @@ function AnalysisPageContent() {
- + {loading ? (

Loading filings...

) : periodEndFilings.length === 0 ? ( @@ -343,9 +389,9 @@ function AnalysisPageContent() { {format(new Date(filing.filing_date), 'MMM dd, yyyy')} {filing.filing_type === '10-Q' ? 'Quarter End' : 'Fiscal Year End'} {filing.filing_type} - {filing.metrics?.revenue !== null && filing.metrics?.revenue !== undefined ? formatCompactCurrency(filing.metrics.revenue) : 'n/a'} - {filing.metrics?.netIncome !== null && filing.metrics?.netIncome !== undefined ? formatCompactCurrency(filing.metrics.netIncome) : 'n/a'} - {filing.metrics?.totalAssets !== null && filing.metrics?.totalAssets !== undefined ? formatCompactCurrency(filing.metrics.totalAssets) : 'n/a'} + {asScaledFinancialCurrency(filing.metrics?.revenue, financialValueScale)} + {asScaledFinancialCurrency(filing.metrics?.netIncome, financialValueScale)} + {asScaledFinancialCurrency(filing.metrics?.totalAssets, financialValueScale)} {filing.filing_url ? ( diff --git a/app/filings/page.tsx b/app/filings/page.tsx index f1ef90d..831b76f 100644 --- a/app/filings/page.tsx +++ b/app/filings/page.tsx @@ -15,7 +15,13 @@ import { useAuthGuard } from '@/hooks/use-auth-guard'; import { useTaskPoller } from '@/hooks/use-task-poller'; import { getTask, listFilings, queueFilingAnalysis, queueFilingSync } from '@/lib/api'; import type { Filing, Task } from '@/lib/types'; -import { formatCompactCurrency } from '@/lib/format'; +import { formatCurrencyByScale, type NumberScaleUnit } from '@/lib/format'; + +const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [ + { value: 'thousands', label: 'Thousands (K)' }, + { value: 'millions', label: 'Millions (M)' }, + { value: 'billions', label: 'Billions (B)' } +]; export default function FilingsPage() { return ( @@ -39,6 +45,17 @@ function hasFinancialSnapshot(filing: Filing) { return filing.filing_type === '10-K' || filing.filing_type === '10-Q'; } +function asScaledFinancialSnapshot( + value: number | null | undefined, + scale: NumberScaleUnit +) { + if (value === null || value === undefined) { + return 'n/a'; + } + + return formatCurrencyByScale(value, scale); +} + function resolveOriginalFilingUrl(filing: Filing) { if (filing.filing_url) { return filing.filing_url; @@ -93,6 +110,7 @@ function FilingsPageContent() { const [filterTickerInput, setFilterTickerInput] = useState(''); const [searchTicker, setSearchTicker] = useState(''); const [activeTask, setActiveTask] = useState(null); + const [financialValueScale, setFinancialValueScale] = useState('millions'); useEffect(() => { const ticker = searchParams.get('ticker'); @@ -168,6 +186,10 @@ function FilingsPageContent() { return counts; }, [filings]); + const selectedFinancialScaleLabel = useMemo(() => { + return FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === financialValueScale)?.label ?? 'Millions (M)'; + }, [financialValueScale]); + if (isPending || !isAuthenticated) { return
Opening filings stream...
; } @@ -248,7 +270,25 @@ function FilingsPageContent() {
- + + {FINANCIAL_VALUE_SCALE_OPTIONS.map((option) => ( + + ))} + + )} + > {error ?

{error}

: null} {loading ? (

Fetching filings...

@@ -282,7 +322,7 @@ function FilingsPageContent() {
Financial Snapshot
- {financialForm ? (revenue ? formatCompactCurrency(revenue) : 'n/a') : 'Qualitative filing'} + {financialForm ? asScaledFinancialSnapshot(revenue, financialValueScale) : 'Qualitative filing'}
@@ -351,7 +391,7 @@ function FilingsPageContent() { {filing.filing_type} {formatFilingDate(filing.filing_date)} - {financialForm ? (revenue ? formatCompactCurrency(revenue) : 'n/a') : 'Qualitative filing'} + {financialForm ? asScaledFinancialSnapshot(revenue, financialValueScale) : 'Qualitative filing'} {filing.company_name} {hasAnalysis ? 'Ready' : 'Not generated'} diff --git a/app/financials/page.tsx b/app/financials/page.tsx index e935bfc..7f88963 100644 --- a/app/financials/page.tsx +++ b/app/financials/page.tsx @@ -26,14 +26,21 @@ import { Input } from '@/components/ui/input'; import { Panel } from '@/components/ui/panel'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { getCompanyAnalysis } from '@/lib/api'; -import { formatCompactCurrency, formatCurrency, formatPercent } from '@/lib/format'; +import { + formatCurrencyByScale, + formatPercent, + type NumberScaleUnit +} from '@/lib/format'; import type { CompanyAnalysis } from '@/lib/types'; -type FinancialSeriesPoint = { +type StatementPeriodPoint = { filingDate: string; filingType: '10-K' | '10-Q'; - periodKind: 'quarterly' | 'fiscalYearEnd'; periodLabel: 'Quarter End' | 'Fiscal Year End'; +}; + +type FinancialSeriesPoint = StatementPeriodPoint & { + periodKind: 'quarterly' | 'fiscalYearEnd'; label: string; revenue: number | null; netIncome: number | null; @@ -44,14 +51,18 @@ type FinancialSeriesPoint = { debtToAssets: number | null; }; -type ChartPeriodFilter = 'quarterlyAndFiscalYearEnd' | 'quarterlyOnly' | 'fiscalYearEndOnly'; +type CompanyMetricPoint = StatementPeriodPoint & { + metrics: string[]; +}; -const AXIS_CURRENCY = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - notation: 'compact', - maximumFractionDigits: 1 -}); +type StatementMatrixRow = { + key: string; + label: string; + value: (point: TPoint, index: number, series: TPoint[]) => string; + valueClassName?: (point: TPoint, index: number, series: TPoint[]) => string | undefined; +}; + +type ChartPeriodFilter = 'quarterlyAndFiscalYearEnd' | 'quarterlyOnly' | 'fiscalYearEndOnly'; const CHART_PERIOD_FILTER_OPTIONS: Array<{ value: ChartPeriodFilter; label: string }> = [ { value: 'quarterlyAndFiscalYearEnd', label: 'Quarterly + Fiscal Year End' }, @@ -59,6 +70,12 @@ const CHART_PERIOD_FILTER_OPTIONS: Array<{ value: ChartPeriodFilter; label: stri { value: 'fiscalYearEndOnly', label: 'Fiscal Year End only' } ]; +const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [ + { value: 'thousands', label: 'Thousands (K)' }, + { value: 'millions', label: 'Millions (M)' }, + { value: 'billions', label: 'Billions (B)' } +]; + function formatShortDate(value: string) { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { @@ -85,14 +102,33 @@ function ratioPercent(numerator: number | null, denominator: number | null) { return (numerator / denominator) * 100; } -function asDisplayCurrency(value: number | null) { - return value === null ? 'n/a' : formatCurrency(value); +function asDisplayCurrency( + value: number | null, + scale: NumberScaleUnit +) { + return value === null ? 'n/a' : formatCurrencyByScale(value, scale); } function asDisplayPercent(value: number | null) { return value === null ? 'n/a' : formatPercent(value); } +function asDisplayMultiple(value: number | null) { + return value === null ? 'n/a' : `${value.toFixed(2)}x`; +} + +function asDisplaySignedCurrency( + value: number | null, + scale: NumberScaleUnit +) { + if (value === null) { + return 'n/a'; + } + + const formatted = formatCurrencyByScale(value, scale); + return value > 0 ? `+${formatted}` : formatted; +} + function normalizeTooltipValue(value: unknown) { if (Array.isArray(value)) { const [first] = value; @@ -106,7 +142,10 @@ function normalizeTooltipValue(value: unknown) { return null; } -function asTooltipCurrency(value: unknown) { +function asTooltipCurrency( + value: unknown, + scale: NumberScaleUnit +) { const normalized = normalizeTooltipValue(value); if (normalized === null) { return 'n/a'; @@ -117,7 +156,14 @@ function asTooltipCurrency(value: unknown) { return 'n/a'; } - return formatCompactCurrency(numeric); + return formatCurrencyByScale(numeric, scale); +} + +function asAxisCurrencyTick( + value: number, + scale: NumberScaleUnit +) { + return formatCurrencyByScale(value, scale, { maximumFractionDigits: 1 }); } function includesChartPeriod(point: FinancialSeriesPoint, filter: ChartPeriodFilter) { @@ -132,6 +178,193 @@ function includesChartPeriod(point: FinancialSeriesPoint, filter: ChartPeriodFil return true; } +function ratioMultiple(numerator: number | null, denominator: number | null) { + if (numerator === null || denominator === null || denominator === 0) { + return null; + } + + return numerator / denominator; +} + +function StatementMatrixTable({ + points, + rows +}: { + points: TPoint[]; + rows: Array>; +}) { + return ( +
+ + + + + {points.map((point, index) => ( + + ))} + + + + {rows.map((row) => ( + + + {points.map((point, index, series) => ( + + ))} + + ))} + +
Metric +
+ {formatLongDate(point.filingDate)} + {point.filingType} · {point.periodLabel} +
+
{row.label} + {row.value(point, index, series)} +
+
+ ); +} + +function buildOperatingStatementRows( + scale: NumberScaleUnit +): Array> { + return [ + { + key: 'revenue', + label: 'Revenue', + value: (point) => asDisplayCurrency(point.revenue, scale) + }, + { + key: 'net-income', + label: 'Net Income', + value: (point) => asDisplayCurrency(point.netIncome, scale), + valueClassName: (point) => { + if (point.netIncome === null) { + return undefined; + } + + return point.netIncome >= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'; + } + }, + { + key: 'net-margin', + label: 'Net Margin', + value: (point) => asDisplayPercent(point.netMargin) + } + ]; +} + +function buildBalanceSheetRows( + scale: NumberScaleUnit +): Array> { + return [ + { + key: 'total-assets', + label: 'Total Assets', + value: (point) => asDisplayCurrency(point.totalAssets, scale) + }, + { + key: 'cash', + label: 'Cash', + value: (point) => asDisplayCurrency(point.cash, scale) + }, + { + key: 'debt', + label: 'Debt', + value: (point) => asDisplayCurrency(point.debt, scale) + }, + { + key: 'debt-to-assets', + label: 'Debt / Assets', + value: (point) => asDisplayPercent(point.debtToAssets), + valueClassName: (point) => { + if (point.debtToAssets === null) { + return undefined; + } + + return point.debtToAssets <= 60 ? 'text-[#96f5bf]' : 'text-[#ffd08a]'; + } + } + ]; +} + +function buildCashFlowProxyRows( + scale: NumberScaleUnit +): Array> { + return [ + { + key: 'ending-cash', + label: 'Ending Cash Balance', + value: (point) => asDisplayCurrency(point.cash, scale) + }, + { + key: 'cash-change', + label: 'Cash Change vs Prior', + value: (point, index, series) => { + if (index === 0) { + return 'n/a'; + } + + const previous = series[index - 1]; + if (point.cash === null || previous.cash === null) { + return 'n/a'; + } + + return asDisplaySignedCurrency(point.cash - previous.cash, scale); + }, + valueClassName: (point, index, series) => { + if (index === 0) { + return undefined; + } + + const previous = series[index - 1]; + if (point.cash === null || previous.cash === null) { + return undefined; + } + + return point.cash - previous.cash >= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'; + } + }, + { + key: 'debt-change', + label: 'Debt Change vs Prior', + value: (point, index, series) => { + if (index === 0) { + return 'n/a'; + } + + const previous = series[index - 1]; + if (point.debt === null || previous.debt === null) { + return 'n/a'; + } + + return asDisplaySignedCurrency(point.debt - previous.debt, scale); + }, + valueClassName: (point, index, series) => { + if (index === 0) { + return undefined; + } + + const previous = series[index - 1]; + if (point.debt === null || previous.debt === null) { + return undefined; + } + + return point.debt - previous.debt <= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'; + } + }, + { + key: 'cash-to-debt', + label: 'Cash / Debt', + value: (point) => asDisplayMultiple(ratioMultiple(point.cash, point.debt)) + } + ]; +} + function isFinancialPeriodForm(filingType: CompanyAnalysis['financials'][number]['filingType']): filingType is '10-K' | '10-Q' { return filingType === '10-K' || filingType === '10-Q'; } @@ -154,6 +387,7 @@ function FinancialsPageContent() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [chartPeriodFilter, setChartPeriodFilter] = useState('quarterlyAndFiscalYearEnd'); + const [financialValueScale, setFinancialValueScale] = useState('millions'); useEffect(() => { const fromQuery = searchParams.get('ticker'); @@ -222,6 +456,10 @@ function FinancialsPageContent() { return CHART_PERIOD_FILTER_OPTIONS.find((option) => option.value === chartPeriodFilter)?.label ?? 'Quarterly + Fiscal Year End'; }, [chartPeriodFilter]); + const selectedFinancialScaleLabel = useMemo(() => { + return FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === financialValueScale)?.label ?? 'Millions (M)'; + }, [financialValueScale]); + const latestSnapshot = financialSeries[financialSeries.length - 1] ?? null; const liquidityRatio = useMemo(() => { @@ -253,6 +491,42 @@ function FinancialsPageContent() { }; }, [chartSeries]); + const companyMetricSeries = useMemo(() => { + return (analysis?.filings ?? []) + .filter((entry): entry is CompanyAnalysis['filings'][number] & { filing_type: '10-K' | '10-Q' } => { + return isFinancialPeriodForm(entry.filing_type); + }) + .slice() + .sort((a, b) => Date.parse(a.filing_date) - Date.parse(b.filing_date)) + .map((entry) => ({ + filingDate: entry.filing_date, + filingType: entry.filing_type, + periodLabel: entry.filing_type === '10-Q' ? 'Quarter End' : 'Fiscal Year End', + metrics: entry.analysis?.companyMetrics ?? [] + })); + }, [analysis?.filings]); + + const companyMetricRows = useMemo>>(() => { + const rowCount = companyMetricSeries.reduce((max, point) => Math.max(max, point.metrics.length), 0); + return Array.from({ length: rowCount }, (_, index) => ({ + key: `company-metric-${index + 1}`, + label: `Metric ${index + 1}`, + value: (point) => point.metrics[index] ?? 'n/a' + })); + }, [companyMetricSeries]); + + const operatingStatementRows = useMemo(() => { + return buildOperatingStatementRows(financialValueScale); + }, [financialValueScale]); + + const balanceSheetRows = useMemo(() => { + return buildBalanceSheetRows(financialValueScale); + }, [financialValueScale]); + + const cashFlowProxyRows = useMemo(() => { + return buildCashFlowProxyRows(financialValueScale); + }, [financialValueScale]); + if (isPending || !isAuthenticated) { return
Loading financial terminal...
; } @@ -312,25 +586,25 @@ function FinancialsPageContent() {
= 0} />
@@ -349,6 +623,21 @@ function FinancialsPageContent() {
+ +
+ {FINANCIAL_VALUE_SCALE_OPTIONS.map((option) => ( + + ))} +
+
+
{loading ? ( @@ -361,8 +650,8 @@ function FinancialsPageContent() { - AXIS_CURRENCY.format(value)} /> - asTooltipCurrency(value)} /> + asAxisCurrencyTick(value, financialValueScale)} /> + asTooltipCurrency(value, financialValueScale)} /> @@ -389,8 +678,8 @@ function FinancialsPageContent() { - AXIS_CURRENCY.format(value)} /> - asTooltipCurrency(value)} /> + asAxisCurrencyTick(value, financialValueScale)} /> + asTooltipCurrency(value, financialValueScale)} /> @@ -461,46 +750,49 @@ function FinancialsPageContent() {
- - {loading ? ( -

Loading table...

- ) : financialSeries.length === 0 ? ( -

No financial rows are available for this ticker yet.

- ) : ( -
- - - - - - - - - - - - - - - - {financialSeries.map((point, index) => ( - - - - - - - - - - - - ))} - -
FiledPeriodFormRevenueNet IncomeTotal AssetsCashDebtNet Margin
{formatLongDate(point.filingDate)}{point.periodLabel}{point.filingType}{asDisplayCurrency(point.revenue)}= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}>{asDisplayCurrency(point.netIncome)}{asDisplayCurrency(point.totalAssets)}{asDisplayCurrency(point.cash)}{asDisplayCurrency(point.debt)}{asDisplayPercent(point.netMargin)}
-
- )} -
+
+ + {loading ? ( +

Loading operating statement...

+ ) : financialSeries.length === 0 ? ( +

No operating statement rows are available for this ticker yet.

+ ) : ( + + )} +
+ + + {loading ? ( +

Loading balance sheet...

+ ) : financialSeries.length === 0 ? ( +

No balance sheet rows are available for this ticker yet.

+ ) : ( + + )} +
+ + + {loading ? ( +

Loading cash flow statement...

+ ) : financialSeries.length === 0 ? ( +

No cash flow statement rows are available for this ticker yet.

+ ) : ( + + )} +
+ + + {loading ? ( +

Loading company metrics...

+ ) : companyMetricSeries.length === 0 ? ( +

No filing periods are available for company metrics yet.

+ ) : companyMetricRows.length === 0 ? ( +

No company-specific KPI metrics were extracted for the selected filings.

+ ) : ( + + )} +
+
diff --git a/lib/format.test.ts b/lib/format.test.ts index 049ccfc..8326d8b 100644 --- a/lib/format.test.ts +++ b/lib/format.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import { formatScaledNumber } from './format'; +import { formatCurrencyByScale, formatScaledNumber } from './format'; describe('formatScaledNumber', () => { it('keeps values below one thousand unscaled', () => { @@ -27,3 +27,21 @@ describe('formatScaledNumber', () => { expect(formatScaledNumber(999_950)).toBe('1M'); }); }); + +describe('formatCurrencyByScale', () => { + it('formats values in thousands', () => { + expect(formatCurrencyByScale(12_345, 'thousands')).toBe('$12.3K'); + }); + + it('formats values in millions', () => { + expect(formatCurrencyByScale(12_345_678, 'millions')).toBe('$12.3M'); + }); + + it('formats values in billions', () => { + expect(formatCurrencyByScale(12_345_678_901, 'billions')).toBe('$12.3B'); + }); + + it('keeps sign for negative values', () => { + expect(formatCurrencyByScale(-2_500_000, 'millions')).toBe('-$2.5M'); + }); +}); diff --git a/lib/format.ts b/lib/format.ts index bd7d28d..4e71180 100644 --- a/lib/format.ts +++ b/lib/format.ts @@ -12,6 +12,8 @@ type NumberScale = { suffix: string; }; +export type NumberScaleUnit = 'thousands' | 'millions' | 'billions'; + const NUMBER_SCALES: NumberScale[] = [ { divisor: 1, suffix: '' }, { divisor: 1_000, suffix: 'K' }, @@ -19,11 +21,22 @@ const NUMBER_SCALES: NumberScale[] = [ { divisor: 1_000_000_000, suffix: 'B' } ]; +const NUMBER_SCALE_UNITS: Record = { + thousands: { divisor: 1_000, suffix: 'K' }, + millions: { divisor: 1_000_000, suffix: 'M' }, + billions: { divisor: 1_000_000_000, suffix: 'B' } +}; + type FormatScaledNumberOptions = { minimumFractionDigits?: number; maximumFractionDigits?: number; }; +type FormatScaledCurrencyOptions = { + minimumFractionDigits?: number; + maximumFractionDigits?: number; +}; + export function formatScaledNumber( value: string | number | null | undefined, options: FormatScaledNumberOptions = {} @@ -64,6 +77,28 @@ export function formatScaledNumber( return `${formatted}${NUMBER_SCALES[scaleIndex].suffix}`; } +export function formatCurrencyByScale( + value: string | number | null | undefined, + scale: NumberScaleUnit, + options: FormatScaledCurrencyOptions = {} +) { + const { + minimumFractionDigits = 0, + maximumFractionDigits = 1 + } = options; + const { divisor, suffix } = NUMBER_SCALE_UNITS[scale]; + const scaled = asNumber(value) / divisor; + + const formatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits, + maximumFractionDigits + }).format(scaled); + + return `${formatted}${suffix}`; +} + export function formatCurrency(value: string | number | null | undefined) { return new Intl.NumberFormat('en-US', { style: 'currency',