openapi: 3.1.0
info:
  title: ACS Diagnose API
  version: 0.1.0
  description: |
    Contract for the ACS Diagnose native applications and staff console.
    Diagnostic evidence is collected read-only by the mobile app. The server validates,
    stores and explains the resulting assessment; it does not control the vehicle.
  contact:
    name: Amman Car Services
    url: https://ammancarservices.com
servers:
  - url: https://api.ammancarservices.com/diagnose/v1
    description: Production placeholder
  - url: http://127.0.0.1:8787/diagnose/v1
    description: Local development
tags:
  - name: Authentication
  - name: Account
  - name: Vehicles
  - name: Scans
  - name: Attachments
  - name: Service requests
  - name: Staff
  - name: Privacy

paths:
  /auth/exchange:
    post:
      tags: [Authentication]
      summary: Exchange an OTP provider token for an ACS session
      description: |
        The native app completes phone verification with the configured OTP provider,
        then submits that provider's short-lived identity token here. Provider-specific
        verification is deliberately outside this contract until the provider is selected.
      security: []
      operationId: exchangeIdentityToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthExchangeRequest'
      responses:
        '200':
          description: ACS session issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /auth/refresh:
    post:
      tags: [Authentication]
      summary: Rotate an ACS session using a refresh token
      security: []
      operationId: refreshSession
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [refresh_token]
              properties:
                refresh_token: { type: string, minLength: 32, maxLength: 2048 }
      responses:
        '200':
          description: Rotated session
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthSession' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /auth/logout:
    post:
      tags: [Authentication]
      summary: Revoke the current session
      operationId: logout
      responses:
        '204': { description: Session revoked }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /me:
    get:
      tags: [Account]
      summary: Get the signed-in account and server-controlled capabilities
      operationId: getMe
      responses:
        '200':
          description: Current account
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Account' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    patch:
      tags: [Account]
      summary: Update customer-controlled profile fields
      operationId: updateMe
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: { type: string, minLength: 1, maxLength: 120 }
                preferred_language: { $ref: '#/components/schemas/Language' }
      responses:
        '200':
          description: Updated account
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Account' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /me/consents:
    get:
      tags: [Account]
      summary: List current consent decisions
      operationId: listConsents
      responses:
        '200':
          description: Consent decisions
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: '#/components/schemas/Consent' } }
    put:
      tags: [Account]
      summary: Record or withdraw a versioned consent
      operationId: putConsent
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConsentInput' }
      responses:
        '200':
          description: Consent stored
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Consent' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /vehicles:
    get:
      tags: [Vehicles]
      summary: List vehicles owned by the current customer
      operationId: listVehicles
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Vehicles
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VehiclePage' }
    post:
      tags: [Vehicles]
      summary: Register a Mercedes-Benz vehicle
      operationId: createVehicle
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VehicleInput' }
      responses:
        '201':
          description: Vehicle created
          headers:
            Location: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vehicle' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
  /vehicles/{vehicle_id}:
    parameters:
      - $ref: '#/components/parameters/VehicleId'
    get:
      tags: [Vehicles]
      summary: Get a customer vehicle
      operationId: getVehicle
      responses:
        '200':
          description: Vehicle
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vehicle' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Vehicles]
      summary: Update customer-confirmed vehicle fields
      description: An observed VIN cannot silently replace the selected vehicle's VIN.
      operationId: updateVehicle
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema: { $ref: '#/components/schemas/VehicleInput' }
      responses:
        '200':
          description: Updated vehicle
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vehicle' }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Vehicles]
      summary: Remove a vehicle from the active account view
      description: Eligible data is deleted asynchronously according to retention policy.
      operationId: deleteVehicle
      responses:
        '202':
          description: Deletion request accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyRequest' }

  /scans:
    get:
      tags: [Scans]
      summary: List the customer's server-synced scans
      operationId: listScans
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - name: vehicle_id
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Scan summaries
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScanPage' }
    post:
      tags: [Scans]
      summary: Upload a completed or inconclusive scan package
      description: |
        This endpoint is safe to retry. The client must retain the same scan ID,
        Idempotency-Key and raw evidence hash. A matching retry reconciles to the
        existing record; a different hash for the same scan ID returns a conflict.
      operationId: uploadScan
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: './acs-diagnose-schema.json'
      responses:
        '201':
          description: New scan accepted
          headers:
            Location: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScanReceipt' }
        '200':
          description: Matching scan already existed and was reconciled
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScanReceipt' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '422': { $ref: '#/components/responses/Unprocessable' }
  /scans/{scan_id}:
    parameters:
      - $ref: '#/components/parameters/ScanId'
    get:
      tags: [Scans]
      summary: Get a complete customer-visible scan report
      operationId: getScan
      responses:
        '200':
          description: Scan record
          content:
            application/json:
              schema:
                $ref: './acs-diagnose-schema.json'
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Scans]
      summary: Request deletion of an eligible scan
      operationId: deleteScan
      responses:
        '202':
          description: Deletion request accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyRequest' }
  /scans/{scan_id}/explanation:
    parameters:
      - $ref: '#/components/parameters/ScanId'
    get:
      tags: [Scans]
      summary: Get explanation generation status and result
      operationId: getExplanation
      responses:
        '200':
          description: Explanation state
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExplanationStatus' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Scans]
      summary: Retry a missing or failed explanation
      description: Does not rerun or alter deterministic safety assessment.
      operationId: retryExplanation
      responses:
        '202':
          description: Explanation job queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExplanationStatus' }
        '409': { $ref: '#/components/responses/Conflict' }

  /scans/{scan_id}/attachments:
    parameters:
      - $ref: '#/components/parameters/ScanId'
    post:
      tags: [Attachments]
      summary: Upload an optional dashboard photograph
      description: |
        Uploads independently from diagnostic evidence. The native app converts the image to
        JPEG; the server strips metadata and stores it outside the public web root. The attachment ID is client-generated.
      operationId: uploadAttachment
      requestBody:
        required: true
        content:
          image/jpeg:
            schema: { type: string, contentEncoding: binary }
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: X-Attachment-Id
          in: header
          required: true
          schema: { type: string, minLength: 8, maxLength: 128 }
      responses:
        '201':
          description: Attachment stored
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AttachmentReceipt' }
        '200':
          description: Matching attachment already existed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AttachmentReceipt' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMedia' }
  /scans/{scan_id}/attachments/{attachment_id}:
    parameters:
      - $ref: '#/components/parameters/ScanId'
      - name: attachment_id
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Attachments]
      summary: Delete an eligible optional photograph
      operationId: deleteAttachment
      responses:
        '204': { description: Attachment deleted or already absent }
        '404': { $ref: '#/components/responses/NotFound' }

  /scans/{scan_id}/service-request:
    parameters:
      - $ref: '#/components/parameters/ScanId'
    post:
      tags: [Service requests]
      summary: Ask ACS for a callback or appointment
      operationId: createServiceRequest
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceRequestInput' }
      responses:
        '201':
          description: Request created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceRequestHandoff' }
        '409': { $ref: '#/components/responses/Conflict' }
  /scans/{scan_id}/share:
    parameters:
      - $ref: '#/components/parameters/ScanId'
    put:
      tags: [Service requests]
      summary: Share or withdraw sharing of a report with ACS
      operationId: setScanSharing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [shared]
              properties:
                shared: { type: boolean }
                consent_version: { type: string, maxLength: 64 }
      responses:
        '200':
          description: Sharing state updated
          content:
            application/json:
              schema:
                type: object
                required: [share_status]
                properties:
                  share_status:
                    enum: [not_shared, customer_shared, withdrawn]

  /staff/step-up:
    post:
      tags: [Staff]
      summary: Obtain a short-lived staff-mode token after PIN or biometric step-up
      operationId: staffStepUp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [method, assertion]
              properties:
                method: { enum: [pin, platform_biometric] }
                assertion: { type: string, minLength: 6, maxLength: 4096 }
      responses:
        '200':
          description: Step-up token issued
          content:
            application/json:
              schema:
                type: object
                required: [staff_token, expires_at]
                properties:
                  staff_token: { type: string }
                  expires_at: { type: string, format: date-time }
        '403': { $ref: '#/components/responses/Forbidden' }
  /staff/queue:
    get:
      tags: [Staff]
      summary: List reports with a valid workshop purpose
      operationId: listStaffQueue
      parameters:
        - $ref: '#/components/parameters/StaffToken'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema: { enum: [needs_review, reviewed, linked_to_job, closed] }
        - name: urgency
          in: query
          schema: { $ref: '#/components/schemas/Urgency' }
      responses:
        '200':
          description: Prioritized queue
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StaffQueuePage' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /staff/scans/{scan_id}:
    parameters:
      - $ref: '#/components/parameters/ScanId'
      - $ref: '#/components/parameters/StaffToken'
    get:
      tags: [Staff]
      summary: Open a shared or purpose-authorized report
      description: Creates an immutable staff-access audit event.
      operationId: getStaffScan
      parameters:
        - name: X-Access-Purpose
          in: header
          required: true
          schema:
            enum: [customer_request, appointment_preparation, urgent_followup, active_job]
      responses:
        '200':
          description: Authorized scan record
          content:
            application/json:
              schema: { $ref: './acs-diagnose-schema.json' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /staff/scans/{scan_id}/review:
    parameters:
      - $ref: '#/components/parameters/ScanId'
      - $ref: '#/components/parameters/StaffToken'
    post:
      tags: [Staff]
      summary: Append a staff review without changing raw evidence
      operationId: reviewScan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [outcome]
              properties:
                outcome: { enum: [reviewed, contact_attempted, customer_contacted, closed] }
                internal_note: { type: string, maxLength: 2000 }
      responses:
        '200':
          description: Review appended
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StaffReview' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /privacy/requests:
    get:
      tags: [Privacy]
      summary: List the customer's privacy requests
      operationId: listPrivacyRequests
      responses:
        '200':
          description: Requests
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: '#/components/schemas/PrivacyRequest' } }
    post:
      tags: [Privacy]
      summary: Request an account export, correction or deletion
      operationId: createPrivacyRequest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [type]
              properties:
                type: { enum: [access_export, correction, account_deletion] }
                details: { type: string, maxLength: 2000 }
      responses:
        '202':
          description: Request accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyRequest' }
  /privacy/requests/{request_id}/download:
    parameters:
      - name: request_id
        in: path
        required: true
        schema: { type: string, minLength: 8, maxLength: 128 }
    get:
      tags: [Privacy]
      summary: Download a completed, unexpired privacy export
      operationId: downloadPrivacyExport
      responses:
        '200':
          description: Customer data export
          headers:
            Content-Disposition: { schema: { type: string } }
          content:
            application/json:
              schema: { type: object }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

