openapi: 3.1.0
info:
  title: Global Clinic API
  version: 1.0.0
  description: >
    Production-ready REST API for Global Clinic, a cross-border medical-tourism
    platform. The current application uses mock data; this specification defines
    the production APIs that support every business workflow. Types conform to
    lib/schema.ts. Money is an integer minor unit plus an ISO-4217 currency.
    This document is a design contract, not a description of current runtime
    behaviour.
  contact:
    name: Global Clinic Engineering
    url: https://globalclinic.app
  license:
    name: Proprietary
servers:
  - url: https://api.globalclinic.app/v1
    description: Production
  - url: https://api.staging.globalclinic.app/v1
    description: Staging
tags:
  - name: Auth
  - name: Patients
  - name: Clinics
  - name: Providers
  - name: Catalogue
  - name: Documents
  - name: Consultations
  - name: Appointments
  - name: Visa
  - name: Travel
  - name: Hotels
  - name: Payments
  - name: Notifications
  - name: Reviews
  - name: Messaging
  - name: Admin
  - name: Reports

security:
  - bearerAuth: []

paths:
  /auth/register:
    post:
      tags: [Auth]
      summary: Create an account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, role]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                role: { type: string, enum: [patient, clinic] }
                name: { type: string }
      responses:
        "201":
          description: Created; verification email sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId: { type: string }
                  verificationRequired: { type: boolean }
        "400": { $ref: "#/components/responses/ValidationError" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/login:
    post:
      tags: [Auth]
      summary: Exchange credentials for tokens
      security: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
                mfaCode: { type: string, description: Required for staff roles }
      responses:
        "200":
          description: Authenticated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenSet" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/token/refresh:
    post:
      tags: [Auth]
      summary: Refresh an access token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken: { type: string }
      responses:
        "200":
          description: Refreshed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenSet" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
  /auth/password/forgot:
    post:
      tags: [Auth]
      summary: Begin password reset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        "202": { description: Reset email sent if the account exists }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/me:
    get:
      tags: [Auth]
      summary: Current principal, roles and tenant scope
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Principal" }
        "401": { $ref: "#/components/responses/Unauthenticated" }

  /patients/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Patients]
      summary: Get a patient
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Patient" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Patients]
      summary: Update a patient
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PatientUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Patient" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /cases/{caseId}:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Patients]
      summary: Get a case
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Case" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /cases/{caseId}/journey:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Patients]
      summary: 14-stage journey progress
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/StageProgress" }

  /clinics/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Clinics]
      summary: Get a clinic profile
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Clinic" }
        "404": { $ref: "#/components/responses/NotFound" }
  /clinics/{id}/inquiries:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - { name: status, in: query, schema: { type: string, enum: [new, quoted, accepted, declined] } }
      - $ref: "#/components/parameters/Limit"
      - $ref: "#/components/parameters/Cursor"
    get:
      tags: [Clinics]
      summary: List inbound inquiries
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ClinicInquiry" }
                  page: { $ref: "#/components/schemas/Page" }
  /clinics/{id}/inquiries/{inquiryId}/quote:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - { name: inquiryId, in: path, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/IdempotencyKey"
    post:
      tags: [Clinics]
      summary: Send an itemised quote
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QuoteInput" }
      responses:
        "201":
          description: Quote sent
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /providers/{id}/cases/{caseId}/milestones/{milestoneId}/confirm:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/CaseId"
      - { name: milestoneId, in: path, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/IdempotencyKey"
    post:
      tags: [Providers]
      summary: Provider confirms a milestone (triggers escrow release)
      responses:
        "200":
          description: Confirmed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentMilestone" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /catalogue/procedures:
    get:
      tags: [Catalogue]
      summary: List procedures
      security: []
      parameters:
        - { name: specialty, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Procedure" }
  /catalogue/clinics:
    get:
      tags: [Catalogue]
      summary: Search the clinic marketplace
      security: []
      parameters:
        - { name: specialty, in: query, schema: { type: string } }
        - { name: country, in: query, schema: { type: string } }
        - { name: maxPriceUsd, in: query, schema: { type: integer } }
        - { name: minRating, in: query, schema: { type: number } }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Clinic" }
                  page: { $ref: "#/components/schemas/Page" }

  /cases/{caseId}/documents:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Documents]
      summary: List documents
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Document" }
    post:
      tags: [Documents]
      summary: Create an upload (returns a pre-signed URL)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, type, contentType, sizeBytes]
              properties:
                name: { type: string }
                type: { $ref: "#/components/schemas/DocType" }
                contentType: { type: string }
                sizeBytes: { type: integer, maximum: 26214400 }
      responses:
        "201":
          description: Upload created
          content:
            application/json:
              schema:
                type: object
                properties:
                  document: { $ref: "#/components/schemas/Document" }
                  uploadUrl: { type: string, format: uri }
        "400": { $ref: "#/components/responses/ValidationError" }
  /cases/{caseId}/documents/{docId}/verify:
    parameters:
      - $ref: "#/components/parameters/CaseId"
      - { name: docId, in: path, required: true, schema: { type: string } }
    post:
      tags: [Documents]
      summary: Reviewer marks a document verified
      responses:
        "200":
          description: Verified; any linked visa checklist item updates
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Document" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /cases/{caseId}/consultations:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    post:
      tags: [Consultations]
      summary: Open a consultation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                documentIds:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Consultation" }

  /cases/{caseId}/appointments:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Appointments]
      summary: List appointments
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Appointment" }
    post:
      tags: [Appointments]
      summary: Schedule an appointment
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kind, startsAt]
              properties:
                kind: { type: string, enum: [consult, admission, procedure, followup] }
                startsAt: { type: string, format: date-time }
      responses:
        "201":
          description: Scheduled
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
        "409": { $ref: "#/components/responses/Conflict" }

  /cases/{caseId}/visa:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Visa]
      summary: Get the visa application
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Visa" }
  /cases/{caseId}/visa/submit:
    parameters:
      - $ref: "#/components/parameters/CaseId"
      - $ref: "#/components/parameters/IdempotencyKey"
    post:
      tags: [Visa]
      summary: Submit the application
      description: Rejected with 409 unless every checklist item resolves to done.
      responses:
        "200":
          description: Submitted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Visa" }
        "409":
          description: Checklist incomplete
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /cases/{caseId}/travel:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Travel]
      summary: Trip plan, options and selections
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TravelData" }
  /cases/{caseId}/travel/selection:
    parameters:
      - $ref: "#/components/parameters/CaseId"
      - $ref: "#/components/parameters/IdempotencyKey"
    post:
      tags: [Travel]
      summary: Submit preferred travel options
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                flightId: { type: string }
                transferId: { type: string }
                addonIds: { type: array, items: { type: string } }
      responses:
        "202": { description: Submitted to the Travel Desk }

  /cases/{caseId}/travel/stays/selection:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    post:
      tags: [Hotels]
      summary: Select a preferred stay
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [stayId]
              properties:
                stayId: { type: string }
      responses:
        "202": { description: Submitted to the Travel Desk }

  /cases/{caseId}/payments:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Payments]
      summary: Escrow summary, milestones and ledger
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payments" }
  /cases/{caseId}/payments/escrow/fund:
    parameters:
      - $ref: "#/components/parameters/CaseId"
      - $ref: "#/components/parameters/IdempotencyKey"
    post:
      tags: [Payments]
      summary: Fund the escrow deposit
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amountMinor, currency, paymentMethodId]
              properties:
                amountMinor: { type: integer }
                currency: { type: string }
                paymentMethodId: { type: string }
      responses:
        "201":
          description: Funded; ledger entry posted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payments" }
        "402": { description: Payment failed }
        "409": { $ref: "#/components/responses/Conflict" }
  /cases/{caseId}/payments/refunds:
    parameters:
      - $ref: "#/components/parameters/CaseId"
      - $ref: "#/components/parameters/IdempotencyKey"
    post:
      tags: [Payments]
      summary: Request a refund
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: string }
                amountMinor: { type: integer, description: Omit for full pre-arrival refund }
      responses:
        "201":
          description: Refund requested
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LedgerEntry" }

  /cases/{caseId}/threads:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Messaging]
      summary: List care-team threads
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Thread" }
  /threads/{id}/messages:
    parameters:
      - $ref: "#/components/parameters/PathId"
      - $ref: "#/components/parameters/Limit"
      - $ref: "#/components/parameters/Cursor"
    get:
      tags: [Messaging]
      summary: List messages
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Message" } }
                  page: { $ref: "#/components/schemas/Page" }
    post:
      tags: [Messaging]
      summary: Send a message
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
                attachmentIds: { type: array, items: { type: string } }
      responses:
        "201":
          description: Sent
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }

  /cases/{caseId}/notifications:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    get:
      tags: [Notifications]
      summary: List notifications
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Notification" }

  /reviews:
    get:
      tags: [Reviews]
      summary: List public reviews
      security: []
      parameters:
        - { name: clinicId, in: query, schema: { type: string } }
        - { name: procedure, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Review" }
  /cases/{caseId}/reviews:
    parameters:
      - $ref: "#/components/parameters/CaseId"
    post:
      tags: [Reviews]
      summary: Submit a verified review at close-out
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating, title, body]
              properties:
                rating: { type: integer, minimum: 1, maximum: 5 }
                title: { type: string }
                body: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Review" }

  /admin/country-packs:
    get:
      tags: [Admin]
      summary: List Country Packs
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/CountryPack" }
    post:
      tags: [Admin]
      summary: Create a Country Pack version
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CountryPack" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CountryPack" }

  /reports/funnel:
    get:
      tags: [Reports]
      summary: Lead-to-case funnel by corridor and channel
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: corridorId, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema: { type: string }
    CaseId:
      name: caseId
      in: path
      required: true
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 25, maximum: 100 }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: Required for create and money-moving operations.

  responses:
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthenticated:
      description: Missing or invalid credentials
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Authenticated but not allowed (out of role, tenant or case scope)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: State-machine or uniqueness conflict
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details:
              type: array
              items:
                type: object
                properties:
                  field: { type: string }
                  issue: { type: string }
            requestId: { type: string }
    Page:
      type: object
      properties:
        nextCursor: { type: string, nullable: true }
        hasMore: { type: boolean }
    Money:
      type: object
      properties:
        amountMinor: { type: integer }
        currency: { type: string, description: ISO-4217 }
    TokenSet:
      type: object
      properties:
        accessToken: { type: string }
        refreshToken: { type: string }
        expiresIn: { type: integer }
        tokenType: { type: string, default: Bearer }
    Principal:
      type: object
      properties:
        userId: { type: string }
        roles: { type: array, items: { type: string } }
        clinicId: { type: string, nullable: true }
        caseIds: { type: array, items: { type: string } }
    Patient:
      type: object
      properties:
        id: { type: string }
        caseId: { type: string }
        name: { type: string }
        initials: { type: string }
        age: { type: integer }
        sex: { type: string }
        origin: { type: string }
        flag: { type: string }
        treatment: { type: string }
        specialty: { type: string }
        hospital: { type: string }
        surgeon: { type: string }
        surgeryDate: { type: string }
        attendant: { type: string }
        corridor: { type: string }
        lang: { type: string }
        currentStage: { type: integer, minimum: 0, maximum: 13 }
    PatientUpdate:
      type: object
      properties:
        name: { type: string }
        origin: { type: string }
        lang: { type: string }
        attendant: { type: string }
    Case:
      type: object
      properties:
        caseId: { type: string }
        patientId: { type: string }
        clinicId: { type: string }
        corridorId: { type: string }
        coordinatorId: { type: string }
        currentStage: { type: integer }
        status: { type: string, enum: [open, on_hold, closed, cancelled] }
        dataRegime: { type: string }
        riskTier: { type: string, enum: [standard, enhanced] }
    StageProgress:
      type: object
      properties:
        key: { type: string }
        label: { type: string }
        group: { type: string, enum: [Discover, Plan, Treat, Return] }
        owner: { type: string }
        slaTarget: { type: string }
        status: { type: string, enum: [done, active, upcoming] }
        enteredAt: { type: string, format: date-time, nullable: true }
        dueAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
    Clinic:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        city: { type: string }
        country: { type: string }
        accreditations: { type: array, items: { type: string } }
        specialties: { type: array, items: { type: string } }
        priceFromUsd: { type: integer }
        savingsPct: { type: integer }
        vetted: { type: boolean }
        reviews:
          type: object
          properties:
            rating: { type: number }
            count: { type: integer }
    ClinicInquiry:
      type: object
      properties:
        id: { type: string }
        patient: { type: string }
        country: { type: string }
        procedure: { type: string }
        date: { type: string }
        status: { type: string, enum: [new, quoted, accepted, declined] }
        budgetUsd: { type: integer, nullable: true }
        message: { type: string }
    QuoteInput:
      type: object
      required: [lines]
      properties:
        lines:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              amountMinor: { type: integer }
              group: { type: string, enum: [Treatment, Hospital, "Travel & stay", Services] }
        currency: { type: string }
        notes: { type: string }
    Quote:
      type: object
      properties:
        id: { type: string }
        inquiryId: { type: string }
        total: { $ref: "#/components/schemas/Money" }
        lines: { type: array, items: { type: object, additionalProperties: true } }
        status: { type: string, enum: [sent, accepted, declined, expired] }
    Procedure:
      type: object
      properties:
        key: { type: string }
        name: { type: string }
        specialty: { type: string }
        priceFromUsd: { type: integer }
        refPriceUsd: { type: integer }
        refCountry: { type: string }
        savingsPct: { type: integer }
        stay: { type: string }
    DocType:
      type: string
      enum: [Identity, Medical, Visa, Financial, Travel]
    Document:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        type: { $ref: "#/components/schemas/DocType" }
        status: { type: string, enum: [pending, verified] }
        size: { type: string }
        date: { type: string }
    Consultation:
      type: object
      properties:
        id: { type: string }
        caseId: { type: string }
        status: { type: string, enum: [open, awaiting_records, opinion_ready, closed] }
        opinion: { type: string, nullable: true }
        specialist: { type: string, nullable: true }
    Appointment:
      type: object
      properties:
        id: { type: string }
        kind: { type: string, enum: [consult, admission, procedure, followup] }
        startsAt: { type: string, format: date-time }
        status: { type: string, enum: [scheduled, completed, cancelled, no_show] }
    Visa:
      type: object
      properties:
        type: { type: string }
        country: { type: string }
        from: { type: string }
        fee: { type: string }
        processing: { type: string }
        validity: { type: string }
        status: { type: string }
        ref: { type: string }
        checklist:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              done: { type: boolean }
              docId: { type: integer, nullable: true }
        timeline:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              date: { type: string }
              done: { type: boolean }
              active: { type: boolean, nullable: true }
    CountryPack:
      type: object
      properties:
        corridorId: { type: string }
        from: { type: string }
        to: { type: string }
        version: { type: integer }
        effectiveFrom: { type: string, format: date }
        type: { type: string, enum: [e-Visa, Paper visa] }
        currency: { type: string }
        payment: { type: string }
        dataRegime: { type: string }
        riskTier: { type: string, enum: [standard, enhanced] }
        requiredDocuments: { type: array, items: { type: string } }
        featureFlags: { type: object, additionalProperties: { type: boolean } }
    TravelData:
      type: object
      properties:
        trip: { type: object, additionalProperties: true }
        flights: { type: array, items: { type: object, additionalProperties: true } }
        stays: { type: array, items: { type: object, additionalProperties: true } }
        transfers: { type: array, items: { type: object, additionalProperties: true } }
        addons: { type: array, items: { type: object, additionalProperties: true } }
    PaymentMilestone:
      type: object
      properties:
        label: { type: string }
        amount: { type: integer }
        status: { type: string, enum: [released, held, scheduled] }
        date: { type: string }
        note: { type: string }
    LedgerEntry:
      type: object
      properties:
        date: { type: string }
        desc: { type: string }
        amount: { type: integer }
        kind: { type: string, enum: [out, rel] }
    Payments:
      type: object
      properties:
        total: { type: integer }
        held: { type: integer }
        currency: { type: string }
        milestones: { type: array, items: { $ref: "#/components/schemas/PaymentMilestone" } }
        ledger: { type: array, items: { $ref: "#/components/schemas/LedgerEntry" } }
    Thread:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        role: { type: string }
        unread: { type: integer }
        online: { type: boolean }
        last: { type: string }
    Message:
      type: object
      properties:
        id: { type: string }
        me: { type: boolean }
        t: { type: string }
        time: { type: string }
    Notification:
      type: object
      properties:
        icon: { type: string }
        text: { type: string }
        time: { type: string }
        tone: { type: string, enum: [amber, green, teal] }
    Review:
      type: object
      properties:
        id: { type: string }
        author: { type: string }
        country: { type: string }
        rating: { type: integer, minimum: 1, maximum: 5 }
        date: { type: string }
        procedure: { type: string }
        clinicId: { type: string, nullable: true }
        source: { type: string, enum: [Trustpilot, Google, Verified visit] }
        verified: { type: boolean }
        title: { type: string }
        body: { type: string }
