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

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

---

## Research Question 1: File Attachment Strategy During Workflow Transition

**Question:** Should files be uploaded atomically with the transition (multipart), or pre-uploaded and referenced by ID?

### Option A: Upload-Then-Reference (Selected ✅)

Client uploads files first via existing Two-Phase upload endpoint → receives `publicId`s → sends `publicId`s in `WorkflowTransitionDto.attachmentPublicIds`.

**Pros:**
- ClamAV scan and file validation run **independently** before the transition DB transaction
- Simpler transaction boundary (no file I/O inside Redlock + DB transaction)
- Compatible with existing `file-storage.service.ts` Two-Phase pattern (ADR-016)
- Incremental upload UX (user can upload as they review, then click Approve)
- No multipart complexity in transition controller

**Cons:**
- Two API round-trips from client
- Orphaned temp files possible if user abandons without transitioning (mitigated by existing `FileCleanupService` cron job)

### Option B: Multipart Transition Request (Rejected ❌)

Single multipart request combining DTO JSON + files.

**Cons:**
- ClamAV scan must happen inside the Redlock-protected transaction — high latency risk
- Breaks ADR-016 Two-Phase Upload contract
- Controller complexity (Multer + DTO parsing + ClamAV + Redlock + TypeORM transaction)

**Decision:** Option A — Upload-Then-Reference.
**Rationale:** Consistent with ADR-016 Two-Phase and existing `file-storage.controller.ts` patterns. Zero change to ClamAV pipeline.

---

## Research Question 2: FK Structure for Step-Specific Attachments

**Question:** Add `workflow_history_id` directly to `attachments` table, or create a junction table `workflow_history_attachments`?

### Option A: Direct FK on `attachments` (Selected ✅)

```sql
ALTER TABLE attachments
  ADD COLUMN workflow_history_id CHAR(36) NULL;
```

**Pros:**
- Specified explicitly in ADR-021 §5
- Single JOIN to get step attachments
- Backward-compatible: existing records have `NULL` (treated as main-doc attachments)
- Simpler query: `WHERE workflow_history_id = :historyId`

**Cons:**
- `attachments` table grows slightly (one nullable column)

### Option B: Junction Table `workflow_history_attachments` (Rejected ❌)

```sql
CREATE TABLE workflow_history_attachments (
  history_id CHAR(36) NOT NULL,
  attachment_id INT NOT NULL,
  PRIMARY KEY (history_id, attachment_id)
);
```

**Cons:**
- Extra table + extra JOIN
- ADR-021 explicitly mandates Approach A
- More complex service code

**Decision:** Option A — Direct FK on `attachments` table.
**Rationale:** Explicitly mandated by ADR-021 §5 §6. Nullable FK provides backward compatibility.

---

## Research Question 3: `workflow_histories.id` UUID Type

**Finding from codebase:**

`@/e:/np-dms/lcbp3/backend/src/modules/workflow-engine/entities/workflow-history.entity.ts:22-23`
```typescript
@PrimaryGeneratedColumn('uuid')
id!: string;
```

**Conclusion:** `workflow_histories.id` is `CHAR(36)` UUID generated by TypeORM `@PrimaryGeneratedColumn('uuid')`. This is **NOT** `UuidBaseEntity` pattern (which uses INT PK + `uuid` column separately).

**Impact on FK:** `attachments.workflow_history_id` must be `CHAR(36)` to match.

**ADR-019 compliance:** `WorkflowHistory` does NOT have the standard `publicId`/`@Exclude() id` pattern — it uses UUID as its primary key directly. When exposing history via API, use `id` (which is already a UUID string). No conversion needed.

---

## Research Question 4: Existing Workflow Visualizer — Reuse vs. New

**Finding:**

`@/e:/np-dms/lcbp3/frontend/components/custom/workflow-visualizer.tsx:26-28`
```typescript
// WorkflowVisualizer Component
// แสดงเส้นเวลา (Timeline) ของกระบวนการอนุมัติแบบแนวนอน
export function WorkflowVisualizer({ steps, className }: WorkflowVisualizerProps)
```

The existing component is **horizontal** layout. ADR-021 §3.2 requires **Vertical Timeline** with Indigo active highlighting.

