openapi: 3.1.0
info:
  title: Digits Health — Clinical API
  version: 0.1.0
  description: >
    REST API for the Digits Health hand-rehabilitation platform: assessments
    (ROM, dexterity, pain, arthritis), patient records, journals, bundled
    reports, biomechanics analysis, text-to-speech, and Stripe billing.


    All paths are relative to a base URL that includes the `/api` prefix
    (e.g. `https://api.digitshealth.com/api`). Authentication uses Auth0
    RS256 bearer tokens; many submission endpoints accept anonymous requests.
  license:
    name: UNLICENSED
servers:
  - url: http://localhost:3001/api
    description: Local development
  - url: https://dev.digitshealth.com/api
    description: Development / Cloud Run
  - url: https://api.digitshealth.com/api
    description: Production

tags:
  - name: Health
  - name: Assessments
  - name: Lookup
  - name: Patients
  - name: Journal
  - name: Reports
  - name: Biomechanics
  - name: AI
  - name: Billing
  - name: Leads

security:
  - bearerAuth: []
  - {}

paths:
  /health:
    get:
      tags: [Health]
      summary: Liveness & version probe
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HealthStatus' }

  /assessments/rom:
    post:
      tags: [Assessments]
      summary: Save a ROM assessment
      description: Optional auth — links to the caller when a token is supplied.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RomSubmission' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RomSubmissionResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

  /assessments/dexterity:
    post:
      tags: [Assessments]
      summary: Save a dexterity (finger-tapping) assessment
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DexteritySubmission' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DexteritySubmissionResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

  /assessments/pain-v2:
    post:
      tags: [Assessments]
      summary: Save a pain assessment (v2)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PainSubmission' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PainSubmissionResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

  /assessments/pain:
    post:
      tags: [Assessments]
      summary: Save a legacy single-rating pain assessment (not persisted)
      deprecated: true
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LegacyPainSubmission' }
      responses:
        '201':
          description: Echoed entry
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '400': { $ref: '#/components/responses/BadRequest' }

  /assessments/arthritis:
    post:
      tags: [Assessments]
      summary: Save an arthritis evaluation
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ArthritisSubmission' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ArthritisSubmissionResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

  /assessments/{idOrCode}/results:
    get:
      tags: [Assessments]
      summary: Fetch a saved assessment with full detail
      security: []
      parameters:
        - name: idOrCode
          in: path
          required: true
          description: Numeric record id or 6-character code (dash optional).
          schema: { type: string }
      responses:
        '200':
          description: Assessment with detail tree
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AssessmentResults' }
        '404': { $ref: '#/components/responses/NotFound' }

  /lookup/{code}:
    get:
      tags: [Lookup]
      summary: Public lookup by code
      security: []
      parameters: [{ $ref: '#/components/parameters/Code' }]
      responses:
        '200':
          description: Assessment summary
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LookupResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }

  /lookup/{code}/claim:
    post:
      tags: [Lookup]
      summary: Claim an anonymous assessment
      parameters: [{ $ref: '#/components/parameters/Code' }]
      responses:
        '200':
          description: Claimed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  code: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /lookup/{code}/email:
    post:
      tags: [Lookup]
      summary: Email assessment results
      security: []
      parameters: [{ $ref: '#/components/parameters/Code' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        '200':
          description: Send result
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmailResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /lookup/anonymous:
    post:
      tags: [Lookup]
      summary: Create a bare assessment record and mint a code
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AnonymousAssessmentInput' }
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AnonymousAssessmentResult' }

  /patients/me/scores:
    get:
      tags: [Patients]
      summary: Score history for the authenticated user
      parameters:
        - { $ref: '#/components/parameters/Hand' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Scores
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScoresResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /patients/me/assessments:
    get:
      tags: [Patients]
      summary: Assessment history for the authenticated user
      parameters:
        - { $ref: '#/components/parameters/Type' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Assessments
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AssessmentHistoryResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /patients:
    get:
      tags: [Patients]
      summary: List patients (care_provider / admin)
      parameters:
        - { name: search, in: query, schema: { type: string } }
        - { name: trend, in: query, schema: { type: string, enum: [improving, declining, stable] } }
        - { name: attention, in: query, schema: { type: string, enum: ['true', 'false'] } }
        - { name: sort, in: query, schema: { type: string, enum: [name, score, recent] } }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Patient list
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PatientListResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /patients/{id}:
    get:
      tags: [Patients]
      summary: Patient profile (care_provider / admin)
      parameters: [{ $ref: '#/components/parameters/PatientId' }]
      responses:
        '200':
          description: Profile
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PatientProfile' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /patients/{id}/assessments:
    get:
      tags: [Patients]
      summary: Assessment history for a patient
      parameters:
        - { $ref: '#/components/parameters/PatientId' }
        - { $ref: '#/components/parameters/Type' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Assessments
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AssessmentHistoryResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /patients/{id}/journal:
    get:
      tags: [Patients]
      summary: Journal entries for a patient
      parameters:
        - { $ref: '#/components/parameters/PatientId' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Journal
          content:
            application/json:
              schema:
                type: object
                properties:
                  journal:
                    type: array
                    items: { $ref: '#/components/schemas/JournalSummaryItem' }
                  total: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /patients/{id}/scores:
    get:
      tags: [Patients]
      summary: Score history for a patient
      parameters:
        - { $ref: '#/components/parameters/PatientId' }
        - { $ref: '#/components/parameters/Hand' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Scores
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScoresResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /patients/{id}/narrative-summary:
    get:
      tags: [Patients]
      summary: AI-generated clinical progression summary
      parameters: [{ $ref: '#/components/parameters/PatientId' }]
      responses:
        '200':
          description: Summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary: { type: string }
                  source: { type: string, enum: [ai, template] }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /journal:
    post:
      tags: [Journal]
      summary: Create a journal entry
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JournalEntryInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '400': { $ref: '#/components/responses/BadRequest' }
    get:
      tags: [Journal]
      summary: List journal entries
      security: []
      parameters:
        - { name: patientId, in: query, required: true, schema: { type: integer } }
        - { $ref: '#/components/parameters/Type' }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200':
          description: Entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  entries: { type: array, items: { type: object, additionalProperties: true } }
                  total: { type: integer }

  /journal/{id}:
    get:
      tags: [Journal]
      summary: Get a journal entry
      security: []
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        '200':
          description: Entry
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '404': { $ref: '#/components/responses/NotFound' }

  /journal/narrative-context/{patientId}:
    get:
      tags: [Journal]
      summary: Prompt-ready narrative context
      security: []
      parameters: [{ name: patientId, in: path, required: true, schema: { type: integer } }]
      responses:
        '200':
          description: Context
          content:
            application/json:
              schema:
                type: object
                properties: { context: { type: string } }

  /reports:
    post:
      tags: [Reports]
      summary: Create a bundled report
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateReportInput' }
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateReportResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

  /reports/{code}:
    get:
      tags: [Reports]
      summary: Get a report by code
      security: []
      parameters: [{ $ref: '#/components/parameters/Code' }]
      responses:
        '200':
          description: Report
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Report' }
        '404': { $ref: '#/components/responses/NotFound' }

  /reports/{code}/pdf:
    get:
      tags: [Reports]
      summary: Download a report as PDF
      security: []
      parameters: [{ $ref: '#/components/parameters/Code' }]
      responses:
        '200':
          description: PDF bytes
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }

  /reports/{code}/email:
    post:
      tags: [Reports]
      summary: Email a report (PDF attached)
      security: []
      parameters: [{ $ref: '#/components/parameters/Code' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to]
              properties: { to: { type: string, format: email } }
      responses:
        '200':
          description: Send result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  sent: { type: boolean }
                  message: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }

  /reports/{code}/explain:
    post:
      tags: [Reports]
      summary: Generate an AI explanation of a report
      security: []
      parameters: [{ $ref: '#/components/parameters/Code' }]
      responses:
        '200':
          description: Explanation
          content:
            application/json:
              schema:
                type: object
                properties:
                  explanation: { type: string }
                  source: { type: string, enum: [ai, fallback] }
        '404': { $ref: '#/components/responses/NotFound' }

  # All biomechanics endpoints share one proxied operation shape (YAML anchor).
  /biomechanics/wrist/deviation:
    post: &biomechPost
      tags: [Biomechanics]
      summary: Kinematic analysis (proxied to the Python service)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        '200':
          description: Analysis result (opaque JSON)
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /biomechanics/wrist/deviation/neutral-pose:
    post: *biomechPost
  /biomechanics/wrist/extension-flexion:
    post: *biomechPost
  /biomechanics/wrist/extension-flexion/neutral-pose:
    post: *biomechPost
  /biomechanics/wrist/rotation:
    post: *biomechPost
  /biomechanics/wrist/rotation/neutral-pose:
    post: *biomechPost
  /biomechanics/thumb/abduction:
    post: *biomechPost
  /biomechanics/thumb/circumduction:
    post: *biomechPost
  /biomechanics/thumb/extension-flexion:
    post: *biomechPost

  /ai/tts:
    post:
      tags: [AI]
      summary: Synthesize speech
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties: { text: { type: string } }
      responses:
        '200':
          description: Audio payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  audio: { type: string, description: Base64-encoded audio }
                  mimeType: { type: string, example: audio/L16;rate=24000 }
        '204': { description: No audio generated }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

  /billing/checkout:
    post:
      tags: [Billing]
      summary: Create a Stripe Checkout session
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { plan: { type: string, enum: [core, plus], default: plus } }
      responses:
        '200':
          description: Checkout URL
          content:
            application/json:
              schema: { type: object, properties: { url: { type: string } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '502': { $ref: '#/components/responses/UpstreamError' }
        '503': { $ref: '#/components/responses/NotConfigured' }

  /billing/checkout/verify:
    post:
      tags: [Billing]
      summary: Verify a checkout session and sync the subscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sessionId]
              properties: { sessionId: { type: string } }
      responses:
        '200':
          description: Subscription
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionInfo' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /billing/portal:
    post:
      tags: [Billing]
      summary: Open the Stripe customer portal
      responses:
        '200':
          description: Portal URL
          content:
            application/json:
              schema: { type: object, properties: { url: { type: string } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { $ref: '#/components/responses/NotConfigured' }

  /billing/subscription:
    get:
      tags: [Billing]
      summary: Get current subscription status
      responses:
        '200':
          description: Subscription
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubscriptionInfo' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /billing/webhook:
    post:
      tags: [Billing]
      summary: Stripe webhook (server-to-server, signature-verified)
      security: []
      description: >
        Requires the raw request body and a valid `stripe-signature` header.
        See docs/webhooks.md.
      parameters:
        - name: stripe-signature
          in: header
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        '200':
          description: Acknowledged
          content:
            application/json:
              schema: { type: object, properties: { received: { type: boolean } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '503': { $ref: '#/components/responses/NotConfigured' }

  /leads/developer:
    post:
      tags: [Leads]
      summary: Capture a developer / partner sign-up
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeveloperLead' }
      responses:
        '201':
          description: Captured
          content:
            application/json:
              schema:
                type: object
                properties:
                  received: { type: boolean }
                  sent: { type: boolean }
                  message: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/ServerError' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Auth0-issued RS256 access token.

  parameters:
    Code:
      name: code
      in: path
      required: true
      description: 6-character assessment/report code (dash optional).
      schema: { type: string, example: XHE7JR }
    PatientId:
      name: id
      in: path
      required: true
      schema: { type: integer, format: int64 }
    Hand:
      name: hand
      in: query
      schema: { type: string, enum: [LEFT, RIGHT, BOTH] }
    Type:
      name: type
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer }
    Offset:
      name: offset
      in: query
      schema: { type: integer }

  responses:
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Insufficient role / ownership
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ServerError:
      description: Internal server error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UpstreamError:
      description: Upstream dependency failed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotConfigured:
      description: Feature not configured
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string }
        message: { type: string }
      required: [error]

    HealthStatus:
      type: object
      properties:
        status: { type: string, enum: [ok] }
        timestamp: { type: string, format: date-time }
        version: { type: string }

    Hand:
      type: string
      enum: [LEFT, RIGHT]
    HandSide:
      type: string
      enum: [LEFT, RIGHT, BOTH]
    LightingQuality:
      type: string
      enum: [GOOD, FAIR, POOR]
    LightingMetrics:
      type: object
      properties:
        meanY: { type: number }
        stdY: { type: number }

    RomWrist:
      type: object
      properties:
        unsigned_rom_deg: { type: number, nullable: true }
        max_extension_deg: { type: number, nullable: true }
        min_extension_deg: { type: number, nullable: true }
        avg_extension_deg: { type: number, nullable: true }
        max_flexion_deg: { type: number, nullable: true }
        min_flexion_deg: { type: number, nullable: true }
        avg_flexion_deg: { type: number, nullable: true }
        frames_total: { type: integer }
        frames_valid: { type: integer }
        percent_valid_frames: { type: number }
    RomJoint:
      type: object
      required: [id, finger, jointType, minAngle, maxAngle, avgAngle, rom, normalRange, flexibilityScore, frameCount]
      properties:
        id: { type: string }
        finger: { type: string }
        jointType: { type: string }
        minAngle: { type: number }
        maxAngle: { type: number }
        avgAngle: { type: number }
        rom: { type: number }
        normalRange:
          type: array
          items: { type: number }
          minItems: 2
          maxItems: 2
        flexibilityScore: { type: number }
        frameCount: { type: integer }
    RomSubmission:
      type: object
      required: [hand, overallScore]
      properties:
        hand: { $ref: '#/components/schemas/Hand' }
        overallScore: { type: number }
        wristScore: { type: number, nullable: true }
        fingerScore: { type: number, nullable: true }
        lightingQuality: { $ref: '#/components/schemas/LightingQuality' }
        lightingMetrics: { $ref: '#/components/schemas/LightingMetrics' }
        wrist: { $ref: '#/components/schemas/RomWrist' }
        joints:
          type: array
          items: { $ref: '#/components/schemas/RomJoint' }
    RomSubmissionResult:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        formattedCode: { type: string }
        overallScore: { type: number }
        wristScore: { type: number }
        fingerScore: { type: number }

    DexterityFingerScore:
      type: object
      properties:
        finger: { type: string }
        accuracy: { type: number }
        consistency: { type: number }
        score: { type: number }
        touchCount: { type: integer }
    DexteritySubmission:
      type: object
      required: [hand, duration, overallScore, speedScore, consistencyScore, cycleCount, totalTouches]
      properties:
        hand: { $ref: '#/components/schemas/Hand' }
        duration: { type: number }
        overallScore: { type: number }
        speedScore: { type: number }
        accuracyScore: { type: number }
        touchPrecision: { type: number }
        consistencyScore: { type: number }
        cycleCount: { type: integer }
        totalTouches: { type: integer }
        sequencesCompleted: { type: integer }
        timeTo10Sequences: { type: number, nullable: true }
        averageFrequency: { type: number }
        sequenceAccuracy: { type: number }
        fingerScores:
          type: object
          additionalProperties: { $ref: '#/components/schemas/DexterityFingerScore' }
        lightingQuality: { $ref: '#/components/schemas/LightingQuality' }
        lightingMetrics: { $ref: '#/components/schemas/LightingMetrics' }
    DexteritySubmissionResult:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        formattedCode: { type: string }
        overallScore: { type: number }
        cycleCount: { type: integer }
        totalTouches: { type: integer }

    PainEntry:
      type: object
      required: [areaType, hand, severity]
      properties:
        areaType: { type: string, enum: [DIGIT_JOINT, PALM, DORSAL, WRIST] }
        hand: { $ref: '#/components/schemas/HandSide' }
        digit: { type: string, enum: [THUMB, INDEX, MIDDLE, RING, LITTLE] }
        location: { type: string, enum: [TIP, DIP, PIP, MCP, CMC, PALM, DORSAL] }
        severity: { type: integer, minimum: 0, maximum: 10 }
    PainMedication:
      type: object
      required: [medicationName]
      properties:
        medicationName: { type: string }
        dose: { type: string }
        effectivenessRating: { type: integer, minimum: 0, maximum: 10 }
    PainSubmission:
      type: object
      required: [mode, hand]
      properties:
        mode: { type: string, enum: [QUICK, DETAILED] }
        hand: { $ref: '#/components/schemas/HandSide' }
        painType: { type: string, enum: [SHARP, DULL_ACHING, BURNING, THROBBING, TINGLING_NUMBNESS, STIFFNESS, SWELLING] }
        timeOfDay: { type: string, enum: [MORNING, AFTERNOON, EVENING, NIGHT, CONSTANT] }
        entries:
          type: array
          items: { $ref: '#/components/schemas/PainEntry' }
        medications:
          type: array
          items: { $ref: '#/components/schemas/PainMedication' }
        strategies:
          type: array
          items: { type: string, enum: [ELEVATION, REST, ICE, COMPRESSION, SPLINTING, OTHER] }
        comments: { type: string }
    PainSubmissionResult:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        formattedCode: { type: string }
        dateCreated: { type: string, format: date-time }
        isComplete: { type: boolean }
        maxSeverity: { type: integer }
        entryCount: { type: integer }
    LegacyPainSubmission:
      type: object
      required: [painRating, isTakingMedication, isElevating, isResting]
      properties:
        painRating: { type: integer, minimum: 0, maximum: 10 }
        isTakingMedication: { type: boolean }
        isTakingAcetaminophen: { type: boolean }
        isTakingIbuprofen: { type: boolean }
        isTakingNarcotics: { type: boolean }
        otherMedications: { type: string }
        isElevating: { type: boolean }
        isResting: { type: boolean }
        comments: { type: string }

    ArthritisSubmission:
      type: object
      required: [hand, morningStiffnessDuration, morningStiffnessSeverity, functionalLimitations, jointSymptoms, symptomSymmetry, symptomOnset, familyHistoryArthritis, compositeScore, progressionLevel, patternType]
      properties:
        hand: { $ref: '#/components/schemas/HandSide' }
        morningStiffnessDuration: { type: string, enum: [NONE, UNDER_15_MIN, MIN_15_TO_30, MIN_30_TO_60, OVER_60_MIN] }
        morningStiffnessSeverity: { type: integer, minimum: 0, maximum: 10 }
        functionalLimitations:
          type: array
          items:
            type: object
            properties:
              task: { type: string }
              difficulty: { type: string, enum: [NONE, MILD, MODERATE, SEVERE, UNABLE] }
        jointSymptoms:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              digit: { type: string }
              location: { type: string }
              tenderness: { type: boolean }
              swelling: { type: boolean }
              warmth: { type: boolean }
              crepitus: { type: boolean }
        symptomSymmetry: { type: string, enum: [LEFT_ONLY, RIGHT_ONLY, BOTH_SYMMETRIC, BOTH_ASYMMETRIC] }
        symptomOnset: { type: string, enum: [GRADUAL_MONTHS, GRADUAL_YEARS, SUDDEN_WEEKS, FLARE_UPS] }
        familyHistoryArthritis: { type: boolean }
        compositeScore: { type: number }
        progressionLevel: { type: integer, minimum: 0, maximum: 4 }
        patternType: { type: string, enum: [UNDETERMINED, OA_PATTERN, RA_PATTERN, MIXED] }
        romDeficitScore: { type: number }
        painBurdenScore: { type: number }
        functionalScore: { type: number }
        stiffnessScore: { type: number }
        affectedJointCount: { type: integer }
        jointHealthMap:
          type: array
          items:
            type: object
            properties:
              digit: { type: string }
              location: { type: string }
              healthScore: { type: number }
              romScore: { type: number, nullable: true }
              painScore: { type: number, nullable: true }
              hasSymptoms: { type: boolean }
              flags: { type: array, items: { type: string } }
        clinicalFlags: { type: array, items: { type: string } }
        recommendation: { type: string }
        sourceRomAssessmentId: { type: integer }
        sourceDexterityAssessmentId: { type: integer }
        sourcePainSessionId: { type: integer }
    ArthritisSubmissionResult:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        formattedCode: { type: string }
        compositeScore: { type: number }
        progressionLevel: { type: integer }
        patternType: { type: string }

    AssessmentResults:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        formattedCode: { type: string }
        assessmentType: { type: string }
        hand: { type: string, nullable: true }
        dateCreated: { type: string, format: date-time }
        resultSnapshot: { type: object, additionalProperties: true, nullable: true }
        results: { type: object, additionalProperties: true, nullable: true }

    LookupResponse:
      type: object
      properties:
        code: { type: string }
        formattedCode: { type: string }
        assessmentType: { type: string }
        hand: { type: string, nullable: true }
        dateCreated: { type: string, format: date-time }
        resultSnapshot: { type: object, additionalProperties: true, nullable: true }
        isAnonymous: { type: boolean }
        viewCount: { type: integer }

    AnonymousAssessmentInput:
      type: object
      required: [assessmentType]
      properties:
        assessmentType: { type: string }
        hand: { type: string }
        resultSnapshot: { type: object, additionalProperties: true }
        email: { type: string, format: email }
    AnonymousAssessmentResult:
      type: object
      properties:
        code: { type: string }
        formattedCode: { type: string }
        message: { type: string }

    EmailResult:
      type: object
      properties:
        success: { type: boolean }
        sent: { type: boolean }
        message: { type: string }
        code: { type: string }

    ScorePoint:
      type: object
      properties:
        date: { type: string, format: date-time }
        overallScore: { type: number }
        romScore: { type: number, nullable: true }
        dexterityScore: { type: number, nullable: true }
        painScore: { type: number, nullable: true }
    ScoresResponse:
      type: object
      properties:
        scores:
          type: array
          items: { $ref: '#/components/schemas/ScorePoint' }

    AssessmentHistoryItem:
      type: object
      properties:
        id: { type: string }
        code: { type: string }
        type: { type: string }
        hand: { type: string, nullable: true }
        score: { type: number, nullable: true }
        detail: { type: string }
        results: { type: object, additionalProperties: true }
        resultSnapshot: { type: object, additionalProperties: true, nullable: true }
        date: { type: string, format: date-time }
    AssessmentHistoryResponse:
      type: object
      properties:
        assessments:
          type: array
          items: { $ref: '#/components/schemas/AssessmentHistoryItem' }
        total: { type: integer }

    PatientListItem:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        hand: { type: string, nullable: true }
        injury: { type: string, nullable: true }
        lastAssessment: { type: string, format: date-time, nullable: true }
        score: { type: number, nullable: true }
        trend: { type: string, enum: [improving, stable, declining] }
        needsAttention: { type: boolean }
        assessmentCount: { type: integer }
    PatientListResponse:
      type: object
      properties:
        patients:
          type: array
          items: { $ref: '#/components/schemas/PatientListItem' }
        total: { type: integer }
    PatientProfile:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        firstName: { type: string }
        lastName: { type: string, nullable: true }
        age: { type: integer, nullable: true }
        hand: { type: string, nullable: true }
        injury: { type: string, nullable: true }
        injuryStory: { type: string, nullable: true }
        surgeryDate: { type: string, format: date-time, nullable: true }
        assignedSince: { type: string, format: date-time }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        scores:
          type: object
          nullable: true
          properties:
            overall: { type: number }
            rom: { type: number, nullable: true }
            dexterity: { type: number, nullable: true }
            pain: { type: number, nullable: true }
            date: { type: string, format: date-time }
        treatments:
          type: array
          items: { type: object, additionalProperties: true }

    JournalSummaryItem:
      type: object
      properties:
        date: { type: string, format: date-time }
        type: { type: string }
        hand: { type: string, nullable: true }
        pain: { type: number, nullable: true }
        function: { type: number, nullable: true }
        summary: { type: string, nullable: true }
    JournalEntryInput:
      type: object
      required: [patientId, entryType, transcript]
      properties:
        patientId: { type: integer }
        entryType: { type: string, enum: [PRE_ASSESSMENT, POST_ASSESSMENT, AD_HOC_CHECK_IN, INITIAL_INTAKE] }
        affectedHand: { $ref: '#/components/schemas/HandSide' }
        painRating: { type: number }
        functionRating: { type: number }
        transcript: { type: string }
        aiSummary: { type: string }
        romAssessmentId: { type: integer }
        dexterityAssessmentId: { type: integer }
        painScaleAssessmentId: { type: integer }
        orchestratorSessionId: { type: string }

    CreateReportInput:
      type: object
      required: [title, assessmentCodes]
      properties:
        title: { type: string }
        assessmentCodes:
          type: array
          items: { type: string }
          minItems: 1
          maxItems: 20
    CreateReportResult:
      type: object
      properties:
        code: { type: string }
        title: { type: string }
        createdAt: { type: string, format: date-time }
        assessmentCount: { type: integer }
    ReportAssessment:
      type: object
      properties:
        code: { type: string }
        type: { type: string }
        hand: { type: string }
        score: { type: number, nullable: true }
        date: { type: string }
        detail: { type: string }
    Report:
      type: object
      properties:
        code: { type: string }
        title: { type: string }
        createdAt: { type: string, format: date-time }
        patientName: { type: string, nullable: true }
        assessments:
          type: array
          items: { $ref: '#/components/schemas/ReportAssessment' }
        aiExplanation: { type: string, nullable: true }

    SubscriptionInfo:
      type: object
      properties:
        tier: { type: string, enum: [CORE, PLUS], nullable: true }
        status: { type: string, enum: [ACTIVE, PAST_DUE, CANCELED, UNPAID, TRIALING, INCOMPLETE], nullable: true }
        currentPeriodEnd: { type: string, format: date-time, nullable: true }
        cancelAtPeriodEnd: { type: boolean }

    DeveloperLead:
      type: object
      required: [name, email]
      properties:
        type: { type: string, enum: [STAY_IN_TOUCH, DEMO], default: STAY_IN_TOUCH }
        name: { type: string, maxLength: 120 }
        email: { type: string, format: email, maxLength: 200 }
        company: { type: string, maxLength: 160 }
        interest: { type: string, enum: [API, SDK, FHIR, RESEARCH, OTHER] }
        message: { type: string, maxLength: 4000 }
        website: { type: string, description: Honeypot — leave empty. }
