Files
OG/docs/05-api-spec.md
T
2026-07-30 14:35:52 +08:00

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 → 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.