{
  "openapi": "3.1.0",
  "info": {
    "title": "Vigotime Planner API",
    "version": "1.0.0",
    "description": "Stateless scheduling API (API-first / headless). Send a self-contained payload (employees, shifts, rules, absences) and receive a finished schedule — no knowledge of Vigotime's internal storage required. Input data is never stored; you manage it in your own system.\n\nProcessing is asynchronous: submit a job, poll its status (live progress), then fetch the result — or supply a webhook to be notified when it finishes.\n\nJobs are persisted per tenant for 12 months and are isolated: you only ever see your own jobs (`GET /api/v1/solve`).",
    "contact": { "name": "Vigotime", "url": "https://vigotime.de" }
  },
  "servers": [
    { "url": "https://planner.vigotime.com", "description": "Production" },
    { "url": "https://staging-planner.vigotime.com", "description": "Staging" },
    { "url": "http://localhost:8000", "description": "Local development" }
  ],
  "security": [{ "ApiKeyAuth": [] }, { "BearerAuth": [] }],
  "tags": [{ "name": "Stateless Solve", "description": "Run the planner on a self-contained payload." }],
  "paths": {
    "/api/v1/solve": {
      "post": {
        "tags": ["Stateless Solve"],
        "summary": "Submit a stateless solve job",
        "description": "Accepts a self-contained payload and starts an asynchronous solve. The problem size (employees × days × shifts) and solver time are capped per tier. Returns a job id to poll.",
        "operationId": "submitSolve",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveRequest" } } }
        },
        "responses": {
          "202": {
            "description": "Job accepted",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveSubmitResponse" } } }
          },
          "401": { "description": "Missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Problem size exceeds the tier limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded for the tier", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "tags": ["Stateless Solve"],
        "summary": "List the caller's solve jobs",
        "description": "Returns the tenant's own solve jobs (newest first), persisted for 12 months. Multi-tenant: only the caller's jobs are returned.",
        "operationId": "listSolveJobs",
        "parameters": [{ "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "default": 50, "maximum": 200 } }],
        "responses": {
          "200": { "description": "The caller's jobs", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveListResponse" } } } }
        }
      }
    },
    "/api/v1/solve/stats": {
      "get": {
        "tags": ["Stateless Solve"],
        "summary": "Usage statistics for the caller",
        "description": "Aggregated usage of the caller's tenant: plans solved/unsolvable/failed, tokens burned and total solver time. Basis for token-based pricing.",
        "operationId": "getSolveStats",
        "responses": {
          "200": { "description": "Usage statistics", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveStatsResponse" } } } }
        }
      }
    },
    "/api/v1/solve/{job_id}": {
      "get": {
        "tags": ["Stateless Solve"],
        "summary": "Get solve job status",
        "operationId": "getSolveStatus",
        "parameters": [{ "name": "job_id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Job status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveStatusResponse" } } } },
          "404": { "description": "Job not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/solve/{job_id}/result": {
      "get": {
        "tags": ["Stateless Solve"],
        "summary": "Get solve job result",
        "description": "Returns the schedule once the job is `completed` (or `failed`). Returns 400 while the job is still running.",
        "operationId": "getSolveResult",
        "parameters": [{ "name": "job_id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Solve result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveResult" } } } },
          "400": { "description": "Job not finished yet", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Job not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "Per-integrator API key. Determines the tier/package and its limits." },
      "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Firebase ID token (internal use)." }
    },
    "schemas": {
      "SolveRequest": {
        "type": "object",
        "required": ["start_date", "end_date", "employees", "shifts", "department"],
        "properties": {
          "start_date": { "type": "string", "format": "date", "description": "First day of the planning window (inclusive)", "example": "2026-07-01" },
          "end_date": { "type": "string", "format": "date", "description": "Last day of the planning window (inclusive)", "example": "2026-07-07" },
          "department": { "type": "object", "description": "Department in Firestore-document shape", "example": { "id": "d1", "name": "Pflege Station 1" } },
          "employees": {
            "type": "array",
            "description": "Employees in Firestore-document shape (camelCase). `id` is required.",
            "items": { "type": "object" },
            "example": [{ "id": "e1", "primaryDepartmentId": "d1", "firstName": "Mara", "contractHoursPerWeek": 40, "qualificationIds": ["q_exam"] }]
          },
          "shifts": {
            "type": "array",
            "description": "Shift definitions in Firestore-document shape. `id` is required.",
            "items": { "type": "object" },
            "example": [{ "id": "s_frueh", "departmentId": "d1", "name": "Frühdienst", "startTime": "06:00", "endTime": "14:00", "shiftType": "early", "minStaff": 1 }]
          },
          "wishes": { "type": "object", "nullable": true, "description": "employee_id → ISO date strings", "additionalProperties": { "type": "array", "items": { "type": "string", "format": "date" } } },
          "vacations": { "type": "object", "nullable": true, "additionalProperties": { "type": "array", "items": { "type": "string", "format": "date" } } },
          "sick_leaves": { "type": "object", "nullable": true, "additionalProperties": { "type": "array", "items": { "type": "string", "format": "date" } } },
          "qualifications": { "type": "object", "nullable": true, "description": "employee_id → qualification ids; derived from employees if omitted", "additionalProperties": { "type": "array", "items": { "type": "string" } } },
          "preferences": { "type": "object", "nullable": true },
          "hourly_rates": { "type": "object", "nullable": true, "additionalProperties": { "type": "number" } },
          "planning_rules": { "type": "array", "nullable": true, "items": { "type": "object" } },
          "staffing_rules": { "type": "array", "nullable": true, "items": { "type": "object" } },
          "workplaces": { "type": "array", "nullable": true, "items": { "type": "object" } },
          "area_groups": { "type": "array", "nullable": true, "items": { "type": "object" } },
          "config": { "$ref": "#/components/schemas/SolveConfig" },
          "webhook_url": { "type": "string", "format": "uri", "nullable": true, "description": "Optional https URL POSTed once the solve finishes (completed or failed).", "example": "https://hooks.example.com/vigotime" },
          "webhook_secret": { "type": "string", "nullable": true, "description": "If set, the webhook carries an HMAC-SHA256 signature in the X-Vigotime-Signature header." }
        }
      },
      "SolveConfig": {
        "type": "object",
        "description": "Solver configuration. Solver time is hard-capped at the tier limit regardless of this value.",
        "properties": {
          "timeout_seconds": { "type": "integer", "example": 30 },
          "max_solutions": { "type": "integer", "example": 1 },
          "num_workers": { "type": "integer", "example": 0 },
          "enabled_constraints": { "type": "array", "nullable": true, "items": { "type": "string" } },
          "enabled_objectives": { "type": "array", "nullable": true, "items": { "type": "string" } },
          "objective_weights": { "type": "object", "nullable": true, "additionalProperties": { "type": "integer" } }
        }
      },
      "SolveSubmitResponse": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string", "example": "f1c2a0d4-…" },
          "status": { "type": "string", "example": "pending" },
          "tier": { "type": "string", "example": "pro" }
        }
      },
      "SolveStatusResponse": {
        "type": "object",
        "description": "Queryable during the run (with live progress) and after completion.",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "running", "completed", "failed"] },
          "tier": { "type": "string", "nullable": true },
          "token_cost": { "type": "integer", "description": "Tokens this plan costs" },
          "created_at": { "type": "string", "nullable": true },
          "completed_at": { "type": "string", "nullable": true },
          "progress": { "type": "object", "nullable": true, "example": { "percent": 64.0, "solutions_found": 2, "status": "improving" } },
          "result_available": { "type": "boolean" },
          "error_message": { "type": "string", "nullable": true },
          "input_summary": { "type": "object", "nullable": true, "example": { "employees": 2, "shifts": 2, "days": 7 } },
          "webhook": { "$ref": "#/components/schemas/WebhookStatus" }
        }
      },
      "WebhookStatus": {
        "type": "object",
        "nullable": true,
        "properties": {
          "url": { "type": "string", "nullable": true },
          "configured": { "type": "boolean" },
          "delivered": { "type": "boolean" },
          "attempts": { "type": "integer" },
          "last_status_code": { "type": "integer", "nullable": true },
          "error": { "type": "string", "nullable": true }
        }
      },
      "SolveJobSummary": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string" },
          "tier": { "type": "string", "nullable": true },
          "token_cost": { "type": "integer" },
          "created_at": { "type": "string", "nullable": true },
          "completed_at": { "type": "string", "nullable": true },
          "result_available": { "type": "boolean" }
        }
      },
      "SolveListResponse": {
        "type": "object",
        "properties": {
          "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/SolveJobSummary" } }
        }
      },
      "SolveStatsResponse": {
        "type": "object",
        "description": "Tenant usage statistics — basis for token-based pricing.",
        "properties": {
          "total": { "type": "integer" },
          "solved": { "type": "integer", "description": "Plans with a solution (optimal/feasible)" },
          "unsolvable": { "type": "integer", "description": "Infeasible plans" },
          "failed": { "type": "integer", "description": "Solver errors" },
          "in_progress": { "type": "integer" },
          "tokens_burned": { "type": "integer" },
          "execution_time_seconds_total": { "type": "number" }
        }
      },
      "SolveResult": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "status": { "type": "string", "enum": ["optimal", "feasible", "infeasible", "unknown", "model_invalid"] },
          "assignments": { "type": "array", "items": { "$ref": "#/components/schemas/Assignment" } },
          "unassigned_shifts": { "type": "array", "items": { "$ref": "#/components/schemas/UnassignedShift" } },
          "execution_time_seconds": { "type": "number", "example": 0.8 },
          "statistics": { "type": "object" },
          "error_message": { "type": "string", "nullable": true }
        }
      },
      "Assignment": {
        "type": "object",
        "properties": {
          "employee_id": { "type": "string", "example": "e1" },
          "shift_id": { "type": "string", "example": "s_frueh" },
          "date": { "type": "string", "format": "date", "example": "2026-07-01" }
        }
      },
      "UnassignedShift": {
        "type": "object",
        "properties": {
          "shift_id": { "type": "string" },
          "date": { "type": "string", "format": "date" },
          "reason": { "type": "string", "example": "no_qualified_staff" },
          "details": { "type": "object" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "detail": { "type": "string", "description": "Human-readable error message" }
        }
      }
    }
  }
}
