{
  "openapi": "3.1.0",
  "info": {
    "title": "SupplierDev API",
    "version": "1.0.0",
    "description": "Public v1 API surface for the SupplierDev tenant runtime. This document is reality-based: every path here corresponds to an existing route handler under `app/api/v1/`. Internal routes (`/api/admin/*`, `/api/owner/*`, `/api/demo/*`) are NOT covered by this contract — see `docs/foundation/api-contract.md`.",
    "contact": {
      "name": "Platform Engineering",
      "url": "https://github.com/anthropics/claude-code/issues"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "Tenant runtime (Next.js App Router)"
    }
  ],
  "tags": [
    { "name": "Health", "description": "Liveness + readiness signals." },
    { "name": "Master Data", "description": "Reference values and lookup tables." },
    { "name": "Suppliers", "description": "Supplier master data records." },
    { "name": "Projects", "description": "Visit / project lifecycle." },
    { "name": "OEE", "description": "Overall Equipment Effectiveness records." },
    { "name": "Assignments", "description": "Consultant scheduling assignments." }
  ],
  "components": {
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "sb-access-token",
        "description": "Supabase session cookie. The middleware (`lib/supabase/proxy.ts`) refreshes the session via `getClaims()` on every request."
      },
      "supportSession": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Support-Session-Id",
        "description": "Time-limited support session created from the owner portal. Bypasses normal tenant-status checks for debugging only. Validated by `lib/support`."
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message." },
          "code": {
            "type": "string",
            "enum": [
              "unauthorized",
              "forbidden",
              "not_found",
              "invalid_request",
              "tenant_disabled",
              "plan_missing",
              "role_denied",
              "kill_switch",
              "service_misconfigured",
              "internal_error"
            ],
            "description": "Stable machine-readable code. Optional for now; required for new routes."
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional structured detail."
          },
          "status": {
            "type": "string",
            "description": "Tenant lifecycle status when error.code = 'tenant_disabled'. Mirrors cp_tenants.status."
          }
        }
      },
      "ListMeta": {
        "type": "object",
        "required": ["page", "limit", "total"],
        "properties": {
          "page": { "type": "integer", "minimum": 1, "description": "1-based page number." },
          "limit": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Page size; clamped to [1, 100]." },
          "total": { "type": "integer", "minimum": 0, "description": "Total matching records." }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": ["status", "timestamp", "version", "environment"],
        "properties": {
          "status": { "type": "string", "enum": ["healthy", "degraded"] },
          "timestamp": { "type": "string", "format": "date-time" },
          "version": { "type": "string", "description": "Application semantic version." },
          "environment": { "type": "string", "enum": ["development", "staging", "production"] }
        }
      },
      "MasterDataValue": {
        "type": "object",
        "required": ["id", "type_id", "code", "label", "sort_order", "is_active"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "type_id": { "type": "string", "format": "uuid" },
          "code": { "type": "string" },
          "label": { "type": "string" },
          "normalized_label": { "type": "string" },
          "sort_order": { "type": "integer" },
          "is_active": { "type": "boolean" },
          "is_hidden": { "type": "boolean" },
          "valid_from": { "type": ["string", "null"], "format": "date-time" },
          "valid_to": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Supplier": {
        "type": "object",
        "description": "Full supplier_master_data row. Schema mirrors the table verbatim — see MO-25/repository-phase1.sql.",
        "required": ["id", "supplier_number", "supplier_name", "is_active", "created_at", "updated_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "supplier_number": { "type": "string", "description": "Globally unique business identifier (NOT NULL UNIQUE)." },
          "supplier_name": { "type": "string" },
          "supplier_location": { "type": ["string", "null"] },
          "plant": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"] },
          "city": { "type": ["string", "null"] },
          "is_active": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "created_by": { "type": ["string", "null"], "format": "uuid" },
          "updated_by": { "type": ["string", "null"], "format": "uuid" }
        },
        "additionalProperties": false
      },
      "SupplierCreate": {
        "type": "object",
        "description": "POST /suppliers body. supplier_name is required at the route layer; supplier_number is required at the DB layer (UNIQUE NOT NULL) — clients SHOULD provide both.",
        "required": ["supplier_name"],
        "properties": {
          "supplier_name": { "type": "string", "minLength": 1 },
          "supplier_number": { "type": "string", "description": "Required by DB constraint; omitting will surface as a 500 from the route handler." },
          "supplier_location": { "type": "string" },
          "plant": { "type": "string" },
          "country": { "type": "string" },
          "city": { "type": "string" },
          "is_active": { "type": "boolean", "default": true }
        },
        "additionalProperties": false
      },
      "Project": {
        "type": "object",
        "description": "Project row. GET /projects/{id} returns the thin shape enumerated below (selected explicitly). PATCH /projects/{id} and POST /projects return the FULL projects row, which carries 25+ MO-24 columns (project_code, status_id, planned_start, etc.) — these are intentionally not enumerated here, so additionalProperties stays open. Tighten only after MO-24 stabilises and the PATCH/POST projection is locked. As of Phase 2b3-ii-d, GET output uses project_type_assignments as the source of truth for project type. project_type_codes carries the full assigned set; project_type_id is a backward-compatibility field derived from the primary junction type via the master code→id lookup.",
        "required": ["id", "user_id", "supplier_id", "created_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "created_at": { "type": "string", "format": "date-time" },
          "user_id": { "type": "string", "format": "uuid" },
          "project_type_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "Compatibility field. Derived from project_type_assignments primary type. Use project_type_codes for the full set."
          },
          "project_type_codes": {
            "type": "array",
            "items": { "type": "string" },
            "description": "All assigned project type codes from project_type_assignments. Single source of truth for project type since Phase 2b3."
          },
          "supplier_id": { "type": "string", "format": "uuid" },
          "project_types": {
            "type": ["object", "null"],
            "deprecated": true,
            "description": "Deprecated since Phase 2b3-ii-d. No longer emitted by GET /projects or GET /projects/{id}. Use project_type_codes instead.",
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "code": { "type": "string" },
              "label": { "type": "string" }
            }
          },
          "project_statuses": {
            "type": ["object", "null"],
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "code": { "type": "string" },
              "label": { "type": "string" }
            }
          },
          "supplier_master_data": {
            "type": ["object", "null"],
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "supplier_name": { "type": "string" },
              "city": { "type": ["string", "null"] }
            }
          }
        },
        "additionalProperties": true
      },
      "ProjectCreate": {
        "type": "object",
        "required": ["supplier_id"],
        "properties": {
          "project_type_id": {
            "type": "string",
            "format": "uuid",
            "description": "Compatibility input. Translated server-side into a single project_type_assignments row via the project_types code lookup. Only IDs of active project types (project_types.is_active = true) are accepted. Used only when project_type_codes is not present in the request body — once project_type_codes is supplied, it wins regardless of value."
          },
          "project_type_codes": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Junction-first input. Each code is written as a project_type_assignments row. When the field is present in the request body it wins over project_type_id, even if invalid. The array must contain at least one non-empty code, and every code must match an active project_types.code; otherwise 400 is returned before any project is inserted."
          },
          "supplier_id": { "type": "string", "format": "uuid" }
        },
        "additionalProperties": true,
        "description": "user_id is derived from the authenticated session; immutable thereafter. Phase 2b3-iii: projects.project_type_id column is no longer written; assigned types are persisted to project_type_assignments only."
      },
      "ProjectPatch": {
        "type": "object",
        "additionalProperties": true,
        "description": "Partial update. Server strips immutable fields (id, user_id, created_at) before persisting."
      },
      "OeeRecord": {
        "type": "object",
        "description": "Reflects what the route handler currently selects/exposes. KNOWN ISSUE: the route references `week_number`, but the DB column is `calendar_week` (MO-OEE/oee-phase1.sql). Until the route is fixed, the field name documented here matches the route — see docs/foundation/api-surface.md and follow-ups.md. additionalProperties stays open because the OEE record table carries computed/derived fields not yet pinned.",
        "required": ["id", "line_name", "year", "week_number"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "line_name": { "type": "string" },
          "year": { "type": "integer" },
          "week_number": { "type": "integer", "minimum": 1, "maximum": 53 },
          "availability": { "type": ["number", "null"] },
          "performance": { "type": ["number", "null"] },
          "quality": { "type": ["number", "null"] },
          "oee": { "type": ["number", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "additionalProperties": true
      },
      "OeeRecordCreate": {
        "type": "object",
        "required": ["line_name", "year", "week_number"],
        "properties": {
          "line_name": { "type": "string", "minLength": 1 },
          "year": { "type": "integer" },
          "week_number": { "type": "integer", "minimum": 1, "maximum": 53 }
        },
        "additionalProperties": true
      },
      "Assignment": {
        "type": "object",
        "description": "Reflects what the route handler currently selects. KNOWN ISSUES: (1) the route filters on `start_date`/`end_date` but the DB carries a single `date` column (MO-23/planning-tables.sql); (2) the route joins `planning_projects` via a non-existent `planning_project_id` column. Schema kept loose (additionalProperties: true) until MO-23 reconciliation lands — see docs/foundation/api-surface.md and follow-ups.md.",
        "required": ["id", "consultant_id", "start_date", "end_date"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "consultant_id": { "type": "string", "format": "uuid" },
          "planning_project_id": { "type": ["string", "null"], "format": "uuid" },
          "start_date": { "type": "string", "format": "date" },
          "end_date": { "type": "string", "format": "date" },
          "consultants": {
            "type": ["object", "null"],
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "display_name": { "type": "string" }
            }
          },
          "planning_projects": {
            "type": ["object", "null"],
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "title": { "type": "string" }
            }
          }
        },
        "additionalProperties": true
      }
    },
    "parameters": {
      "PageQuery": {
        "name": "page",
        "in": "query",
        "required": false,
        "schema": { "type": "integer", "minimum": 1, "default": 1 }
      },
      "LimitQuery": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid session.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      },
      "Forbidden": {
        "description": "Tenant disabled, plan missing, role denied, or kill-switch active.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      },
      "BadRequest": {
        "description": "Malformed request or missing required fields.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      },
      "Misconfigured": {
        "description": "Service misconfigured (e.g., TENANT_ID missing in production).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      }
    }
  },
  "security": [
    { "cookieAuth": [] }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": ["Health"],
        "summary": "Liveness probe",
        "description": "Returns 200 always; status field reflects database reachability. Safe to call without authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service responding (status may be `degraded` if dependent services failed).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthResponse" }
              }
            }
          }
        }
      }
    },
    "/master-data": {
      "get": {
        "tags": ["Master Data"],
        "summary": "List master-data values for a type",
        "parameters": [
          { "name": "type", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The master_data_types.code to query." },
          { "name": "active_only", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Filter is_active = true." }
        ],
        "responses": {
          "200": {
            "description": "Values for the requested type.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/MasterDataValue" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/suppliers": {
      "get": {
        "tags": ["Suppliers"],
        "summary": "Search active suppliers",
        "parameters": [
          { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Substring filter on supplier_name, supplier_number, city." }
        ],
        "responses": {
          "200": {
            "description": "Up to 20 active suppliers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Supplier" } },
                    "meta": {
                      "type": "object",
                      "required": ["total"],
                      "properties": { "total": { "type": "integer" } }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "post": {
        "tags": ["Suppliers"],
        "summary": "Create a supplier",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/SupplierCreate" } }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/Supplier" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/projects": {
      "get": {
        "tags": ["Projects"],
        "summary": "List projects (paginated)",
        "parameters": [
          { "$ref": "#/components/parameters/PageQuery" },
          { "$ref": "#/components/parameters/LimitQuery" },
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by project_statuses.code." },
          { "name": "type", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by project_types.code." }
        ],
        "responses": {
          "200": {
            "description": "Paginated projects.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } },
                    "meta": { "$ref": "#/components/schemas/ListMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "503": { "$ref": "#/components/responses/Misconfigured" }
        }
      },
      "post": {
        "tags": ["Projects"],
        "summary": "Create a project",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectCreate" } } }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/Project" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/projects/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
      ],
      "get": {
        "tags": ["Projects"],
        "summary": "Get a project by id",
        "responses": {
          "200": {
            "description": "Found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/Project" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "tags": ["Projects"],
        "summary": "Partially update a project",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectPatch" } } }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/Project" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "delete": {
        "tags": ["Projects"],
        "summary": "Delete a project",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success"],
                  "properties": { "success": { "type": "boolean", "const": true } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/oee": {
      "get": {
        "tags": ["OEE"],
        "summary": "List OEE records (paginated)",
        "parameters": [
          { "$ref": "#/components/parameters/PageQuery" },
          { "$ref": "#/components/parameters/LimitQuery" },
          { "name": "line", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Substring filter on line_name." },
          { "name": "year", "in": "query", "required": false, "schema": { "type": "integer" } },
          { "name": "week_from", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 53 } },
          { "name": "week_to", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 53 } }
        ],
        "responses": {
          "200": {
            "description": "Paginated OEE records.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/OeeRecord" } },
                    "meta": { "$ref": "#/components/schemas/ListMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "503": { "$ref": "#/components/responses/Misconfigured" }
        }
      },
      "post": {
        "tags": ["OEE"],
        "summary": "Create an OEE record",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OeeRecordCreate" } } }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/OeeRecord" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/assignments": {
      "get": {
        "tags": ["Assignments"],
        "summary": "List planning assignments (paginated)",
        "parameters": [
          { "$ref": "#/components/parameters/PageQuery" },
          { "$ref": "#/components/parameters/LimitQuery" },
          { "name": "consultant_id", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" } },
          { "name": "date_from", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } },
          { "name": "date_to", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } }
        ],
        "responses": {
          "200": {
            "description": "Paginated assignments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Assignment" } },
                    "meta": { "$ref": "#/components/schemas/ListMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "503": { "$ref": "#/components/responses/Misconfigured" }
        }
      }
    }
  }
}
