# Data Model: ADR-021 Integrated Workflow Context & Step-specific Attachments

**Phase 1 Output** | Generated: 2026-04-12

---

## 1. SQL Delta

**File:** `specs/03-Data-and-Storage/deltas/04-add-workflow-history-id-to-attachments.sql`

```sql
-- ============================================================
-- Delta 04: ADR-021 — Step-specific Attachments
-- เพิ่ม FK workflow_history_id ใน attachments table
-- ============================================================
-- ข้อควรระวัง: ค่า NULL = ไฟล์แนบหลัก (Main Document)
--              ค่าไม่ NULL = ไฟล์ประจำ Workflow Step นั้น

ALTER TABLE attachments
  ADD COLUMN workflow_history_id CHAR(36) NULL
    COMMENT 'FK to workflow_histories.id สำหรับไฟล์แนบประจำ Step (ADR-021). NULL = ไฟล์แนบหลัก',
  ADD CONSTRAINT fk_attachments_workflow_history
    FOREIGN KEY (workflow_history_id)
    REFERENCES workflow_histories (id)
    ON DELETE SET NULL
    ON UPDATE CASCADE;

-- Index สำหรับ optimize การดึงไฟล์แนบตาม Step + เรียงตามวันที่
CREATE INDEX idx_att_wfhist_created
  ON attachments (workflow_history_id, created_at);
```

**Migration Notes (ADR-009):**
- Apply via `MariaDB CLI` หรือผ่าน n8n delta workflow
- ไม่มี TypeORM migration file — ห้ามสร้าง (ADR-009)
- Rollback: `ALTER TABLE attachments DROP FOREIGN KEY fk_attachments_workflow_history; ALTER TABLE attachments DROP COLUMN workflow_history_id;`

---

## 2. Backend Entity Changes

### 2.1 `attachment.entity.ts` — Add `workflowHistoryId`

**Current state (existing columns):**

```@/e:/np-dms/lcbp3/backend/src/common/file-storage/entities/attachment.entity.ts:43-58```

**Required additions:**

```typescript
// เพิ่มหลัง referenceDate column
@Column({ name: 'workflow_history_id', length: 36, nullable: true })
workflowHistoryId?: string;

// Lazy relation — ไม่ include ใน default query เพื่อป้องกัน N+1
@ManyToOne(
  () => WorkflowHistory,
  (history: WorkflowHistory) => history.attachments,
  { nullable: true, onDelete: 'SET NULL', lazy: true }
)
@JoinColumn({ name: 'workflow_history_id' })
workflowHistory?: Promise<WorkflowHistory>;
```

**Import to add:**
```typescript
import { WorkflowHistory } from '../../../modules/workflow-engine/entities/workflow-history.entity';
```

---

### 2.2 `workflow-history.entity.ts` — Add `attachments` relation

**Current state:**

```@/e:/np-dms/lcbp3/backend/src/modules/workflow-engine/entities/workflow-history.entity.ts:18-61```

**Required additions:**

```typescript
// เพิ่ม import
import { OneToMany } from 'typeorm';
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';

// เพิ่มใน Class (หลัง createdAt)
// Lazy relation — โหลดเฉพาะเมื่อต้องการ (ป้องกัน N+1 ใน History list queries)
@OneToMany(
  () => Attachment,
  (attachment: Attachment) => attachment.workflowHistory,
  { lazy: true }
)
attachments?: Promise<Attachment[]>;
```

---

## 3. DTO Changes

### 3.1 `workflow-transition.dto.ts` — Add `attachmentPublicIds`

**Extended DTO:**

```typescript
// เพิ่ม imports
import { IsArray, IsUUID, ArrayMaxSize } from 'class-validator';

// เพิ่ม field ใน WorkflowTransitionDto
@ApiPropertyOptional({
  description: 'รายการ publicId ของไฟล์แนบ (ต้องอัปโหลดผ่าน Two-Phase ก่อน — ADR-016)',
  example: ['019505a1-7c3e-7000-8000-abc123def456'],
  type: [String],
})
@IsArray()
@IsUUID('all', { each: true })
@ArrayMaxSize(20)  // ป้องกัน payload ขนาดใหญ่เกิน (controlled by infra, this is soft guard)
@IsOptional()
attachmentPublicIds?: string[];
```

---

## 4. New Types

### 4.1 Backend — `WorkflowHistoryResponseDto`

**File:** `backend/src/modules/workflow-engine/dto/workflow-history-response.dto.ts` (NEW)

```typescript
export class AttachmentSummaryDto {
  publicId!: string;           // UUIDv7 (ADR-019)
  originalFilename!: string;
  mimeType!: string;
  fileSize!: number;
  createdAt!: Date;
}

export class WorkflowHistoryItemDto {
  id!: string;                 // UUID — เป็น PK โดยตรง (ไม่ใช่ UuidBaseEntity)
  fromState!: string;
  toState!: string;
  action!: string;
  actionByUserId?: number;
  comment?: string;
  createdAt!: Date;
  attachments!: AttachmentSummaryDto[];  // ไฟล์แนบประจำ Step นี้
}
```

---

## 5. Frontend Type Changes

### 5.1 `frontend/types/workflow.ts` — Add `WorkflowHistoryItem`

**Addition to existing file:**

