Why not just convert at checkout?
The naive approach converts a base price to the customer's currency at the moment of payment using the live rate. It feels correct and it's a trap: the displayed price can differ from the charged price if the rate ticks between page load and payment, refunds computed later use a different rate than the sale, and your invoices become impossible to reconcile because every one used a slightly different number. Cent-accuracy and auditability both break.
Sync rates on a schedule, persist the price
Instead, a cron pulls the ECB reference rates once per day and writes concrete per-currency prices into the database. Checkout reads a persisted price, not a live conversion. The rate a customer paid at is a stored fact, not a re-derivation — so the invoice, the refund and the report all agree.
// Daily cron: fetch ECB rates, persist derived prices
const rates = await fetchEcbRates(); // base EUR
for (const product of products) {
for (const cur of SUPPORTED) {
const amount = toMinorUnits(product.eur * rates[cur], cur);
await db.upsertPrice({ productId: product.id, currency: cur, amount, rateDate: rates.date });
}
}Cent-accuracy means integer minor units
Money never lives in a float. Prices are stored and computed in minor units (cents, and the right number of decimals per currency — some have zero). Rounding happens once, at the moment a price is derived from a rate, and then never again. Every downstream number is integer arithmetic on that stored amount.
Floating-point money is a latent bug. 0.1 + 0.2 is not 0.3, and across thousands of invoices those fractions become real reconciliation discrepancies. Store minor-unit integers; round exactly once.
Locale is data, not branches
18 locales and country-specific validity rules do not mean 18 code paths. Currency formatting, invoice localization and the country rules live in configuration keyed by locale/country, so adding a market is a data change, not a deploy full of new if-statements. The commerce engine stays the same; the config grows.
Attribution and analytics still work
Because prices are persisted and orders carry their currency and rate date, GA4 ecommerce events and lead-to-sale attribution report in a consistent way — the analytics layer isn't guessing what the customer actually paid.
Key takeaways
- Persist prices from scheduled rates; never convert at checkout.
- Store money as integer minor units and round exactly once.
- Keep locale and country rules in config so a new market is data, not code.
- A stored rate-date makes sales, refunds and reports reconcile to the cent.