AstroBaaS

Checkout & payments

Multi-currency checkout

Free — GPL coresize Lplanned, not built

Generated from docs/plan/core/multi-currency-checkout/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.

Currency is hardcoded to EUR in checkout, blocking cross-border trade. This core feature makes currency dynamic (configurable per shop, display in customer’s locale), enabling merchants to trade in multiple currencies.

The problem

I want to sell to customers in the US, Germany, and the UK, but checkout only shows EUR. Customers see unfamiliar currency and abandon. I need to display prices in USD, EUR, and GBP and convert totals accordingly.

What it does

  • Shop currency setting: merchant chooses base currency (EUR, USD, GBP, etc.); locked until all orders are cleared (migration concern)
  • Display currency: storefront shows prices in customer’s currency (via Geolocation or customer selection)
  • Exchange rates: fetch live rates from ECB/OANDA; cache for 24 hours to avoid rate-lookup latency
  • Frozen currency: order captures the exchange rate at checkout, used for that order’s refunds and invoicing
  • Checkout calculation: convert all prices to display currency for customer review; convert back to base for storage
  • Refunds: process in original currency (frozen at checkout), not current exchange rate
  • Multi-currency pricing: allow per-product prices in different currencies (advanced; core just freezes at order time)

What it deliberately does NOT do

Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.

  • Currency plate (currency converter widget) — that’s theme UX; module just provides the conversion data.
  • Per-region tax rules — tax is always calculated in base currency; regional tax compliance is separate.

Data model

Migration: Add to Order: currency (string, e.g., ‘EUR’, ‘USD’, ‘GBP’; frozen at checkout). Add Setting: shop_currency (default ‘EUR’). Add ExchangeRate collection (cache): { from_currency, to_currency, rate, fetched_at } for quick lookup.

API

  • GET /api/settings/shop-currency — fetch current base currency
  • GET /api/exchange-rates?from=EUR&to=USD — fetch live rate (or cached if < 24h old)
  • POST /api/orders/quote — checkout includes display_currency; totals show in that currency, frozen currency stored in response
  • POST /api/orders — checkout request includes display_currency; order is created with that currency frozen

Every route added here must also appear in src/pages/openapi.json.ts — a test fails the build if it does not.

Admin

Settings > General > Shop Currency dropdown (EUR, USD, GBP, etc.). Warning if currency has existing orders (migration risk). Exchange rates panel shows cached rates and last-update time; merchant can manually refresh.

The seam — why this is core

Core owns currency handling, exchange rate caching, and frozen-currency enforcement on orders. Storefront theme owns which currencies to offer and geolocation logic.

Base platform behavior. Single-currency hardcoding blocks cross-border trade.

Dependencies

  • structured-address-model — addresses have country, which may inform currency/tax decisions
  • payment-status-tracking — orders with foreign currency still track payment status

Acceptance checks

Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.

  • Shop currency set to EUR; merchant enables USD display; product priced €100 is shown as ~$110 (1.10 rate)
  • Customer in USD locale: totals shown in USD; checkout converts to EUR for storage and payment
  • Order created: currency=‘USD’, total_cents=11000 (stored in customer’s currency); frozen_rate=1.10
  • Refund issued: amount in USD (~$100 refund); converted to EUR (€90.91) and processed to gateway
  • Exchange rate older than 24h: system fetches new rate on next quote/checkout
  • Merchant changes shop currency from EUR to GBP: warning shown; migration flag set (existing orders stay in EUR)

Risks

Rate staleness: if rates are not refreshed, old rates are used, leading to customer confusion or loss/gain. Rate updates must be automated (hourly cron). Refund mismatch: if a refund is issued weeks later and exchange rate has moved significantly, customer sees a different amount than expected. Frozen rate solves this, but must be documented. Rounding: converting €100.50 to USD at 1.10 rate = $110.55; storing as cents (11055) then displaying as $110.55 is correct, but micro-rounding errors can accumulate across many orders. Use banker’s rounding or test the math thoroughly.

Commercial context

Suggested priceCore
Rival anchorMagento Open Source: base plus display currency per store view, free.

The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.


Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full catalogue and ../../AI-GUIDE.md for how to work on this repository.