MSO Teleconsultation Architecture¶
Overview¶
The MSO (Medical Second Opinion) teleconsultation flow allows patients to consult with a specialist doctor via video before committing to medical travel. It spans three subsystems: payment routing (Stripe or Razorpay depending on geography), video room provisioning (Daily.co), and admin oversight (session management, recording enforcement, PII filtering).
Implemented in Phase 2a (ADR-0018 §G). Video provider decision: ADR-0025.
Flow Overview¶
sequenceDiagram
participant Patient
participant API as FastAPI Backend
participant PG as PostgreSQL
participant PRouter as Payment Router
participant Stripe
participant Razorpay
participant Daily as Daily.co
participant QStash
Patient->>API: POST /api/v1/mso/bookings
API->>PG: Create MSOSession (status=pending_payment)
API->>PRouter: pick_payment_provider(currency, tenant, country)
PRouter-->>API: "stripe" | "razorpay"
alt Stripe path
API->>Stripe: Create PaymentIntent
Stripe-->>API: client_secret
API-->>Patient: { provider: "stripe", client_secret }
Patient->>Stripe: Confirm payment (Elements)
Stripe->>API: Webhook: payment_intent.succeeded
else Razorpay path
API->>Razorpay: Create Order
Razorpay-->>API: order_id
API-->>Patient: { provider: "razorpay", order_id }
Patient->>Razorpay: Confirm payment (Checkout)
Razorpay->>API: Webhook: payment.captured
end
API->>PG: Update MSOSession (status=paid)
API->>Daily: POST /rooms (enable_recording=false)
Daily-->>API: room_url, token
API->>PG: Store room_url (PII-stripped)
API->>QStash: Enqueue: send_confirmation_emails
API-->>Patient: { room_url, join_token, session_id }
Payment Routing¶
Payment provider selection is deterministic and operator-transparent. The routing function app/services/payments/routing.py::pick_payment_provider applies rules in priority order:
| Priority | Rule | Provider |
|---|---|---|
| 1 | MSOSession.payment_provider_override is set (admin-forced) |
Forced value |
| 2 | tenant_settings.payments_provider is non-null |
Tenant setting |
| 3 | Patient preferred_currency == 'INR' or country_of_residence == 'IND' |
Razorpay |
| 4 | Tenant data_residency_region starts with ap-south |
Razorpay |
| 5 | Default | Stripe |
Patient transparency gate: Patients never see the provider name (Stripe / Razorpay). The frontend receives a provider field but does not display it — only the payment UI widget changes.
Supported currencies¶
| Provider | Currencies |
|---|---|
| Stripe | USD, GBP, EUR, AED, and all other non-INR |
| Razorpay | INR only |
All monetary values are stored in smallest currency unit (paise for INR, cents for USD/GBP/EUR). See ADR-0017.
Webhook handling¶
Both providers deliver payment confirmation asynchronously via webhooks:
- Stripe:
POST /api/v1/webhooks/stripe— handlespayment_intent.succeeded,payment_intent.payment_failed - Razorpay:
POST /api/v1/webhooks/razorpay— handlespayment.captured,payment.failed
Webhook handlers update MSOSession.status and trigger the Daily.co room provisioning step. Idempotency is enforced by checking MSOSession.payment_reference before processing.
Video Room Provisioning¶
Video is provided by Daily.co per ADR-0025. The integration is in app/services/video_room_service.py.
Room lifecycle¶
| Event | Action |
|---|---|
| Payment confirmed | POST /v1/rooms — room created with enable_recording=false, 2-hour expiry |
| Session start | Doctor + patient each receive a short-lived participant token |
| Session end / expiry | Daily.co auto-deletes the room; MSOSession.status set to completed |
| Session cancelled | DELETE /v1/rooms/{room_name} called immediately |
Recording prohibition¶
ADR-0025 commits to "no recording, ever" for clinical-legal reasons. Three layers enforce this:
enable_recording=falseis set on everyPOST /roomsAPI call- The round-trip
config.enable_recordingfield is assertedfalsein the room creation response — if Daily.co ever returnstrue, the room is immediately deleted and the booking fails tests/test_video_room_service.pyverifies the assertion guard runs on every response
PII filtering¶
The raw Daily.co room URL (https://curaway.daily.co/<room-name>) contains no PII. However, participant tokens encode the user identity. app/services/video_room_pii_filter.py strips any identity-bearing fields before the token is stored in Postgres:
user_name→ omitteduser_id→ omitted (not required for Daily.co room join)- Only
room_name,exp(expiry), andis_ownerare retained in the stored token metadata
Admin Oversight¶
MSO Session management¶
Admins can view all MSO sessions via GET /api/v1/admin/mso/sessions. The endpoint supports filtering by:
status—pending_payment,paid,in_progress,completed,cancelled,refundedtenant_id— limit to a specific tenantdate_range— ISO 8601 date range
Admins can trigger a refund via POST /api/v1/admin/mso/sessions/{id}/refund, which calls the appropriate payment provider API and updates MSOSession.status to refunded.
MSO Doctor management¶
MSO doctors are managed separately from provider doctors:
GET /api/v1/admin/mso-doctors— list with availability and language filtersPOST /api/v1/admin/mso-doctors— onboard a new MSO doctorPATCH /api/v1/admin/mso-doctors/{id}— update availability schedule
MSO doctors are not projected to Neo4j — they operate outside the matching engine and are booked directly.
Data Model¶
Key tables:
| Table | Purpose |
|---|---|
mso_sessions |
One row per teleconsultation booking. Tracks payment status, room URL, provider. |
mso_doctors |
MSO specialist doctors with availability schedules. |
mso_doctor_bookings |
Join between mso_sessions and mso_doctors. |
All tables carry tenant_id and are subject to RLS (Row-Level Security) enforcement. GDPR erasure cascade covers mso_sessions — see GDPR Erasure Runbook.
Operational Runbooks¶
- Payments operations — webhook verification, Stripe/Razorpay key rotation, refund procedures
- Video operations — Daily.co key rotation, recording audit, room cleanup
Related¶
- ADR-0018 — Multi-Tenancy Platform §G
- ADR-0025 — MSO Video Provider: Daily.co
- ADR-0017 — Multicurrency Architecture
- Integrations — Stripe, Razorpay, Daily.co environment variables