Files
lcbp3/specs/200-fullstacks/202-adr-021-integrated-workflow-conte/plan.md
T

9.6 KiB

Implementation Plan: ADR-021 Integrated Workflow Context & Step-specific Attachments

Branch: 200-fullstacks/202-adr-021-integrated-workflow-context | Date: 2026-04-12 | Spec: spec.md Location: specs/200-fullstacks/202-adr-021-integrated-workflow-context/ Input: Feature specification from ADR-021


Summary

ปรับปรุง Workflow Engine ให้รองรับ (1) Integrated Banner ที่ยุบรวม Metadata + Status + Actions ไว้ด้วยกัน (2) Vertical Timeline Lifecycle พร้อม Active Step Highlighting และ (3) Step-specific Attachments ที่เชื่อมโยงไฟล์แนบกับ workflow_history ของแต่ละขั้นตอนโดยตรง

แนวทางเทคนิค: ขยาย workflow_histories ด้วย FK ใน attachments (Nullable) + ขยาย WorkflowTransitionDto รับ attachmentPublicIds (pre-uploaded UUIDv7 list) + สร้าง Frontend components ใหม่ 4 ชิ้น


Technical Context

Language/Version: TypeScript 5.x (strict mode), Node.js 20+ Primary Dependencies:

  • Backend: NestJS 10, TypeORM 0.3, MariaDB 10.6+, Redis (Redlock), BullMQ
  • Frontend: Next.js 14 (App Router), TailwindCSS 3.4, shadcn/ui, TanStack Query v5, React Hook Form + Zod Storage: MariaDB (schema via SQL delta — ADR-009), MinIO / Local FS via StorageService Testing: Jest (backend unit + e2e), Vitest (frontend) Target Platform: QNAP Container Station (Docker), Browser (Chrome/Edge latest) Project Type: Web application (backend/ + frontend/ monorepo) Performance Goals: (1) Workflow history + attachment join query < 200ms p95 (mitigated by Redis Cache TTL 1h); (2) POST /instances/:id/transition (พร้อม file) P95 ≤ 5 วินาที สำหรับ file ≤ 10MB (รวม ClamAV + Redlock + DB transaction) Constraints: No TypeORM migrations (ADR-009); UUID via publicId only (ADR-019); ClamAV scan mandatory (ADR-016); BullMQ for all async jobs (ADR-008) Scale/Scope: ~50 concurrent users, documents in hundreds per project

Constitution Check

GATE: Checked against .windsurfrules before Phase 0. Re-verified after Phase 1.