components:
  securitySchemes:
    acsBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Short-lived ACS access token issued after identity-provider exchange.
  parameters:
    VehicleId:
      name: vehicle_id
      in: path
      required: true
      schema: { type: string, minLength: 8, maxLength: 128 }
    ScanId:
      name: scan_id
      in: path
      required: true
      schema: { type: string, minLength: 8, maxLength: 128 }
    Cursor:
      name: cursor
      in: query
      schema: { type: string, maxLength: 512 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8, maxLength: 256 }
    StaffToken:
      name: X-Staff-Token
      in: header
      required: true
      schema: { type: string, minLength: 32, maxLength: 4096 }
  responses:
    BadRequest:
      description: Request could not be parsed or validated
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Unauthorized:
      description: Missing, expired or invalid authentication
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Forbidden:
      description: Authenticated but not permitted for this resource or purpose
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    NotFound:
      description: Resource not found or not visible to this account
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Conflict:
      description: State, identity or immutable-hash conflict
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Unprocessable:
      description: Supported request format but unsupported schema or rules version
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    RateLimited:
      description: Too many attempts
      headers:
        Retry-After: { schema: { type: integer, minimum: 1 } }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    PayloadTooLarge:
      description: Request exceeds server limits
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    UnsupportedMedia:
      description: File type is not accepted
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
  schemas:
    Language:
      type: string
      enum: [en, ar]
    Urgency:
      type: string
      enum: [routine, monitor, visit_soon, urgent, stop_driving, inconclusive]
    AuthExchangeRequest:
      type: object
      additionalProperties: false
      required: [provider, identity_token, device]
      properties:
        provider:
          type: string
          description: Configuration name, for example firebase; value finalized later.
          maxLength: 64
        identity_token: { type: string, minLength: 20, maxLength: 8192 }
        device: { $ref: '#/components/schemas/DeviceInfo' }
    DeviceInfo:
      type: object
      additionalProperties: false
      required: [device_id, platform, app_version, os_version]
      properties:
        device_id: { type: string, minLength: 16, maxLength: 256 }
        platform: { enum: [ios, android] }
        app_version: { type: string, maxLength: 64 }
        os_version: { type: string, maxLength: 64 }
        attestation_token: { type: string, maxLength: 8192 }
    AuthSession:
      type: object
      additionalProperties: false
      required: [access_token, refresh_token, token_type, expires_in, account]
      properties:
        access_token: { type: string }
        refresh_token: { type: string }
        token_type: { const: Bearer }
        expires_in: { type: integer, minimum: 60 }
        account: { $ref: '#/components/schemas/Account' }
    Account:
      type: object
      additionalProperties: false
      required: [account_id, mobile_masked, roles, capabilities, preferred_language, onboarding_complete]
      properties:
        account_id: { type: string }
        mobile_masked: { type: string, examples: ['+973 **** 1234'] }
        name: { type: [string, 'null'], maxLength: 120 }
        preferred_language: { $ref: '#/components/schemas/Language' }
        roles:
          type: array
          uniqueItems: true
          items: { enum: [customer, service_adviser, administrator] }
        capabilities:
          type: array
          uniqueItems: true
          items:
            enum: [scan_create, scan_read_own, staff_mode, staff_queue_read, staff_review, staff_admin]
        onboarding_complete: { type: boolean }
    ConsentInput:
      type: object
      additionalProperties: false
      required: [purpose, policy_version, granted]
      properties:
        purpose:
          enum: [terms_and_privacy, diagnostic_processing, photo_processing, cross_border_ai, whatsapp_urgent_contact, optional_analytics]
        policy_version: { type: string, maxLength: 64 }
        granted: { type: boolean }
    Consent:
      allOf:
        - $ref: '#/components/schemas/ConsentInput'
        - type: object
          required: [recorded_at]
          properties:
            recorded_at: { type: string, format: date-time }
            withdrawn_at: { type: [string, 'null'], format: date-time }
    VehicleInput:
      type: object
      additionalProperties: false
      properties:
        registration_plate: { type: string, minLength: 1, maxLength: 32 }
        vin: { type: [string, 'null'], pattern: '^[A-HJ-NPR-Z0-9]{17}$' }
        model: { type: string, minLength: 1, maxLength: 120 }
        model_year: { type: integer, minimum: 1980, maximum: 2100 }
        powertrain: { enum: [petrol, diesel, mild_hybrid, plug_in_hybrid, unknown] }
        odometer_value: { type: number, minimum: 0 }
        odometer_unit: { enum: [km, mi] }
    Vehicle:
      allOf:
        - $ref: '#/components/schemas/VehicleInput'
        - type: object
          required: [vehicle_id, make, coverage_tier, created_at, updated_at]
          properties:
            vehicle_id: { type: string }
            make: { const: Mercedes-Benz }
            coverage_tier: { enum: [unverified, validated, expected, limited, unsupported] }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
    VehiclePage:
      type: object
      required: [items, next_cursor]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/Vehicle' } }
        next_cursor: { type: [string, 'null'] }
    ScanSummary:
      type: object
      additionalProperties: false
      required: [scan_id, vehicle_id, created_at, status, urgency, explanation_state]
      properties:
        scan_id: { type: string }
        vehicle_id: { type: string }
        created_at: { type: string, format: date-time }
        status: { enum: [completed, failed, inconclusive] }
        urgency: { $ref: '#/components/schemas/Urgency' }
        headline: { type: [string, 'null'], maxLength: 120 }
        explanation_state: { enum: [not_requested, pending, ready, failed, unavailable] }
    ScanPage:
      type: object
      required: [items, next_cursor]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/ScanSummary' } }
        next_cursor: { type: [string, 'null'] }
    ScanReceipt:
      type: object
      additionalProperties: false
      required: [scan_id, outcome, server_received_at, explanation_state]
      properties:
        scan_id: { type: string }
        outcome: { enum: [accepted, reconciled] }
        server_received_at: { type: string, format: date-time }
        explanation_state: { enum: [pending, ready, unavailable] }
    ExplanationStatus:
      type: object
      additionalProperties: false
      required: [scan_id, state]
      properties:
        scan_id: { type: string }
        state: { enum: [pending, ready, failed, unavailable] }
        retry_after_seconds: { type: [integer, 'null'], minimum: 1 }
        explanation:
          type: [object, 'null']
          description: Matches the explanation object in the scan record schema.
    AttachmentReceipt:
      type: object
      additionalProperties: false
      required: [attachment_id, state, created_at]
      properties:
        attachment_id: { type: string }
        state: { const: uploaded }
        created_at: { type: string, format: date-time }
    ServiceRequestInput:
      type: object
      additionalProperties: false
      required: [type, preferred_contact]
      properties:
        type: { enum: [callback, appointment] }
        preferred_contact: { enum: [phone, whatsapp] }
        preferred_time_note: { type: [string, 'null'], maxLength: 500 }
        whatsapp_consent_version: { type: [string, 'null'], maxLength: 64 }
    ServiceRequest:
      allOf:
        - $ref: '#/components/schemas/ServiceRequestInput'
        - type: object
          required: [request_id, scan_id, status, created_at]
          properties:
            request_id: { type: string }
            scan_id: { type: string }
            status: { enum: [open, contacted, scheduled, closed, cancelled] }
            created_at: { type: string, format: date-time }
    ServiceRequestHandoff:
      allOf:
        - $ref: '#/components/schemas/ServiceRequest'
        - type: object
          required: [whatsapp_url, whatsapp_number]
          properties:
            whatsapp_url:
              type: string
              format: uri
              pattern: '^https://wa\\.me/97339680866\\?text='
            whatsapp_number: { const: '+97339680866' }
    StaffQueueItem:
      type: object
      additionalProperties: false
      required: [scan_id, customer_display, vehicle_display, urgency, queue_reason, queue_status, created_at]
      properties:
        scan_id: { type: string }
        customer_display: { type: string, maxLength: 120 }
        vehicle_display: { type: string, maxLength: 160 }
        urgency: { $ref: '#/components/schemas/Urgency' }
        queue_reason: { enum: [customer_shared, service_request, active_job, urgent_followup] }
        queue_status: { enum: [needs_review, reviewed, linked_to_job, closed] }
        created_at: { type: string, format: date-time }
    StaffQueuePage:
      type: object
      required: [items, next_cursor]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/StaffQueueItem' } }
        next_cursor: { type: [string, 'null'] }
    StaffReview:
      type: object
      additionalProperties: false
      required: [review_id, scan_id, staff_ref, outcome, created_at]
      properties:
        review_id: { type: string }
        scan_id: { type: string }
        staff_ref: { type: string }
        outcome: { enum: [reviewed, contact_attempted, customer_contacted, closed] }
        internal_note: { type: [string, 'null'], maxLength: 2000 }
        created_at: { type: string, format: date-time }
    PrivacyRequest:
      type: object
      additionalProperties: false
      required: [request_id, type, status, created_at]
      properties:
        request_id: { type: string }
        type: { enum: [access_export, correction, account_deletion, vehicle_deletion, scan_deletion] }
        status: { enum: [received, verifying, processing, completed, declined] }
        created_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
    Problem:
      type: object
      additionalProperties: false
      required: [type, title, status, code, request_id]
      properties:
        type: { type: string, format: uri }
        title: { type: string, maxLength: 200 }
        status: { type: integer, minimum: 400, maximum: 599 }
        code:
          enum:
            - AUTH_TOKEN_INVALID
            - AUTH_SESSION_EXPIRED
            - AUTH_RATE_LIMITED
            - STAFF_STEP_UP_REQUIRED
            - ACCESS_PURPOSE_REQUIRED
            - RESOURCE_NOT_FOUND
            - VALIDATION_FAILED
            - SCHEMA_VERSION_UNSUPPORTED
            - RULES_VERSION_UNSUPPORTED
            - SCAN_HASH_CONFLICT
            - VEHICLE_VIN_CONFLICT
            - IDEMPOTENCY_CONFLICT
            - CONSENT_REQUIRED
            - EXPLANATION_ALREADY_PENDING
            - EXPLANATION_UNAVAILABLE
            - FILE_TOO_LARGE
            - FILE_TYPE_UNSUPPORTED
            - SERVICE_REQUEST_ALREADY_OPEN
            - EXPORT_NOT_READY
            - INTERNAL_ERROR
        detail: { type: string, maxLength: 1000 }
        request_id: { type: string }
        field_errors:
          type: array
          items:
            type: object
            required: [field, message]
            properties:
              field: { type: string }
              message: { type: string }

security:
  - acsBearer: []
