// Copyright: © 2026 TWWIM UG. All rights reserved. (www.twwim.com) import { Currency, VAT_RATES, isCurrency } from '@archer/domain'; /** * Currency-aware price breakdown for checkout-style displays. * * Mirrors `Price.applyTax()` from @archer/domain — single source of truth for * what VAT rate applies per currency (EUR 19%, USD 0%). Same math the backend * uses in `CreatePaymentIntentUseCase` and `PurchaseCommandsUseCase`, so the * displayed gross is byte-identical to what Stripe will actually charge. * * Inputs are major units (e.g. 79.00 EUR, 94.99 USD). Outputs are major-unit * numbers rounded to 2 dp, ready for display. */ export interface CheckoutBreakdown { net: number; vatRate: number; vat: number; gross: number; currency: Currency; currencySymbol: string; } const CURRENCY_SYMBOLS: Record = { [Currency.EUR]: '€', [Currency.USD]: '$', }; /** * Resolve a currency from a string the API gave us, defaulting to EUR when * absent or unrecognised. EUR-default preserves pre-multicurrency display * behaviour for legacy responses without a currency field. */ export function resolveCurrency(input: string | null | undefined): Currency { if (input && isCurrency(input)) return input as Currency; return Currency.EUR; } export function resolveCheckoutBreakdown(netMajor: number, currencyInput: string | null | undefined): CheckoutBreakdown { const currency = resolveCurrency(currencyInput); const vatRate = VAT_RATES[currency]; const net = round2(netMajor); const vat = round2(net * vatRate); const gross = round2(net + vat); return { net, vatRate, vat, gross, currency, currencySymbol: CURRENCY_SYMBOLS[currency] }; } /** Single-line price label, e.g. "€79.00" or "$94.99". */ export function formatPlanPrice(amountMajor: number, currencyInput: string | null | undefined): string { const currency = resolveCurrency(currencyInput); const symbol = CURRENCY_SYMBOLS[currency]; return `${symbol}${amountMajor.toFixed(2)}`; } function round2(n: number): number { return Math.round(n * 100) / 100; }