Gate Status Notes
🔴 UUID Pattern (ADR-019) PASS All attachment references via publicId (UUIDv7 string). workflow_history_id FK value is CHAR(36) UUID from workflow_histories.id. No parseInt usage.
🔴 Schema via SQL Delta (ADR-009) PASS Delta file 04-add-workflow-history-id-to-attachments.sql — no TypeORM migration
🔴 Two-Phase Upload (ADR-016) PASS Files uploaded via existing Two-Phase endpoint first; publicIds referenced in transition DTO
🔴 ClamAV Scan (ADR-016) PASS ClamAV scan runs during Phase 1 of file upload (before transition)
🔴 CASL Guard (ADR-016) PASS New WorkflowTransitionGuard implements 4-Level RBAC
🔴 Idempotency-Key (Security Rule #1) PASS POST /instances/:id/transition validates Idempotency-Key header
🔴 BullMQ Async (ADR-008) PASS Notifications dispatched via WorkflowEventService (existing BullMQ pattern)
🔴 No any types PASS All new types fully typed
🟡 Thin Controller PASS Controller delegates to Service; Guard handles RBAC
🟡 Test Coverage 80% business logic ⚠️ REQUIRED See testing plan in Phase 3
🔴 Redis Redlock (ADR-002) PASS Redlock applied to instanceId during processTransition() — Fail-closed: Retry 3x (500ms exponential backoff) → HTTP 503 if Redis unavailable
🔴 Upload State Restriction PASS Step-attachment upload permitted only in PENDING_REVIEW/PENDING_APPROVAL; Terminal states (APPROVED,REJECTED,CLOSED) → HTTP 409

Project Structure

Documentation (this feature)

specs/feat/adr-021-integrated-workflow-context/
├── spec.md              # Feature specification
├── plan.md              # This file
├── tasks.md             # Generated by speckit-tasks
└── checklists/          # Quality checklists

Source Code (impacted files)

# 🔴 Backend — DB & Entities
specs/03-Data-and-Storage/deltas/
└── 04-add-workflow-history-id-to-attachments.sql   [NEW]

backend/src/common/file-storage/entities/
└── attachment.entity.ts                             [MODIFY — add workflowHistoryId + relation]

backend/src/modules/workflow-engine/entities/
└── workflow-history.entity.ts                       [MODIFY — add OneToMany attachments]

# 🔴 Backend — API & Guards
backend/src/modules/workflow-engine/dto/
└── workflow-transition.dto.ts                       [MODIFY — add attachmentPublicIds]

backend/src/modules/workflow-engine/guards/
└── workflow-transition.guard.ts                     [NEW — 4-Level RBAC]

backend/src/modules/workflow-engine/
├── workflow-engine.service.ts                       [MODIFY — extend processTransition()]
├── workflow-engine.controller.ts                    [MODIFY — add idempotency header, guard]
└── workflow-engine.module.ts                        [MODIFY — register guard]

# 🟡 Frontend — Types
frontend/types/
└── workflow.ts                                      [MODIFY — add attachments to WorkflowHistoryStep]

frontend/types/dto/workflow-engine/
└── workflow-engine.dto.ts                           [MODIFY — add WorkflowTransitionWithAttachmentsDto]

# 🟡 Frontend — New Components
frontend/components/workflow/
├── integrated-banner.tsx                            [NEW — Status + Metadata + Action bar]
└── workflow-lifecycle.tsx                           [NEW — Vertical timeline with Indigo active step]

frontend/components/common/
└── file-preview-modal.tsx                           [NEW — PDF/Image inline preview]

# 🟡 Frontend — New Hook
frontend/hooks/
└── use-workflow-action.ts                           [NEW — upload + transition orchestration]

# 🟡 Frontend — Page Refactors (use new components)
frontend/app/(dashboard)/rfas/[uuid]/page.tsx              [MODIFY — integrate IntegratedBanner + WorkflowLifecycle]
frontend/app/(dashboard)/transmittals/[uuid]/page.tsx       [MODIFY — same as RFA]
frontend/app/(dashboard)/circulation/[uuid]/page.tsx        [MODIFY — same as RFA]
frontend/app/(dashboard)/correspondences/[uuid]/page.tsx    [MODIFY — same as RFA]

Complexity Tracking

No constitution violations. Architecture is additive (Nullable FK, extended DTO, new components).


Design Decisions

Data Model

  • attachments.workflow_history_id = CHAR(36) NULL FK → workflow_histories.id
  • ON DELETE SET NULL (preserve attachment records if history row deleted)
  • Composite index: INDEX idx_att_wfhist_created (workflow_history_id, created_at)
  • WorkflowHistory gains @OneToMany(() => Attachment, a => a.workflowHistory)lazy-loaded only

API Contract

Extended POST /workflow-engine/instances/:instanceId/transition:

Header: Idempotency-Key: <UUIDv7>
Body: {
  action: string               // existing
  comment?: string             // existing
  payload?: Record             // existing
  attachmentPublicIds?: string[] // NEW — UUIDv7 list of pre-uploaded attachments
}

New GET /workflow-engine/instances/:instanceId/history:

Response: WorkflowHistoryItem[] with nested attachments[] per step

Frontend Architecture

3 new components follow compound pattern:

  • <IntegratedBanner> — Status + Metadata + Action bar
  • <WorkflowLifecycle> — Vertical timeline, Indigo active step (pulse animation)
  • <FilePreviewModal> — PDF iframe / Image viewer

use-workflow-action hook responsibilities:

  1. Validate Idempotency-Key (generate UUIDv7 once per action intent)
  2. Guard: Check currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL} before transition
  3. Ensure all attachmentPublicIds are committed (not temp) before transition
  4. Call POST /instances/:id/transition with Idempotency-Key header
  5. Handle HTTP 503 → toast "ระบบยุ่ง กรุณาลองใหม่"
  6. Invalidate TanStack Query cache for the document + workflow instance

Modules in scope (v1.8.6): RFA, Transmittal, Circulation, Correspondence (4 modules)


Risk Register

Risk Likelihood Impact Mitigation
N+1 query on history + attachments join Medium High Eager-load only when explicitly querying history; Redis cache TTL 1h
Race condition: 2 users upload to same step simultaneously Low High Redis Redlock on instanceId — only 1 transition allowed at a time
Attachment linked to wrong history record Low High processTransition() creates history row first, then links attachments in same transaction
ClamAV timeout during upload Low Medium Upload endpoint has its own timeout; transition is decoupled
Frontend: stale workflow state after transition Medium Medium use-workflow-action hook invalidates TanStack Query cache on success

Dependencies Map

ADR-021
  ├── ADR-001 (Workflow Engine DSL) — extends processTransition()
  ├── ADR-002 (Redis Redlock) — existing lock pattern applied to transition
  ├── ADR-016 (Security) — Two-Phase upload, ClamAV, CASL Guard
  ├── ADR-019 (UUID) — publicId for all attachment references
  └── ADR-008 (BullMQ) — notification dispatch (unchanged, existing pattern)