5.4 KiB
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:
{ "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→ 409HAS_USERSif 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.