**Decision:** Create NEW `frontend/components/workflow/workflow-lifecycle.tsx` (vertical). Keep existing `workflow-visualizer.tsx` (horizontal) for backward compat on other pages.

**Interface Difference:**
- Existing: `WorkflowStep { id, label, subLabel, status, date }`
- New: `WorkflowHistoryStep { id, fromState, toState, action, actor, comment, date, attachments[], isCurrent }`

---

## Research Question 5: Idempotency-Key Implementation

**Existing pattern:** No existing idempotency implementation found in workflow engine.

**Design:**
- Key format: `idempotency:transition:{idempotencyKey}:{userId}`
- Storage: Redis `SET` with `EX 86400` (24h TTL)
- Value: Serialized transition response `{ success, nextState, historyId, isCompleted }`
- On hit: Return cached value, HTTP 200 (or original status code if stored)
- On miss: Execute transition, store result, return

**Where to implement:** `WorkflowEngineController.processTransition()` — pre-execution check using `RedisService`. If key exists → return cached. If not → run `workflowService.processTransition()` → store result.

**Frontend:** `use-workflow-action` hook generates a UUIDv7 idempotency key once per "action intent" (when user clicks Approve/Reject). Key is regenerated if the user cancels and re-opens the action dialog.

---

## Research Question 6: Cache Strategy for Workflow History

**ADR-021 §9:** Redis Cache TTL 1 hour for workflow history data. Invalidate immediately on state change.

**Cache key pattern:**
- `wf:history:{instanceId}` → JSON of history items with attachments
- Invalidated in `WorkflowEngineService.processTransition()` after `commitTransaction()`

**Implementation:** Use existing `CacheService` or `@nestjs/cache-manager` with Redis store. Set `ttl: 3600` on history queries.

---

## Research Question 7: CASL 4-Level RBAC for Transition Guard

**ADR-021 §6:** 4-Level hierarchy: Superadmin > Org Admin > Assigned Handler > Read-only

**Existing patterns:**
- `RbacGuard` + `@RequirePermission()` in `workflow-engine.controller.ts`
- CASL ability checked via `CaslAbilityFactory`

**New `WorkflowTransitionGuard` logic:**

```typescript
// Level 1: system.manage_all (Superadmin) → always allowed
// Level 2: organization.manage_users AND same org as document → allowed
// Level 3: Assigned handler (workflow instance context.userId === req.user.user_id) → allowed
// Level 4: Read-only → FORBIDDEN (403)
```

**Impersonation (Upload on behalf):** Superadmin and Org Admin may supply `actingAsUserId` in payload to impersonate the handler for upload. Guard validates this permission level.

---

## Research Question 8: Priority Enum Visual Indicators

**ADR-021 Clarification:** `URGENT`, `HIGH`, `MEDIUM`, `LOW` — 4-tier system with visual indicators.

**Proposed Tailwind classes:**

| Priority | Badge Color | Dot/Icon |
|----------|------------|---------|
| `URGENT` | `bg-red-100 text-red-800 border-red-300` | 🔴 Pulse animation |
| `HIGH` | `bg-orange-100 text-orange-800 border-orange-300` | 🟠 |
| `MEDIUM` | `bg-yellow-100 text-yellow-800 border-yellow-300` | 🟡 |
| `LOW` | `bg-green-100 text-green-800 border-green-300` | 🟢 |

**Note:** Priority field source — `workflow_instances.context` JSON (existing `metadata`/`payload` field) or document master table. Needs clarification on which document modules expose `priority`. **Interim:** Render from `WorkflowInstance.context.priority` if present.

---

## Research Question 9: File Preview — Security Considerations

**Requirements:** PDF and Image preview via Modal (inline, no tab change).

**Backend:** Need a `GET /files/preview/:publicId` or `GET /attachments/:publicId/preview` endpoint that:
1. Validates user has `document.view` permission on the parent document
2. Streams file with `Content-Disposition: inline` (not `attachment`)
3. Sets `Content-Security-Policy: frame-src 'self'` for PDFs in `<iframe>`