```typescript
// ไฟล์แนบสรุปสำหรับแสดงใน Workflow Timeline
export interface WorkflowAttachmentSummary {
  publicId: string;           // ADR-019: ใช้ publicId เท่านั้น
  originalFilename: string;
  mimeType: string;
  fileSize: number;
  createdAt: string;
}

// ประวัติ 1 ขั้นตอนใน Workflow Timeline
export interface WorkflowHistoryItem {
  id: string;                 // UUID — history record ID
  fromState: string;
  toState: string;
  action: WorkflowAction;
  actorName?: string;         // ชื่อผู้ดำเนินการ (populated via join)
  comment?: string;
  createdAt: string;
  attachments: WorkflowAttachmentSummary[];
  isCurrent?: boolean;        // computed by frontend
}

// Priority Enum สำหรับ Integrated Banner
export enum WorkflowPriority {
  URGENT = 'URGENT',
  HIGH = 'HIGH',
  MEDIUM = 'MEDIUM',
  LOW = 'LOW',
}
```

### 5.2 `frontend/types/dto/workflow-engine/workflow-engine.dto.ts` — Add transition DTO

**Addition:**

```typescript
// Extended Transition DTO รองรับ Step-specific Attachments (ADR-021)
export interface WorkflowTransitionWithAttachmentsDto {
  action: string;
  comment?: string;
  payload?: Record<string, unknown>;
  attachmentPublicIds?: string[];    // pre-uploaded UUIDv7 list
}
```

---

## 6. State Transition with Attachment Flow

```
[User reviews document]
     │
     ▼
[Upload files via POST /files/upload (Two-Phase)]
  → ClamAV scan (auto)
  → Returns: { publicId, tempId, ... }[]
     │
     ▼
[User clicks Approve/Reject/Return]
  → use-workflow-action hook (client-side guard):
      1. ❗️ ตรวจสอบ currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL}
         └ ถ้าไม่ใช่ → ไม่ส่ง API (ปุ่ม disabled ไว้แล้ว)
      2. Generates Idempotency-Key (UUIDv7)
      3. POST /workflow-engine/instances/:id/transition
         Header: Idempotency-Key
         Body: { action, comment, attachmentPublicIds: [uuid1, uuid2] }
     │
     ▼
[WorkflowTransitionGuard] — RBAC check (4-Level: Superadmin / Org Admin / Contract Member / Assigned Handler)
     │ pass
     ▼
[WorkflowEngineService.processTransition()]
  → Check Redis idempotency key (return cached if duplicate)
  → ❗️ Server-side state check: currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL}
      └ ถ้าไม่ใช่ → throw HTTP 409 Conflict "ไม่สามารถอัปโหลดในสถานะนี้" (Clarify Q1)
  → Acquire Redis Redlock on instanceId
      └ Retry 3x (500ms exponential backoff)
      └ ถ้า Redis ล่ม / Lock ไม่ได้ → throw HTTP 503 "Service temporarily unavailable" (Clarify Q2 Fail-closed)
  → Begin DB Transaction:
      1. Lock WorkflowInstance (pessimistic_write)
      2. Evaluate DSL transition
      3. Update WorkflowInstance.currentState
      4. Create WorkflowHistory record
      5. Resolve attachmentPublicIds → internal IDs
      6. UPDATE attachments SET workflow_history_id = :historyId
         WHERE uuid IN (:publicIds) AND is_temporary = false
      7. Commit Transaction
  → Release Redlock
  → Dispatch BullMQ events (notification, audit)
  → Invalidate Redis cache key wf:history:{instanceId}
  → Store idempotency response in Redis (TTL 24h)
     │
     ▼
[Response: { success, nextState, historyId, isCompleted }]
     │
     ▼
[Frontend: invalidate TanStack Query cache → reload document + timeline]
  → HTTP 503 → แสดง toast "ระบบยุ่ง กรุณาลองใหม่" (user may retry)
```

---

## 7. Entity Relationship Diagram

```
contracts
  │ 1
  │ (FK, nullable) [delta-07]
  ▼ N
workflow_definitions
       │ 1
       │ has many
       ▼ N
workflow_instances ────────────────── documents (RFA/Corr/Transmittal/Circulation)
  contract_id: INT NULL [delta-07]       (entityType + entityId)
  (NULL = org-scoped e.g. Circulation)
       │ 1
       │ has many
       ▼ N
workflow_histories ◄─────────────────────────┤
       │ id: CHAR(36) UUID                        │
       │                                          │
       │ ◄── attachments.workflow_history_id (FK, nullable)
       │
attachments
  id: INT (internal, @Exclude)
  uuid: UUID (publicId — ADR-019)
  workflow_history_id: CHAR(36) NULL ← NEW (ADR-021)
```

---

## 8. Index Strategy

| Table | Index | Columns | Purpose |
|-------|-------|---------|---------|
| `attachments` | `idx_att_wfhist_created` (NEW) | `(workflow_history_id, created_at)` | Fetch step attachments sorted by date |
| `workflow_histories` | `idx_wf_hist_instance` (existing) | `(instance_id)` | Fetch all steps for a workflow instance |
| `workflow_histories` | `idx_wf_hist_user` (existing) | `(action_by_user_id)` | Audit queries per user |
| `workflow_instances` | `idx_wf_inst_contract` (NEW — delta-07) | `(contract_id, entity_type, status)` | Guard contract-membership lookup + dashboard queries per contract |
