openapi: "3.0.3" info: title: "ADR-021: Workflow Transition with Step-specific Attachments" version: "1.8.6" description: | Extended Workflow Engine API contracts for ADR-021. - POST /instances/:id/transition — extended with attachmentPublicIds - GET /instances/:id/history — new endpoint returning history with attachments per step servers: - url: "/api/workflow-engine" description: "NAP-DMS Backend API" security: - BearerAuth: [] components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT headers: IdempotencyKey: description: | UUIDv7 idempotency token. If the same key+userId is received within 24h, the cached response is returned without re-processing. (ADR-021 §5.1) schema: type: string format: uuid required: true schemas: WorkflowTransitionRequest: type: object required: - action properties: action: type: string description: "Action ที่ต้องการทำ — ต้องตรงกับ DSL ของ Workflow นี้" example: "APPROVE" enum: [APPROVE, REJECT, RETURN, ACKNOWLEDGE] comment: type: string description: "ความเห็นประกอบการอนุมัติ" example: "อนุมัติแล้ว ดำเนินการต่อได้เลย" nullable: true payload: type: object description: "ข้อมูลเพิ่มเติมสำหรับ DSL Context" additionalProperties: true nullable: true example: urgent: true attachmentPublicIds: type: array description: | รายการ publicId (UUIDv7) ของไฟล์แนบที่ต้องการผูกกับขั้นตอนนี้. ไฟล์ต้องผ่าน Two-Phase Upload ก่อน (is_temporary = false, ClamAV passed). ADR-019: ใช้ publicId เท่านั้น — ห้ามใช้ INT id. items: type: string format: uuid example: "019505a1-7c3e-7000-8000-abc123def456" maxItems: 20 nullable: true WorkflowTransitionResponse: type: object properties: success: type: boolean example: true nextState: type: string description: "สถานะใหม่หลัง Transition" example: "APPROVED" historyId: type: string format: uuid description: "ID ของ WorkflowHistory record ที่เพิ่งสร้าง" example: "b8a2e3c1-4567-4abc-8def-0123456789ab" isCompleted: type: boolean description: "true ถ้า Workflow สิ้นสุดแล้ว (Terminal State)" example: false attachmentsLinked: type: integer description: "จำนวนไฟล์แนบที่ถูก link กับ History record นี้" example: 2 AttachmentSummary: type: object properties: publicId: type: string format: uuid description: "UUIDv7 identifier (ADR-019)" example: "019505a1-7c3e-7000-8000-abc123def456" originalFilename: type: string example: "drawing-rev-A.pdf" mimeType: type: string example: "application/pdf" fileSize: type: integer description: "ขนาดไฟล์ (bytes)" example: 2048000 createdAt: type: string format: date-time example: "2026-04-12T10:30:00Z" WorkflowHistoryItem: type: object properties: id: type: string format: uuid description: "UUID ของ history record (PK โดยตรง)" example: "b8a2e3c1-4567-4abc-8def-0123456789ab" fromState: type: string example: "PENDING_REVIEW" toState: type: string example: "APPROVED" action: type: string example: "APPROVE" actorName: type: string description: "ชื่อผู้ดำเนินการ (populated via user join)" example: "สมชาย ใจดี" nullable: true comment: type: string nullable: true example: "อนุมัติแล้ว" createdAt: type: string format: date-time example: "2026-04-12T10:30:00Z" attachments: type: array description: "ไฟล์แนบที่อัปโหลดพร้อมขั้นตอนนี้ (Step-specific)" items: $ref: "#/components/schemas/AttachmentSummary" ErrorResponse: type: object properties: statusCode: type: integer message: type: string description: "Technical error message (for developers)" userMessage: type: string description: "User-friendly message (Thai, for display)" recoveryAction: type: string description: "คำแนะนำสำหรับผู้ใช้ในการแก้ปัญหา" nullable: true errorCode: type: string nullable: true paths: /instances/{instanceId}/transition: post: operationId: processWorkflowTransition summary: "ดำเนินการเปลี่ยนสถานะ Workflow พร้อมแนบไฟล์ประจำขั้นตอน" description: | Executes a workflow state transition. Optionally links pre-uploaded attachments to this workflow history step. **Security:** - Requires `workflow.action_review` permission - 4-Level RBAC: Superadmin > Org Admin > Assigned Handler > Read-only - Idempotency-Key header REQUIRED (prevents duplicate submissions) **File Attachment:** - Files must be pre-uploaded via Two-Phase upload endpoint - Files must be committed (is_temporary = false, ClamAV passed) - Max 20 attachments per transition **Concurrency:** - Redis Redlock applied to instanceId - Only 1 concurrent transition allowed per instance parameters: - name: instanceId in: path required: true schema: type: string format: uuid description: "Workflow Instance ID (UUID)" - name: Idempotency-Key in: header required: true schema: type: string format: uuid description: "UUIDv7 idempotency token (TTL: 24h). Re-use returns cached response." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WorkflowTransitionRequest" examples: approve_with_attachments: summary: "อนุมัติพร้อมไฟล์แนบ" value: action: "APPROVE" comment: "อนุมัติแล้ว เอกสารครบถ้วน" attachmentPublicIds: - "019505a1-7c3e-7000-8000-abc123def456" - "019505b2-8d4f-7000-9000-def456abc789" reject_no_attachments: summary: "ปฏิเสธโดยไม่มีไฟล์แนบ" value: action: "REJECT" comment: "เอกสารไม่ครบถ้วน กรุณาแก้ไขและส่งใหม่" responses: "200": description: "Transition executed successfully" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/WorkflowTransitionResponse" "200_idempotent": description: "Idempotent response — duplicate key detected, cached result returned" headers: X-Idempotent-Replay: schema: type: string enum: ["true"] content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/WorkflowTransitionResponse" "400": description: | Bad Request — possible causes: - Missing Idempotency-Key header - Invalid action for current state - attachmentPublicIds contains non-committed or non-existent UUIDs - Workflow not in ACTIVE status content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "403": description: "Forbidden — user does not have permission for this transition" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" example: statusCode: 403 userMessage: "คุณไม่มีสิทธิ์ดำเนินการในขั้นตอนนี้" recoveryAction: "ติดต่อผู้รับผิดชอบหรือ Admin หากคิดว่านี่เป็นข้อผิดพลาด" "404": description: "Workflow Instance not found" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "409": description: "Conflict — Concurrent transition in progress (Redlock busy)" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" example: statusCode: 409 userMessage: "มีการดำเนินการอื่นอยู่ระหว่างดำเนินการ กรุณารอสักครู่แล้วลองใหม่" recoveryAction: "รอ 5 วินาทีแล้วลองใหม่อีกครั้ง" /instances/{instanceId}/history: get: operationId: getWorkflowHistory summary: "ดึงประวัติการเปลี่ยนสถานะพร้อมไฟล์แนบประจำแต่ละขั้นตอน" description: | Returns the complete workflow history for an instance, including step-specific attachments for each transition. **Caching:** Redis cache TTL 1h, invalidated on state change. **Security:** Requires `document.view` permission. parameters: - name: instanceId in: path required: true schema: type: string format: uuid description: "Workflow Instance ID (UUID)" responses: "200": description: "History with step attachments" content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/WorkflowHistoryItem" example: data: - id: "b8a2e3c1-4567-4abc-8def-0123456789ab" fromState: "DRAFT" toState: "PENDING_REVIEW" action: "SUBMIT" actorName: "สมชาย ใจดี" comment: "ส่งเพื่อตรวจสอบ" createdAt: "2026-04-10T09:00:00Z" attachments: [] - id: "c9b3f4d2-5678-5bcd-9ef0-1234567890bc" fromState: "PENDING_REVIEW" toState: "APPROVED" action: "APPROVE" actorName: "วิชัย รักดี" comment: "อนุมัติแล้ว" createdAt: "2026-04-12T10:30:00Z" attachments: - publicId: "019505a1-7c3e-7000-8000-abc123def456" originalFilename: "drawing-rev-A.pdf" mimeType: "application/pdf" fileSize: 2048000 createdAt: "2026-04-12T10:25:00Z" "403": description: "Forbidden" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "404": description: "Instance not found" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse"