Add spec pack

This commit is contained in:
Weei Han
2026-07-30 14:35:52 +08:00
commit 641fbdadeb
9 changed files with 803 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
# API Specification
## SOP Governance Tool — Stage 1 (Next.js Route Handlers)
Conventions: JSON in/out · session cookie auth (Supabase) verified server-side on every route · errors as `{ "error": { "code": "FORBIDDEN", "message": "…" } }` with proper HTTP status · every mutation writes `audit_log` in the same transaction.
Plain reads (lists, dashboard) may query Supabase directly from server components under RLS; the routes below are the **mutations and AI calls** — the state machine lives here and only here (`lib/workflow.ts`).
## 1. SOPs
### POST /api/sops — create draft (editor+)
Req: `{ "code":"WH-PICK-001", "title":"Order Picking", "department_id":"…", "category":"Warehouse", "review_months":12 }`
Res 201: `{ "id":"…", "status":"draft" }`
409 if code exists.
### PATCH /api/sops/:id — update draft content/meta (editor+; only when status = draft)
Req: `{ "draft_content": { …sections… }, "title?":"…" }` → Res 200 `{ "ok":true, "saved_at":"…" }`
409 `NOT_EDITABLE` if not draft.
### POST /api/sops/:id/submit — draft → submitted (editor+)
Validates mandatory sections (purpose, ≥1 step). Res 200 `{ "status":"submitted" }`
422 `VALIDATION` listing missing sections.
### POST /api/sops/:id/decide — approve/reject (approver/admin)
Req: `{ "decision":"approved" | "rejected", "comment":"…" }` (comment required on reject)
approved → status `approved`; rejected → status `draft`. Writes `approvals` row.
Res 200 `{ "status":"approved" }`.
### POST /api/sops/:id/publish — approved → published (admin)
Req: `{ "change_note":"Initial release", "is_major":false }`
Logic (one transaction): compute next label via `nextVersion(current, is_major)` (none→1.0, 1.0+minor→1.1, +major→2.0) → insert `sop_versions` snapshot of `draft_content` → set `sops.current_version_id`, `status='published'`, `published_at` → mark existing translations of prior version stale (no-op flag; new version simply has none) → audit.
Res 200 `{ "version_label":"1.0", "version_id":"…" }`.
### POST /api/sops/:id/assign — set departments (admin)
Req: `{ "department_ids": ["…","…"] }` (replaces the set)
Res 200 `{ "assigned_departments": 2 }`.
## 2. Acknowledgement
### POST /api/ack (staff, self only)
Req: `{ "sop_version_id":"…", "language_viewed":"ms", "typed_name":"Ahmad bin Ali" }`
Checks: user's department is assigned to the SOP; version is the current one; typed_name non-empty.
Res 201 `{ "acknowledged_at":"…" }` · 409 `ALREADY_ACKNOWLEDGED` · 403 if not an assignee.
### GET /api/sops/:id/ack-report (approver/admin)
Res 200:
```json
{ "sop": { "code":"WH-PICK-001", "title":"…", "version":"1.2" },
"summary": { "assignees": 24, "acknowledged": 21, "pct": 87.5 },
"rows": [ { "name":"Ahmad", "department":"Outbound",
"status":"acknowledged", "version":"1.2",
"language":"ms", "at":"2026-07-01T02:11:00Z" },
{ "name":"Ravi", "department":"Outbound", "status":"outstanding" } ] }
```
`?format=csv` streams CSV of the same.
## 3. AI
### POST /api/ai/draft (editor+, rate-limited 20/day)
Req: `{ "description":"How we pick e-commerce orders…", "notes?":"pasted messy notes" }`
Server: prompt template → Anthropic → parse strict JSON (retry once) → log `ai_log`.
Res 200: `{ "draft": { "purpose":"…", "scope":"…", "roles":"…", "steps":[{"order":1,"text":"…"}], "exceptions":"…", "safety":"…", "records":"…" } }`
Client fills the editor; **user must Save** — the route never writes to `sops`.
429 `RATE_LIMIT` · 502 `AI_FAILED` (input preserved client-side).
### POST /api/ai/translate (editor+)
Req: `{ "sop_version_id":"…", "languages":["ms","zh"] }`
Res 200: `{ "created":[ {"language":"ms","translation_id":"…"} ] }` (upserts `sop_translations`, `machine=true`).
### POST /api/translations/:id/review (editor+) — human sign-off
Res 200 `{ "reviewed": true }`. Staff see safety sections in a translation only when reviewed (FRD-6.2).
## 4. Users & Departments (admin)
- `POST /api/users` `{ email, full_name, role, department_id, preferred_language }` → creates auth user (invite email) + profile.
- `PATCH /api/users/:id` `{ role?, department_id?, active?, preferred_language? }`
- `POST /api/departments` `{ name }` · `PATCH /api/departments/:id` `{ name }`
- `DELETE /api/departments/:id` → 409 `HAS_USERS` if occupied.
## 5. Incidents
### POST /api/incidents (any authenticated role)
Req: `{ "description":"Pallet label mismatch on inbound", "severity":"high", "sop_id?":"…", "sop_version_id?":"…", "photo_path?":"sop-photos/…" }`
Server sets reporter_id + department from the session profile. Res 201 `{ "id":"…", "status":"open" }` · 422 if description empty.
### PATCH /api/incidents/:id (approver/admin)
Req: `{ "status":"reviewed" | "closed", "resolution_note?":"Racking relabeled; SOP WH-INB-002 updated" }`
Transitions: open→reviewed→closed only; closed is immutable (409 `CLOSED`). Writes audit (`incident.reviewed` / `incident.closed`). Res 200 `{ "status":"closed" }`.
Reads (incident list, filters, per-SOP tab) go through RLS-scoped queries in server components — staff see only their own reports; approver/admin see all.
## 6. Status Codes Summary
200/201 success · 400 malformed · 401 no session · 403 wrong role / not assignee · 404 not found in org · 409 state conflict (duplicate code, not editable, already acknowledged) · 422 validation with field list · 429 AI rate limit · 500/502 server or AI failure.