**Frontend:**
- PDF: `<iframe src="/api/files/preview/:publicId" className="w-full h-full" />`
- Image: `<img src="/api/files/preview/:publicId" alt={filename} />`
- Detection: by `mimeType` field from attachment metadata

**Finding:** `file-storage.controller.ts` exists — check if preview endpoint already implemented or needs addition.

---

## Research Question 10: Redis Redlock Failure Mode During Transition

**Question:** If Redis is unreachable or Redlock cannot be acquired, should the system fail-open (allow transition) or fail-closed (block transition)?

**Context (Clarify Q2):** In DMS, a race condition that creates duplicate approvals is worse than temporary service unavailability. Incorrect document state corrupts audit trails and may have legal/contractual consequences.

### Option A: Fail-open (Rejected ❌)
Allow transition even without a lock. Accepts Race Condition risk.
- **Con:** Two concurrent approvals possible — violates document integrity.

### Option B: Retry then fail-open (Rejected ❌)
Retry 3x then allow anyway.
- **Con:** Still permits race condition under high load.

### Option C: Fail-closed — Retry then HTTP 503 (Selected ✅)
Retry 3x with 500ms exponential backoff → if still unavailable → throw HTTP 503.
- **Pro:** Data integrity preserved at all times.
- **Pro:** HTTP 503 is a retryable error — client can display toast and user can retry manually.
- **Con:** Temporary service degradation during Redis outage (~50 concurrent users; outage rare).

**Decision:** Option C — Fail-closed.
**Implementation:**
```typescript
// ใน WorkflowEngineService.processTransition()
// ลอง Acquire Redlock สูงสุด 3 ครั้ง
const MAX_LOCK_RETRIES = 3;
const LOCK_BACKOFF_MS = 500;

for (let attempt = 1; attempt <= MAX_LOCK_RETRIES; attempt++) {
  try {
    lock = await this.redlock.acquire([`lock:wf:${instanceId}`], 10000);
    break; // สำเร็จ
  } catch {
    if (attempt === MAX_LOCK_RETRIES) {
      throw new ServiceUnavailableException('ระบบยุ่งชั่วคราว กรุณาลองใหม่ภายหลัง');
    }
    await sleep(LOCK_BACKOFF_MS * attempt); // exponential backoff
  }
}
```

---

## Research Question 11: Upload-Permitted States

**Question:** Which workflow states allow step-attachment upload? (Clarify Q1)

**Decision:** Upload permitted ONLY in `PENDING_REVIEW` and `PENDING_APPROVAL`.

**Rationale:** Attachments are decision-support evidence. After a terminal decision (`APPROVED`, `REJECTED`, `CLOSED`), adding attachments retroactively would contaminate the audit trail.

**Implementation:** Both client-side guard (disable upload UI) AND server-side validation in `processTransition()` before acquiring Redlock.

---

## Summary of All Decisions

| # | Decision | Chosen | Rejected |
|---|---------|--------|---------|
| 1 | File attachment strategy | Upload-Then-Reference | Multipart atomic |
| 2 | FK structure | Direct nullable FK on `attachments` | Junction table |
| 3 | `workflow_histories.id` type | `CHAR(36)` UUID direct PK | N/A |
| 4 | Visualizer reuse | New vertical `workflow-lifecycle.tsx` | Reuse horizontal |
| 5 | Idempotency storage | Redis key `idempotency:transition:{key}:{userId}` TTL 24h | DB table |
| 6 | History cache | Redis `wf:history:{instanceId}` TTL 1h, invalidate on transition | No cache |
| 7 | Transition RBAC | New `WorkflowTransitionGuard` 4-Level | Extend existing `RbacGuard` |
| 8 | Priority display | Tailwind badges from `instance.context.priority` | Hardcoded status |
| 9 | File preview security | `Content-Disposition: inline` + permission check | Direct storage URL |
| 10 | Redlock failure mode | Fail-closed: Retry 3x (500ms backoff) → HTTP 503 | Fail-open |
| 11 | Upload-permitted states | `PENDING_REVIEW`, `PENDING_APPROVAL` only | All non-terminal states |
| 12 | Module scope (v1.8.6) | **RFA, Transmittal, Circulation, Correspondence** (v2 Revised 2026-04-19) | ~~Excluding Correspondence~~ |
