Multicurrency Infrastructure -- Feature Spec¶
Status: Implemented in Session 42
Companion steer: ai-steer/multicurrency-steer.md
This is the implementation plan for Tier 1 only (the foundation). Read the steer first -- it covers the problem statement, design principles, three-tier plan, key decisions, risks, and success criteria. Tiers 2 and 3 are outlined at the end of this document but are not detailed.
Overview¶
Tier 1 establishes the currency foundation: budget storage on cases, provider
base currency, cost normalization in the matching engine, an exchange rate audit
table, and a frontend formatCurrency() utility. No payment models, no tax
tables, no gateway integration.
Estimated effort: 3--4 sessions
Dependencies: None (builds on existing currency_service.py)
File-by-file change list¶
Database migrations¶
Migration 1: Case budget fields¶
Add two columns to the cases table.
ALTER TABLE cases ADD COLUMN budget_cents INTEGER;
ALTER TABLE cases ADD COLUMN budget_currency VARCHAR(3) DEFAULT 'USD';
budget_cents: patient's stated budget in smallest currency unit (nullable -- not every patient states a budget)budget_currency: ISO 4217 code, defaults to"USD", not nullable oncebudget_centsis set
Alembic file: alembic/versions/xxxx_add_case_budget_fields.py
Migration 2: Provider base currency¶
Add one column to the providers table.
base_currency: the currency in which the provider bills. Default"USD"for backward compatibility with all 42 existing providers.
Alembic file: alembic/versions/xxxx_add_provider_base_currency.py
Migration 3: Exchange rates table¶
Create the exchange_rates audit table.
CREATE TABLE exchange_rates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_currency VARCHAR(3) NOT NULL,
to_currency VARCHAR(3) NOT NULL,
rate DOUBLE PRECISION NOT NULL,
rate_date DATE NOT NULL,
source VARCHAR(50) NOT NULL DEFAULT 'frankfurter',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_exchange_rates_pair_date
ON exchange_rates (from_currency, to_currency, rate_date);
rate: stored to 6 decimal places (sufficient for all currency pairs)rate_date: the date the rate was published by the source (notcreated_at)source:"frankfurter"for ECB data,"pegged"for fixed-rate currencies,"manual"for overrides
Alembic file: alembic/versions/xxxx_create_exchange_rates_table.py
Migration 4: Procedure costs JSONB schema update¶
No DDL change -- procedure_costs remains a JSONB column on providers. The
schema of the JSON content evolves. A data migration script updates existing
rows.
Before (current):
After:
The legacy keys (min_usd_cents, max_usd_cents) are retained alongside the
new keys for backward compatibility during the transition. Once all consumers
are updated, the legacy keys are dropped in a follow-up migration.
Alembic file: alembic/versions/xxxx_migrate_procedure_costs_schema.py (data migration, not DDL)
Backend changes¶
app/models/case.py¶
| Change | Detail | Tier tag |
|---|---|---|
Add budget_cents field |
Mapped[int \| None] = mapped_column(Integer) |
Sonnet |
Add budget_currency field |
Mapped[str \| None] = mapped_column(String(3), default="USD") |
Sonnet |
Two new SQLAlchemy mapped columns. No logic changes -- just field definitions.
app/models/provider.py¶
| Change | Detail | Tier tag |
|---|---|---|
Add base_currency field |
Mapped[str] = mapped_column(String(3), default="USD", nullable=False) |
Sonnet |
| Add import | from sqlalchemy import ... String (already imported) |
Sonnet |
| Update docstring | Add base_currency to the class docstring |
Sonnet |
One new field. Existing cost_index and procedure_costs fields remain unchanged.
app/models/exchange_rate.py -- NEW¶
New SQLAlchemy model for the exchange_rates table.
class ExchangeRate(Base, UUIDPrimaryKeyMixin):
__tablename__ = "exchange_rates"
from_currency: Mapped[str] = mapped_column(String(3), nullable=False, index=True)
to_currency: Mapped[str] = mapped_column(String(3), nullable=False, index=True)
rate: Mapped[float] = mapped_column(Float, nullable=False)
rate_date: Mapped[date] = mapped_column(Date, nullable=False)
source: Mapped[str] = mapped_column(String(50), nullable=False, default="frankfurter")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
| Item | Tier tag |
|---|---|
| Model definition | Sonnet |
Composite index on (from_currency, to_currency, rate_date) |
Sonnet |
app/services/currency_service.py¶
| Change | Detail | Tier tag |
|---|---|---|
Add persist_rate() |
Saves a rate to the exchange_rates table. Called by the daily refresh job after fetching from Frankfurter. |
Sonnet |
Add get_historical_rate() |
Queries exchange_rates table for a specific pair + date. Used for audit reconstruction. |
Sonnet |
Modify _get_rates() |
After fetching from Frankfurter, call persist_rate() for each currency pair in the response. Best-effort -- DB failure does not block the in-memory cache update. |
Sonnet |
Add convert_with_audit() |
Same as convert() but also returns exchange_rate_id (the UUID of the stored rate row). Used by match_service when building responses. |
Opus |
Estimated new lines: ~60
app/services/matching_engine.py¶
| Change | Detail | Tier tag |
|---|---|---|
Modify MatchInput dataclass |
Add patient_budget_cents: int \| None = None, patient_currency: str = "USD" fields |
Sonnet |
Modify WeightedScoringV1._score_cost() |
Convert provider costs to patient currency using currency_service.convert() before scoring. If provider has procedure_costs with the needed procedure, use converted min_cents/max_cents averaged. Fall back to cost_index if no procedure-specific cost. |
Opus |
Modify GraphEnhancedWeightedV1._score_cost() |
Same conversion for graph-sourced cost_min_cents/cost_max_cents. Use provider's base_currency (or "USD" if graph data does not specify). |
Opus |
Add _budget_filter() method |
Pre-filter: remove providers whose converted minimum cost exceeds patient_budget_cents * 1.2 (20% tolerance). If no budget set, skip filter. |
Opus |
Modify score_providers() in both strategies |
Call _budget_filter() before scoring. Pass match_input.patient_currency to _score_cost(). |
Opus |
Modify _build_explanation() in both strategies |
Use converted cost range in explanation text. No hardcoded "$" -- format as "{min}--{max} {currency}" (frontend handles symbol). |
Sonnet |
Estimated modified lines: ~80
Key design note: The matching engine does not import the database session or
call persist_rate() directly. It calls currency_service.convert() which
returns the rate and rate_date. The match_service (orchestrator) is responsible
for persisting audit data.
app/services/match_service.py¶
| Change | Detail | Tier tag |
|---|---|---|
Pass patient budget to MatchInput |
Read case.budget_cents and case.budget_currency (if available) and set on MatchInput. |
Sonnet |
Pass patient_currency to MatchInput |
Already reads patient.preferred_currency at line 386. Now passes it to MatchInput constructor. |
Sonnet |
| Update response builder | Include rate_date from currency conversion in ScoredProvider response for transparency. |
Sonnet |
Estimated modified lines: ~15
app/services/chat_extractor.py¶
| Change | Detail | Tier tag |
|---|---|---|
| Ensure budget extraction returns structured data | The extractor already parses budget. Verify it returns {"budget_amount": 8000, "budget_currency": "USD"} or equivalent. If not, add budget parsing rules. |
Sonnet |
| Handle multi-currency budget expressions | "6 lakh rupees" -> {"budget_amount": 600000, "budget_currency": "INR"}. "8000 dollars" -> {"budget_amount": 800000, "budget_currency": "USD"} (in cents). "50,000 AED" -> {"budget_amount": 5000000, "budget_currency": "AED"}. |
Opus |
Estimated modified lines: ~20
config/prompts/base/chat_extractor_v2.yaml¶
| Change | Detail | Tier tag |
|---|---|---|
| Enhance budget extraction rules | Add examples for INR (lakh/crore notation), AED, EUR, GBP, TRY, THB. Instruct the LLM to always output budget_cents (integer, smallest unit) + budget_currency (ISO 4217). |
Sonnet |
| Add currency detection heuristics | "$" -> USD, "Rs" / "INR" / "lakh" -> INR, "AED" / "dirhams" -> AED, "pounds" / "GBP" -> GBP, "euros" / "EUR" -> EUR, "lira" / "TL" / "TRY" -> TRY, "baht" / "THB" -> THB. | Sonnet |
app/agents/case_orchestrator.py¶
| Change | Detail | Tier tag |
|---|---|---|
| Save extracted budget to case | After chat extractor returns budget data, update case.budget_cents and case.budget_currency via case_service. |
Sonnet |
| Infer currency from patient country | If patient says "8000" with no currency indicator, infer from patient.country_of_residence: USA -> USD, GBR -> GBP, ARE -> AED, IND -> INR, TUR -> TRY, THA -> THB, ESP -> EUR. Default: USD. |
Sonnet |
Estimated modified lines: ~25
Frontend changes¶
All frontend files are in the curaway-health-navigator repository.
src/utils/formatCurrency.ts -- NEW¶
New utility function using the browser's Intl.NumberFormat API.
/**
* Format an amount in cents to a locale-appropriate currency string.
*
* @param amountCents - Amount in smallest currency unit (cents/paise/fils)
* @param currencyCode - ISO 4217 currency code (e.g., "USD", "INR", "AED")
* @param locale - Optional BCP 47 locale (defaults to navigator.language)
* @returns Formatted string (e.g., "$6,000.00", "Rs 5,00,000.00", "AED 22,038.00")
*/
export function formatCurrency(
amountCents: number,
currencyCode: string = "USD",
locale?: string,
): string {
const amount = amountCents / 100;
const resolvedLocale = locale || navigator.language || "en-US";
return new Intl.NumberFormat(resolvedLocale, {
style: "currency",
currency: currencyCode,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
}
/**
* Format a cost range (min--max) in a locale-appropriate currency string.
*/
export function formatCostRange(
minCents: number,
maxCents: number,
currencyCode: string = "USD",
locale?: string,
): string {
const min = formatCurrency(minCents, currencyCode, locale);
const max = formatCurrency(maxCents, currencyCode, locale);
return `${min} -- ${max}`;
}
| Item | Tier tag |
|---|---|
formatCurrency() function |
Sonnet |
formatCostRange() function |
Sonnet |
| Unit tests (Vitest) | Sonnet |
src/components/MatchCard.tsx (or equivalent match result component)¶
| Change | Detail | Tier tag |
|---|---|---|
Import formatCurrency |
Replace any hardcoded $ or toLocaleString() calls with formatCurrency(). |
Sonnet |
Use display_currency from API response |
The API already returns display_currency on ScoredProvider. Pass it to formatCurrency(). |
Sonnet |
Format procedure_costs_converted |
If present, use min_converted / max_converted with the currency field. |
Sonnet |
src/pages/storefront/ProviderProfile.tsx¶
| Change | Detail | Tier tag |
|---|---|---|
Import formatCurrency |
Replace hardcoded cost displays with formatCurrency(). |
Sonnet |
Use provider's base_currency for local pricing |
Display both local currency and patient's preferred currency where available. | Sonnet |
src/pages/ConversationApp.tsx¶
| Change | Detail | Tier tag |
|---|---|---|
| Cost displays in match results rich card | Use formatCurrency() for any cost values rendered inline in the conversation. |
Sonnet |
Seed data updates¶
app/seed_data.py (or equivalent)¶
| Change | Detail | Tier tag |
|---|---|---|
Set base_currency for all 42 providers |
India providers: "INR", Turkey: "TRY", Thailand: "THB", Spain: "EUR", UAE: "AED", UK: "GBP", US: "USD". |
Sonnet |
Update procedure_costs JSONB |
Convert existing USD-based costs to local currency equivalents using current rates. Add currency key. Keep legacy min_usd_cents / max_usd_cents keys for backward compat. |
Sonnet |
Seed exchange_rates table |
Insert current rates for all corridor pairs (USD/INR, USD/AED, USD/GBP, USD/EUR, USD/TRY, USD/THB + reverse pairs). | Sonnet |
Tier 2 outline: Payment Infrastructure¶
Scope only -- detailed spec will be written separately when Tier 1 is deployed and validated.
New models¶
| Model | Key fields | Purpose |
|---|---|---|
Transaction |
id, case_id, patient_id, provider_id, amount_cents, currency, exchange_rate_id, gateway, gateway_ref, status, type (deposit/full/installment) |
Records every payment attempt |
Invoice |
id, transaction_id, provider_id, line_items (JSONB), subtotal_cents, tax_cents, total_cents, currency, status |
Provider-facing invoice |
ProviderPayout |
id, provider_id, invoice_id, amount_cents, currency, payout_method, status, settled_at |
Tracks disbursement to provider |
PlatformFee |
id, transaction_id, fee_cents, currency, fee_rate_bps, service_tier |
Platform commission per transaction |
Payment gateway service¶
PaymentGatewayabstract base class withcreate_charge(),capture(),refund(),get_status()StripeGatewayimplementation for USD, EUR, GBPRazorpayGatewayimplementation for INR- Gateway selection via
currency -> gatewaymapping inconfig/payment_gateways.yaml - Webhook handlers for async payment status updates (Stripe webhooks, Razorpay webhooks)
Provider portal¶
GET /api/v1/providers/{id}/invoices-- list invoicesGET /api/v1/providers/{id}/invoices/{invoice_id}-- invoice detailGET /api/v1/providers/{id}/payouts-- payout history
Financial events¶
All payment lifecycle events written to the events table: payment.initiated, payment.succeeded, payment.failed, payment.refunded, invoice.generated, payout.initiated, payout.settled.
Tier 3 outline: Compliance¶
Scope only -- detailed spec will be written separately.
Tax configuration¶
tax_configtable:country_code,state_code,tax_type(VAT/GST/sales_tax),rate_bps,effective_date,expires_at- Tax calculation at invoice generation (not at quote time)
- Seeded with: India 18% GST, UK 20% VAT (medical services may be exempt), UAE 5% VAT, Turkey 8-18% KDV, Thailand 7% VAT, Spain 21% IVA, US (state-by-state)
KYC/AML¶
- Integration with identity verification provider (Onfido or similar)
- Risk scoring per transaction (amount, corridor, patient history)
- Enhanced due diligence for transactions above corridor thresholds
Cross-border regulatory¶
- FEMA compliance for India (inward remittance documentation)
- FCA compliance for UK (payment services authorization)
- CBUAE compliance for UAE (payment processing authorization)
- Per-corridor document requirements stored in
corridor_complianceconfig
Reconciliation¶
- Daily reconciliation job comparing gateway records vs internal transactions
- Discrepancy alerting via events table + notification system
- Monthly settlement reports per provider
Test plan¶
Unit tests¶
| Test | What it validates | File | Tier tag |
|---|---|---|---|
test_convert_with_known_rates |
currency_service.convert() returns correct converted_cents for fixed test rates (mock Frankfurter) |
tests/test_currency_service.py |
Sonnet |
test_persist_rate |
persist_rate() writes a row to exchange_rates table with correct fields |
tests/test_currency_service.py |
Sonnet |
test_get_historical_rate |
get_historical_rate() retrieves the correct rate for a past date |
tests/test_currency_service.py |
Sonnet |
test_budget_filter_removes_expensive |
_budget_filter() removes providers whose min cost (converted) exceeds budget * 1.2 |
tests/test_matching_engine.py |
Opus |
test_budget_filter_skips_when_no_budget |
_budget_filter() passes all providers through when patient_budget_cents is None |
tests/test_matching_engine.py |
Sonnet |
test_score_cost_converts_currency |
_score_cost() converts provider cost from INR to USD (or vice versa) before scoring |
tests/test_matching_engine.py |
Opus |
test_score_cost_normalization_consistent |
Two providers with equivalent costs in different currencies get the same cost score | tests/test_matching_engine.py |
Opus |
test_chat_extractor_budget_usd |
Extracts {"budget_cents": 800000, "budget_currency": "USD"} from "my budget is $8,000" |
tests/test_chat_extractor.py |
Sonnet |
test_chat_extractor_budget_inr |
Extracts {"budget_cents": 60000000, "budget_currency": "INR"} from "6 lakh rupees" |
tests/test_chat_extractor.py |
Sonnet |
test_chat_extractor_budget_aed |
Extracts {"budget_cents": 5000000, "budget_currency": "AED"} from "50,000 AED" |
tests/test_chat_extractor.py |
Sonnet |
test_case_budget_persistence |
Orchestrator saves budget to case.budget_cents + case.budget_currency |
tests/test_case_orchestrator.py |
Sonnet |
test_exchange_rate_model |
ExchangeRate model creates/reads correctly with all fields |
tests/test_models.py |
Sonnet |
Integration tests¶
| Test | What it validates | File | Tier tag |
|---|---|---|---|
test_matching_multicurrency_providers |
End-to-end: patient (INR preferred) matched against providers in INR, USD, TRY. Cost scores are normalized. Provider ranking is currency-independent. | tests/integration/test_matching_multicurrency.py |
Opus |
test_budget_gate_filters_correctly |
Patient with INR 8,00,000 budget. Providers: one at INR 5,00,000 (passes), one at INR 12,00,000 (filtered), one at USD 15,000 (converted to INR, evaluated against budget). | tests/integration/test_matching_multicurrency.py |
Opus |
test_match_response_includes_conversion |
API response includes display_currency, procedure_costs_converted, and rate_date. |
tests/integration/test_match_api.py |
Sonnet |
Frontend tests¶
| Test | What it validates | File | Tier tag |
|---|---|---|---|
test_format_currency_usd |
formatCurrency(600000, "USD") returns "$6,000" |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_currency_inr |
formatCurrency(50000000, "INR") returns locale-appropriate INR string |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_currency_aed |
formatCurrency(2203800, "AED") returns locale-appropriate AED string |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_currency_eur |
formatCurrency(550000, "EUR") returns locale-appropriate EUR string |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_currency_gbp |
formatCurrency(470000, "GBP") returns locale-appropriate GBP string |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_currency_try |
formatCurrency(19200000, "TRY") returns locale-appropriate TRY string |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_currency_thb |
formatCurrency(21600000, "THB") returns locale-appropriate THB string |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
test_format_cost_range |
formatCostRange(600000, 1200000, "USD") returns "$6,000 -- $12,000" |
src/utils/__tests__/formatCurrency.test.ts |
Sonnet |
Edge Cases¶
| Edge Case | Scenario | Handling | Severity |
|---|---|---|---|
| Exchange rate API (Frankfurter) is down during matching | The daily cron job refreshes rates at midnight, but the cache has expired or rates were never fetched for a new currency pair. currency_service.convert() calls Frankfurter and gets a timeout or 5xx. The matching engine cannot score costs without conversion. |
currency_service.convert() already has an in-memory cache. Add a fallback chain: (1) in-memory cache (hot), (2) exchange_rates table (last persisted rate), (3) hardcoded stale rates from config/fallback_rates.yaml (refreshed manually each quarter). If all three fail, _score_cost() falls back to cost_index normalization (the v1 behavior) and logs a currency.fallback event. The match proceeds with degraded cost accuracy rather than failing entirely. |
High |
| Patient budget in an unsupported currency | Patient says "my budget is 500,000 Naira" (NGN). Frankfurter (ECB data) does not cover NGN. convert() returns an error for the NGN→USD pair. |
currency_service.convert() should return a clear UnsupportedCurrencyError (not a generic exception). The orchestrator catches this and asks the patient to restate their budget in USD, EUR, or one of the 6 supported corridor currencies. The budget is stored as-is (budget_cents=50000000, budget_currency=NGN) but _budget_filter() skips filtering when conversion fails (passes all providers through). Log a currency.unsupported event with the currency code for future coverage decisions. |
Medium |
Provider cost_index is 0 or negative |
Data quality issue — a provider's cost_index is set to 0 (division by zero in normalization) or negative (nonsensical). This could crash _score_cost() or produce inverted rankings. |
Add a guard in _score_cost(): if cost_index <= 0, treat the provider as having no cost data (skip cost scoring for that provider, redistribute weight to other factors via weight_redistribution.py). Log a data_quality.invalid_cost_index warning event with the provider ID. Add a seed data validation check that asserts all cost_index values are > 0. |
Medium |
| Budget specified without currency ("my budget is 50000") | The chat extractor parses budget_amount: 50000 but cannot determine budget_currency. No currency symbol, no keyword, no unit — just a bare number. |
Apply the country-based inference rule from Part 16 of this spec: use patient.country_of_residence to infer currency (USA→USD, IND→INR, ARE→AED, etc.). If country is also unknown, default to USD and add a follow-up question to the LLM prompt: "Just to confirm — is your budget of 50,000 in US dollars?" Store the inferred currency with a source: "inferred" flag so it can be corrected later. |
Medium |
| Exchange rate changes between quote generation and patient viewing | Patient triggers matching at 10am (USD/INR = 83.5). They view results at 6pm (USD/INR = 84.2). The displayed cost ranges are stale by the rate that was used at match time. | Store the exchange_rate_id (from convert_with_audit()) on the match result. Display the rate_date alongside converted costs in the frontend: "Costs converted at USD/INR 83.50 (April 10, 2026)". If the patient re-runs matching, fresh rates are used. Do not auto-refresh displayed results — that would change rankings without user action. Add a "Rates as of {date}" footnote to the MatchCard component. |
Low |
| Rounding errors across multiple conversions | Provider cost is in TRY. Patient currency is INR. The system converts TRY→USD→INR (two hops via the USD base). Each hop introduces rounding. Over a range (min→max), the accumulated error could be noticeable (e.g., displayed range off by hundreds of rupees). | Always convert directly between the pair when Frankfurter supports it (TRY→INR is available via ECB cross-rates). Only use USD as an intermediate when a direct rate is unavailable. All intermediate math uses Decimal (not float) with 6 decimal places. Final display rounds to the nearest whole currency unit (no sub-unit display for large amounts). Add a unit test that converts USD→INR→USD and asserts the round-trip error is < 0.01%. |
Medium |
Provider has no cost data at all (procedure_costs is null) |
A newly onboarded provider has no procedure_costs JSONB and cost_index is also null or 0. Should they appear in match results? |
Yes — they should still appear. _score_cost() returns a neutral score (0.5) when no cost data exists, and the cost weight is redistributed to other factors. The MatchCard displays "Cost information not yet available" instead of a range. _budget_filter() skips providers with no cost data (they pass through — we don't filter out a provider just because we don't know their price). This prevents new providers from being invisible until cost data is entered. |
Low |
Frontend Intl.NumberFormat with unsupported locale |
formatCurrency() uses navigator.language which could be a locale that Intl.NumberFormat doesn't fully support for the given currency (e.g., locale sw-TZ formatting AED). The browser may throw a RangeError or fall back to a generic format. |
Wrap the Intl.NumberFormat constructor in a try-catch. On failure, fall back to en-US locale with manual currency code append: "50,000 AED" instead of a locale-specific format. Add the fallback to formatCurrency.ts and add a Vitest case that passes an obscure locale + currency combination to verify graceful degradation. |
Low |
Implementation checklist¶
Tier 1 tasks with model tier tags¶
| # | Task | Tier | Est. lines | Depends on |
|---|---|---|---|---|
| 1 | Migration: add budget_cents + budget_currency to cases |
Sonnet | ~20 | -- |
| 2 | Migration: add base_currency to providers |
Sonnet | ~15 | -- |
| 3 | Migration: create exchange_rates table |
Sonnet | ~30 | -- |
| 4 | Migration: procedure_costs JSONB schema data migration |
Sonnet | ~50 | 2 |
| 5 | Model: Case -- add budget_cents, budget_currency fields |
Sonnet | ~5 | 1 |
| 6 | Model: Provider -- add base_currency field |
Sonnet | ~3 | 2 |
| 7 | Model: ExchangeRate -- new model |
Sonnet | ~25 | 3 |
| 8 | Service: currency_service.py -- add persist_rate(), get_historical_rate(), convert_with_audit() |
Sonnet | ~60 | 7 |
| 9 | Service: matching_engine.py -- modify MatchInput with budget/currency fields |
Sonnet | ~5 | -- |
| 10 | Service: matching_engine.py -- modify _score_cost() for currency conversion |
Opus | ~40 | 8, 9 |
| 11 | Service: matching_engine.py -- add _budget_filter() method |
Opus | ~25 | 9 |
| 12 | Service: matching_engine.py -- integrate budget filter into score_providers() |
Opus | ~15 | 10, 11 |
| 13 | Service: match_service.py -- pass budget + currency to MatchInput |
Sonnet | ~15 | 5, 9 |
| 14 | Service: chat_extractor.py -- ensure multi-currency budget extraction |
Sonnet | ~20 | -- |
| 15 | Config: chat_extractor_v2.yaml -- enhance budget extraction prompt |
Sonnet | ~30 | -- |
| 16 | Agent: case_orchestrator.py -- save budget to case |
Sonnet | ~25 | 5, 14 |
| 17 | Seed: set base_currency for all 42 providers |
Sonnet | ~50 | 6 |
| 18 | Seed: update procedure_costs JSONB with currency key |
Sonnet | ~80 | 4, 17 |
| 19 | Seed: insert current exchange rates into exchange_rates table |
Sonnet | ~40 | 7 |
| 20 | Frontend: formatCurrency.ts utility |
Sonnet | ~40 | -- |
| 21 | Frontend: match result components use formatCurrency() |
Sonnet | ~20 | 20 |
| 22 | Frontend: storefront pages use formatCurrency() |
Sonnet | ~15 | 20 |
| 23 | Frontend: conversation cost displays use formatCurrency() |
Sonnet | ~10 | 20 |
| 24 | Tests: currency_service unit tests (persist, historical, convert) |
Sonnet | ~80 | 8 |
| 25 | Tests: matching engine cost normalization + budget filter | Opus | ~100 | 10, 11, 12 |
| 26 | Tests: chat extractor budget extraction (USD, INR, AED) | Sonnet | ~60 | 14 |
| 27 | Tests: ExchangeRate model tests |
Sonnet | ~30 | 7 |
| 28 | Tests: integration -- multicurrency matching end-to-end | Opus | ~80 | 10, 11, 13 |
| 29 | Tests: frontend formatCurrency (7 currencies + range) |
Sonnet | ~50 | 20 |
Summary: 6 Opus tasks (matching engine cost normalization, budget filter, integration, exchange rate audit design), 23 Sonnet tasks (migrations, model fields, seed data, frontend utility, prompt config, unit tests).
References¶
| Document | Relevance |
|---|---|
ai-steer/multicurrency-steer.md |
Problem statement, design principles, key decisions, risks, success criteria |
app/services/currency_service.py |
Existing conversion service to be extended |
app/services/matching_engine.py |
_score_cost() methods to be modified for currency normalization |
app/services/match_service.py |
Orchestrator that passes patient currency to response builder |
app/models/case.py |
Case model -- budget fields to be added |
app/models/provider.py |
Provider model -- base_currency field to be added |
app/services/chat_extractor.py |
Budget extraction from conversation |
app/agents/case_orchestrator.py |
Saves extracted data to case |
dao-layer-feature.md |
DAO repositories for new models (ExchangeRate) |
| CLAUDE.md Section 22 | Microservices extraction plan -- payment service as Phase 2 candidate |
| CLAUDE.md Section 4.6 | Multicurrency architecture summary |