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¶
- Store money as integer cents in the smallest unit (cents, paise, fils) — never float. This prevents precision loss in calculations.
- 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). - Exchange rates are versioned — every conversion records the
rate used and the date in an
exchange_ratestable. If a transaction happens at rate X and the rate changes to Y, the original rate is preserved for audit and reconciliation. - 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.
- Frontend uses
Intl.NumberFormatwith patient locale — never hardcoded symbols. AED patient sees "د.إ 22,000", INR patient sees "₹6,00,000", USD patient sees "$6,000.00". - 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.
- Tax is configurable per country/state via a
tax_configtable — 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 conversionPatient.preferred_currencyfield — stored in DB, extracted during intakeProviderProcedure.avg_cost_usd/min_cost_usd/max_cost_usd— Integer cents (correct type)match_service.pycallsconvert_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)