openapi: 3.1.0
info:
  title: Routely API
  version: "1.0.0"
  description: |
    Dispatch intelligence for field service.
    Every endpoint is idempotent where appropriate, explainable where scored,
    and replayable by weights_version.
  contact:
    name: Routely
    url: https://routely.ai
servers:
  - url: https://api.routely.ai
    description: Production
  - url: http://localhost:3000
    description: Local
security:
  - bearerAuth: []

tags:
  - name: Jobs
  - name: Technicians
  - name: Dispatch
  - name: Prediction
  - name: SLA
  - name: Simulation
  - name: Learning

paths:
  /v1/jobs:
    post:
      tags: [Jobs]
      summary: Create a job
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobInput"
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeJob"
    get:
      tags: [Jobs]
      summary: List jobs
      parameters:
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: date
          schema: { type: string, format: date }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeJobList"

  /v1/technicians:
    post:
      tags: [Technicians]
      summary: Create a technician
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TechnicianInput"
      responses:
        "200":
          description: Created
    get:
      tags: [Technicians]
      summary: List technicians
      responses:
        "200":
          description: OK

  /v1/dispatch/assign:
    post:
      tags: [Dispatch]
      summary: Score and assign a job
      description: |
        Returns ranked technicians with per-factor scores, predicted duration,
        and the weights_version that produced the ranking. Every event is
        persisted for audit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobId]
              properties:
                jobId: { type: string }
                commit:
                  type: boolean
                  description: If true, persist the winner to the job row.
                  default: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeAssignment"

  /v1/dispatch/emergency:
    post:
      tags: [Dispatch]
      summary: Reroute the board for a priority-one job
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobId]
              properties:
                jobId: { type: string }
      responses:
        "200":
          description: Rerouted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data:
                    type: object
                    properties:
                      winner: { $ref: "#/components/schemas/AssignmentScore" }
                      displaced:
                        type: array
                        items: { type: string }
                      elapsedMs: { type: integer }

  /v1/dispatch/schedule:
    get:
      tags: [Dispatch]
      summary: Get schedules
      parameters:
        - in: query
          name: date
          schema: { type: string, format: date }
        - in: query
          name: technicianId
          schema: { type: string }
      responses:
        "200":
          description: OK

  /v1/prediction/duration:
    post:
      tags: [Prediction]
      summary: Predict job duration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [skill]
              properties:
                skill: { type: string }
                technicianId: { type: string, nullable: true }
                jobEstimateMinutes: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data:
                    type: object
                    properties:
                      minutes: { type: number }
                      source: { type: string, enum: [tech_prior, skill_prior, fallback] }
                      sampleSize: { type: integer }

  /v1/sla/at-risk:
    get:
      tags: [SLA]
      summary: List at-risk jobs
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/SlaAlert" }

  /v1/simulation/run:
    post:
      tags: [Simulation]
      summary: Run a what-if scenario
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [scenario]
              properties:
                scenario:
                  type: object
                  properties:
                    addTechnicians: { type: integer }
                    shiftWindows:
                      type: object
                      properties:
                        startDelta: { type: integer }
                        endDelta: { type: integer }
                    closeTerritory:
                      type: object
                      properties:
                        latMin: { type: number }
                        latMax: { type: number }
                        lngMin: { type: number }
                        lngMax: { type: number }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  data:
                    type: object
                    properties:
                      baseline:
                        $ref: "#/components/schemas/ScenarioMetrics"
                      projected:
                        $ref: "#/components/schemas/ScenarioMetrics"
                      delta:
                        $ref: "#/components/schemas/ScenarioMetrics"

  /v1/learning/run:
    post:
      tags: [Learning]
      summary: Retrain scoring weights
      description: Admin-only. Writes a new weights version and persists history.
      security:
        - adminAuth: []
      responses:
        "200":
          description: OK

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ROUTELY_API_KEY
    adminAuth:
      type: http
      scheme: bearer
      bearerFormat: ROUTELY_ADMIN_KEY

  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      schema: { type: string }
      required: false

  schemas:
    JobInput:
      type: object
      required: [customerName, address, lat, lng, requiredSkills, windowStart, windowEnd, estimatedMinutes]
      properties:
        customerName: { type: string }
        address: { type: string }
        lat: { type: number }
        lng: { type: number }
        priority: { type: integer, minimum: 1, maximum: 5, default: 3 }
        requiredSkills:
          type: array
          items: { type: string }
        windowStart: { type: string, format: date-time }
        windowEnd: { type: string, format: date-time }
        estimatedMinutes: { type: integer }
        revenueCents: { type: integer }
        externalRef: { type: string }

    TechnicianInput:
      type: object
      required: [fullName, homeLat, homeLng, skills, shiftStart, shiftEnd]
      properties:
        fullName: { type: string }
        homeLat: { type: number }
        homeLng: { type: number }
        skills:
          type: array
          items: { type: string }
        shiftStart: { type: string, example: "08:00" }
        shiftEnd: { type: string, example: "17:00" }

    AssignmentScore:
      type: object
      properties:
        technicianId: { type: string }
        score: { type: number }
        rank: { type: integer }
        reason:
          type: object
          properties:
            skill_match: { type: number }
            distance_score: { type: number }
            priority_weight: { type: number }
            performance: { type: number }
            availability: { type: number }
            predicted_duration_minutes: { type: number }
            delay_risk: { type: number }
        weightsVersion: { type: integer }

    SlaAlert:
      type: object
      properties:
        id: { type: string }
        jobId: { type: string }
        riskScore: { type: number }
        projectedMinutesLate: { type: integer }
        resolved: { type: boolean }

    ScenarioMetrics:
      type: object
      properties:
        jobsCompleted: { type: integer }
        driveMinutes: { type: integer }
        onTimeRate: { type: number }
        revenueCents: { type: integer }

    EnvelopeJob:
      type: object
      properties:
        ok: { type: boolean }
        data: { $ref: "#/components/schemas/JobInput" }

    EnvelopeJobList:
      type: object
      properties:
        ok: { type: boolean }
        data:
          type: array
          items: { $ref: "#/components/schemas/JobInput" }

    EnvelopeAssignment:
      type: object
      properties:
        ok: { type: boolean }
        data:
          type: object
          properties:
            assignments:
              type: array
              items: { $ref: "#/components/schemas/AssignmentScore" }
