Skip to content

ADR-0017: Multicurrency Architecture

Status: Accepted — Not Yet Implemented Date: 2026-04-10 Session: 35

Context

Curaway operates across 7 corridors (US, UK, UAE, India, Turkey, Thailand, Spain) but all costs are locked to USD. Provider cost_index is a relative multiplier (0.1–3.0) without currency context — an Indian provider at 1.0 and a Turkish provider at 1.0 have very different absolute costs. Patient budget is extracted during conversation but never persisted. The frontend hardcodes "$" on all monetary values.

The platform needs multicurrency support for: 1. Patient conversations — budget in local currency 2. Provider pricing — hospital quotes in local currency 3. Cost comparisons — normalize to common currency for matching 4. Future payments — service opt-in fees, provider commissions, invoices, payouts in local currency

A gap analysis found 26 gaps across the stack. Overall readiness: 25%.

Decision

Design principles

  1. Store money as integer cents in the smallest unit (cents, paise, fils) — never float. This prevents precision loss in calculations.
  2. Every monetary value has a currency code (ISO 4217) alongside it — no implied currency. If a field stores 600000, the currency column says "USD" (= $6,000.00) or "INR" (= ₹6,000.00).
  3. Exchange rates are versioned — every conversion records the rate used and the date in an exchange_rates table. If a transaction happens at rate X and the rate changes to Y, the original rate is preserved for audit and reconciliation.
  4. Conversion at scoring time — the matching engine converts provider costs to patient currency before scoring, not just at display time. This enables budget filtering and accurate affordability ranking.
  5. Frontend uses Intl.NumberFormat with patient locale — never hardcoded symbols. AED patient sees "د.إ 22,000", INR patient sees "₹6,00,000", USD patient sees "$6,000.00".
  6. Payment models are corridor-aware — different gateways per region (Stripe for USD/EUR/GBP, Razorpay for INR). Adapter pattern enables adding gateways without changing business logic.
  7. Tax is configurable per country/state via a tax_config table — not hardcoded rates. Different corridors have different tax treatments (India IGST, Spain VAT, UK VAT post-Brexit).

Three implementation tiers

Tier 1 (Foundation): Budget storage on Case model, provider base_currency field, cost normalization in matching engine, exchange rate audit table, frontend formatCurrency() utility. No payment infrastructure yet.

Tier 2 (Payments): Transaction, Invoice, ProviderPayout, PlatformFee models. Payment gateway adapter (Stripe/Razorpay). Commission tracking. Financial event audit trail.

Tier 3 (Compliance): Tax/VAT config per country. KYC/AML per corridor. Cross-border regulations (FATCA, PSD2, RBI). Automated reconciliation.

Key schema changes (Tier 1)

ALTER TABLE cases ADD COLUMN budget_cents INT;
ALTER TABLE cases ADD COLUMN budget_currency VARCHAR(3) DEFAULT 'USD';

ALTER TABLE providers ADD COLUMN base_currency VARCHAR(3) DEFAULT 'USD';

CREATE TABLE exchange_rates (
  id VARCHAR(36) PRIMARY KEY,
  from_currency VARCHAR(3) NOT NULL,
  to_currency VARCHAR(3) NOT NULL,
  rate NUMERIC(12,6) NOT NULL,
  rate_date DATE NOT NULL,
  source VARCHAR(50) DEFAULT 'frankfurter',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Matching engine change

# Before: relative cost_index comparison (meaningless across currencies)
score = 1.0 - ((cost_index - 0.1) / 2.9)

# After: convert to patient currency, compare against budget
provider_cost = convert(provider_min_cents, provider.base_currency, patient_currency)
if patient_budget_cents > 0 and provider_cost > patient_budget_cents:
    return 0.0  # Excluded — above budget
score = 1.0 / (1.0 + (provider_cost / patient_budget_cents))

Consequences

Positive: - Patient sees costs in their currency throughout the flow - Matching engine filters by budget correctly - Exchange rate is auditable for every conversion - Payment infrastructure has a clean foundation - Provider onboarding in non-USD markets becomes possible - Natural microservices extraction candidate (payment domain)

Negative: - Every existing cost display needs migration (frontend) - Provider seed data needs base_currency assignment - Matching engine cost scoring changes behavior — needs careful testing with real patient data - Exchange rate API adds a dependency (Frankfurter — already exists and has 9 pegged rate fallbacks)

Neutral: - Currency service already exists and works — this decision extends it, doesn't replace it - No impact on clinical pipeline (FHIR doesn't embed costs)

What exists already (not changing)

  • app/services/currency_service.py — Frankfurter API + 9 pegged Middle Eastern/Asian rates, 24hr cache, cents-based conversion
  • Patient.preferred_currency field — stored in DB, extracted during intake
  • ProviderProcedure.avg_cost_usd / min_cost_usd / max_cost_usd — Integer cents (correct type)
  • match_service.py calls convert_procedure_costs() at response time — already works, just needs to move to scoring time

Implementation

  • Full spec: docs/specs/ai-steer/multicurrency-steer.md + docs/specs/multicurrency-feature.md
  • Tier 1: ~2-3 weeks (foundation, no payments)
  • Tier 2: ~3-4 weeks (payment models + gateway)
  • Tier 3: ~2-3 weeks (compliance)
  • Opus/Sonnet tier tags on implementation checklist

References

  • Multicurrency gap analysis (Session 35 — 26 gaps found)
  • Currency service: app/services/currency_service.py
  • Microservices readiness: docs/architecture/20-microservices-readiness.md (payment as natural extraction candidate)
  • DAO spec: docs/specs/ai-steer/dao-layer-steer.md (repositories for new payment models)