Skip to content

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 — handles payment_intent.succeeded, payment_intent.payment_failed
  • Razorpay: POST /api/v1/webhooks/razorpay — handles payment.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:

  1. enable_recording=false is set on every POST /rooms API call
  2. The round-trip config.enable_recording field is asserted false in the room creation response — if Daily.co ever returns true, the room is immediately deleted and the booking fails
  3. tests/test_video_room_service.py verifies 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 → omitted
  • user_id → omitted (not required for Daily.co room join)
  • Only room_name, exp (expiry), and is_owner are 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:

  • statuspending_payment, paid, in_progress, completed, cancelled, refunded
  • tenant_id — limit to a specific tenant
  • date_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 filters
  • POST /api/v1/admin/mso-doctors — onboard a new MSO doctor
  • PATCH /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