690414:1113 Update README.md /.agents/skills, /.windsurf/workflows

This commit is contained in:
2026-04-14 11:13:42 +07:00
parent 02400fd88c
commit 6d45bdaeb5
194 changed files with 12708 additions and 8762 deletions
+333
View File
@@ -0,0 +1,333 @@
# Implementation Plan: Transmittals + Circulation Complete Integration (v1.8.7 Post-ADR-021)
**Branch**: `001-transmittals-circulation` | **Date**: 2026-04-12 | **Spec**: `specs/001-transmittals-circulation/spec.md`
---
## Summary
Wire up the ADR-021 `IntegratedBanner` + `WorkflowLifecycle` components (already imported as stubs) into the Transmittal and Circulation detail pages with live workflow data, add the missing `workflowInstanceId` exposure on both backend services, implement pending edge-case handlers (EC-RFA-004, EC-CIRC-001/002/003), and create missing TanStack Query hooks and list-page features.
---
## Technical Context
**Language/Version**: TypeScript 5.x (strict)
**Primary Dependencies**: NestJS 10, Next.js 14 (App Router), TypeORM, MariaDB, TanStack Query v5, shadcn/ui, TailwindCSS
**Storage**: MariaDB (via `workflow_instances` + `workflow_history` + `attachments` tables)
**Testing**: Vitest (frontend), Jest (backend)
**Target Platform**: QNAP Container Station (on-prem)
**Performance Goals**: Detail page loads workflow data < 1s; TanStack Query staleTime 60s
**Constraints**: ADR-009 (no migrations), ADR-019 (UUID strings only), ADR-016 (ClamAV), ADR-008 (BullMQ for notifications)
---
## Constitution Check
| Rule | Status | Notes |
|------|--------|-------|
| UUID patterns (ADR-019) — no parseInt | ✅ PASS | Use `publicId` string throughout |
| Schema changes via SQL delta (ADR-009) | ✅ PASS | No schema changes needed — additive response only |
| Security: CASL Guard on new endpoints | ✅ REQUIRED | Reassign/ForceClose endpoints need guards |
| Security: Idempotency-Key on workflow transitions | ✅ PASS | `use-workflow-action.ts` already sends key |
| BullMQ for notifications (ADR-008) | ✅ PASS | Existing NotificationService handles this |
| No `any` types | ✅ REQUIRED | Strict TypeScript enforcement |
| Thin controllers, business logic in services | ✅ PASS | Following existing RFA/Correspondence pattern |
| Test coverage ≥ 80% business logic | ⚠️ REQUIRED | New service methods need unit tests |
| Redis Redlock for document numbering | ✅ N/A | No new document numbering in this feature |
---
## Project Structure
### Documentation
```text
specs/001-transmittals-circulation/
├── spec.md ✅ done
├── plan.md ✅ this file
├── data-model.md (inline in this plan)
└── tasks.md (next step)
```
### Source Code Changes
```text
backend/src/modules/
├── workflow-engine/
│ └── workflow-engine.service.ts [ADD] getInstanceByEntity()
├── transmittal/
│ ├── transmittal.service.ts [MODIFY] findOneByUuid + findAll + EC-RFA-004
│ └── transmittal.controller.ts [MODIFY] add /submit endpoint
├── circulation/
│ ├── circulation.service.ts [MODIFY] findOneByUuid + reassign + forceClose
│ └── circulation.controller.ts [MODIFY] add /reassign + /force-close endpoints
frontend/
├── types/
│ ├── transmittal.ts [MODIFY] add workflowInstanceId, workflowState, availableActions
│ └── circulation.ts [MODIFY] add workflowInstanceId, workflowState, deadline fields
├── hooks/
│ ├── use-transmittal.ts [NEW] useTransmittal(uuid) hook
│ └── use-circulation.ts [MODIFY] add useCirculation(uuid)
├── app/(dashboard)/
│ ├── transmittals/[uuid]/page.tsx [MODIFY] wire IntegratedBanner + WorkflowLifecycle
│ └── circulation/[uuid]/page.tsx [MODIFY] wire IntegratedBanner + WorkflowLifecycle + Overdue
└── public/locales/
├── th/transmittal.json [ADD/UPDATE] i18n keys
└── en/transmittal.json [ADD/UPDATE] i18n keys
```
---
## Phase 0: Research Findings
### Key Design Decisions
1. **`workflowInstanceId` exposure**: No schema changes needed. The `WorkflowInstance` table has `entity_type` + `entity_id` columns. Add `getInstanceByEntity(entityType, entityId)` to `WorkflowEngineService`, then call it in `findOneByUuid` for both modules.
2. **Transmittal entityType**: Use `'transmittal'` (matching the entity ID = `correspondence.id.toString()`). Consistent with how RFA uses `'rfa'`.
3. **Circulation entityType**: Use `'circulation'` (entity ID = `circulation.id.toString()`). The Circulation entity already extends `UuidBaseEntity` (has `publicId`).
4. **Transmittal publicId**: The Transmittal entity has no own `publicId` — it shares `correspondence.publicId`. The service already maps it: `publicId: t.correspondence?.publicId`. This is correct; no change needed.
5. **EC-RFA-004 validation**: Fires in `TransmittalService.submit()` (new method). Check all `transmittal_items` → fetch their correspondence's current revision status → if any is DRAFT, throw `ValidationException`.
6. **EC-CIRC-001 (Reassign)**: New `CirculationService.reassignRouting(routingId, newAssigneeUuid, user)`. Requires `Document Control` or above role. Updates `routing.assignedTo` to the new user's INT id.
7. **EC-CIRC-002 (Force Close)**: New `CirculationService.forceClose(circulationId, reason, user)`. Requires `Document Control` or above. Updates all PENDING routings to `CANCELLED`, sets `circulation.statusCode = 'CANCELLED'`, writes audit log.
8. **EC-CIRC-003 (Overdue)**: Pure frontend logic. Compare `routing.deadline` with `new Date()`. Show `OverdueBadge` if `now > deadline + 1 day`. No backend change needed.
---
## Phase 1: Design Decisions
### Data Model Changes (No SQL delta required)
The `WorkflowInstance` table already exists with `entity_type VARCHAR`, `entity_id VARCHAR`. The only change is:
- **Add** `getInstanceByEntity(entityType, entityId)` to `WorkflowEngineService` (TypeORM query, no schema change).
### API Contract
#### `GET /transmittals/:uuid`
**Response addition** (existing fields unchanged):
```json
{
"data": {
"publicId": "...",
"workflowInstanceId": "019abc...",
"workflowState": "IN_REVIEW",
"availableActions": ["APPROVE", "REJECT"],
...existing fields...
}
}
```
#### `GET /circulation/:uuid`
**Response addition**:
```json
{
"data": {
"publicId": "...",
"workflowInstanceId": "019def...",
"workflowState": "OPEN",
"availableActions": [],
"routings": [
{
"id": 1,
"deadline": "2026-04-20T00:00:00.000Z",
"isOverdue": true,
...
}
],
...
}
}
```
#### `POST /transmittals/:uuid/submit` (NEW)
**Request**:
```json
{ "templateId": "optional-workflow-template-uuid" }
```
**Response**: `{ "instanceId": "...", "currentState": "IN_REVIEW" }`
**Error (EC-RFA-004)**: `422` `{ "message": "RFA [doc-no] ยังอยู่ใน Draft กรุณา Submit ก่อน" }`
#### `PATCH /circulation/:id/routing/:routingId/reassign` (NEW)
**Request**: `{ "newAssigneeId": "uuid" }`
**Guards**: `@RequirePermission('circulation.manage')`
#### `POST /circulation/:id/force-close` (NEW)
**Request**: `{ "reason": "mandatory string" }`
**Guards**: `@RequirePermission('circulation.manage')`
**Response**: `{ "success": true }`
### Frontend Architecture
#### `useTransmittal(uuid)` hook
```ts
useQuery<Transmittal>({
queryKey: ['transmittal', uuid],
queryFn: () => transmittalService.getByUuid(uuid),
enabled: !!uuid,
staleTime: 60_000,
})
```
Returns: `{ transmittal, isLoading, error }` — replaces inline `useQuery` in detail page.
#### `useCirculation(uuid)` hook
```ts
useQuery<Circulation>({
queryKey: ['circulation', uuid],
queryFn: () => circulationService.getByUuid(uuid),
enabled: !!uuid,
staleTime: 60_000,
})
```
#### `useWorkflowHistory(instanceId)` — already exists, use directly in pages.
#### Overdue Badge logic (frontend-only)
```ts
function isOverdue(deadline?: string): boolean {
if (!deadline) return false;
const deadlinePlusOne = new Date(deadline);
deadlinePlusOne.setDate(deadlinePlusOne.getDate() + 1);
return new Date() > deadlinePlusOne;
}
```
---
## Phase 2: Task Breakdown
### Critical (Backend)
| Task | File | Description |
|------|------|-------------|
| B1 | `workflow-engine.service.ts` | Add `getInstanceByEntity(entityType, entityId)` returning `{ id, currentState }` or null |
| B2 | `transmittal.service.ts` | `findOneByUuid`: lookup workflow instance, add to response |
| B3 | `transmittal.service.ts` | `findAll`: add `purpose` filter |
| B4 | `transmittal.service.ts` | Add `submit(uuid, user)` with EC-RFA-004 validation |
| B5 | `transmittal.controller.ts` | Add `POST /:uuid/submit` endpoint with guard |
| B6 | `circulation.service.ts` | `findOneByUuid`: lookup workflow instance, compute overdue |
| B7 | `circulation.service.ts` | Add `reassignRouting(routingId, newAssigneeUuid, user)` |
| B8 | `circulation.service.ts` | Add `forceClose(uuid, reason, user)` with EC-CIRC-002 |
| B9 | `circulation.controller.ts` | Add PATCH `/routing/:id/reassign` + POST `/force-close` |
### Important (Frontend)
| Task | File | Description |
|------|------|-------------|
| F1 | `types/transmittal.ts` | Add `workflowInstanceId?`, `workflowState?`, `availableActions?` |
| F2 | `types/circulation.ts` | Add `workflowInstanceId?`, `workflowState?`, deadline to routing |
| F3 | `hooks/use-transmittal.ts` | New `useTransmittal(uuid)` hook |
| F4 | `hooks/use-circulation.ts` | Add `useCirculation(uuid)` hook |
| F5 | `transmittals/[uuid]/page.tsx` | Wire banner + lifecycle with real data |
| F6 | `circulation/[uuid]/page.tsx` | Wire banner + lifecycle + overdue badge |
| F7 | `transmittals/page.tsx` | Verify list page works, add purpose filter |
### Guidelines (i18n + Tests)
| Task | File | Description |
|------|------|-------------|
| I1 | `public/locales/th/*.json` | Add missing transmittal/circulation workflow keys |
| T1 | `transmittal.service.spec.ts` | Unit test EC-RFA-004 submit validation |
| T2 | `circulation.service.spec.ts` | Unit tests EC-CIRC-001/002/003 handlers |
---
## Phase 3: Verification Plan
### Backend
```bash
# TypeScript compile
cd backend && pnpm tsc --noEmit
# Unit tests (target files)
cd backend && pnpm jest --testPathPattern="transmittal|circulation"
# Manual curl — Transmittal detail with workflowInstanceId
curl http://localhost:3001/api/transmittals/{uuid} | jq '.data.workflowInstanceId'
# Manual curl — Circulation detail with workflowInstanceId
curl http://localhost:3001/api/circulation/{uuid} | jq '.data.workflowInstanceId'
# EC-RFA-004 — submit transmittal with DRAFT item (expect 422)
curl -X POST http://localhost:3001/api/transmittals/{uuid}/submit
# Force Close test
curl -X POST http://localhost:3001/api/circulation/{uuid}/force-close \
-H "Content-Type: application/json" \
-d '{"reason":"Test force close"}'
```
### Frontend
```bash
# TypeScript compile (zero errors)
cd frontend && pnpm tsc --noEmit
# Lint (zero warnings)
cd frontend && pnpm lint
# Vitest unit tests
cd frontend && pnpm test
# Manual: Navigate to /transmittals/{uuid}
# → IntegratedBanner shows doc number + status badge
# → Workflow tab shows history timeline
# → workflowState shown in banner (if instance exists)
# Manual: Navigate to /circulation/{uuid}
# → Overdue badge on past-deadline routings
# → Workflow tab shows history
```
### Security Verification
- [ ] Reassign endpoint: 403 if user is not Document Control or above
- [ ] Force Close endpoint: 403 if user is not Document Control or above
- [ ] Workflow transition: `Idempotency-Key` header enforced
- [ ] No `parseInt` on any UUID in new code
- [ ] `workflowInstanceId` is string (not number) in all responses
---
## Dependencies Map
```
ADR-001 (Unified Workflow Engine)
└→ WorkflowEngineService.getInstanceByEntity() [B1]
├→ TransmittalService.findOneByUuid() [B2]
└→ CirculationService.findOneByUuid() [B6]
├→ types/transmittal.ts [F1]
├→ types/circulation.ts [F2]
├→ useTransmittal() [F3]
├→ useCirculation() [F4]
├→ transmittals/[uuid]/page.tsx [F5]
└→ circulation/[uuid]/page.tsx [F6]
ADR-016 (Security/RBAC)
└→ CirculationService.reassignRouting() [B7]
└→ CirculationService.forceClose() [B8]
ADR-021 (IntegratedBanner/WorkflowLifecycle components)
└→ Already implemented — just need instanceId prop wired [F5, F6]
```
---
## Risk Register
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|-----------|
| `entity_type` mismatch — Transmittal uses wrong entityType in WF instance | Medium | High | Check `workflowEngine.createInstance()` call in `transmittal.service.ts`; use same entityType string consistently |
| Circulation has no WF instance (workflow never started) | High | Medium | `getInstanceByEntity()` returns null → `workflowInstanceId` = undefined → banner shows status only, no actions |
| Transmittal entity has no `publicId` own column | Medium | Low | Already handled: `publicId` maps from `correspondence.publicId` in `findAll()` |
| EC-CIRC-003 timezone issues (deadline at 23:59:59) | Low | Medium | Use UTC comparison; test with mocked date |
+183
View File
@@ -0,0 +1,183 @@
# Feature Specification: Transmittals + Circulation Complete Integration (v1.8.7 Post-ADR-021)
**Feature Branch**: `001-transmittals-circulation`
**Version**: 1.8.7
**Created**: 2026-04-12
**Status**: Draft
**Depends On**: ADR-021 (Integrated Workflow Context & Step-specific Attachments — in `feat/adr-021-integrated-workflow-context`)
**Input**: "Transmittals + Circulation (v1.8.7) Post-ADR-021"
---
## Context
ADR-021 introduced the shared `IntegratedBanner`, `WorkflowLifecycle`, and `use-workflow-action` components, and wired them fully for RFA and Correspondence. Both the **Transmittal** and **Circulation** detail pages already import these components but pass no workflow data — they are currently stub wired.
This feature delivers the **complete, production-ready integration** of both modules with the ADR-021 Workflow Engine, fixes known type violations (ADR-019), implements all pending edge cases (EC-RFA-004, EC-CIRC-001003), and adds missing hooks and list-page functionality.
---
## User Scenarios & Testing _(mandatory)_
### User Story 1 — Transmittal Workflow-Wired Detail Page (Priority: P1) 🎯 MVP
A Document Control officer opens an existing Transmittal and immediately sees the document number, subject, workflow state, and available action buttons in the `IntegratedBanner`. The Workflow tab shows the full vertical timeline with each step's actor, date, comment, and evidence files.
**Why this priority**: The detail page is the primary touchpoint for reviewing/approving a Transmittal. Without live workflow data, reviewers cannot take action — this is a blocking gap.
**Independent Test**: Navigate to `/transmittals/{uuid}`. Verify: (1) `IntegratedBanner` shows real doc number, status badge, and action buttons (when user is the handler); (2) Workflow tab renders `WorkflowLifecycle` with at least the creation step; (3) all `pnpm tsc --noEmit` checks pass.
**Acceptance Scenarios**:
1. **Given** a submitted Transmittal with an active workflow instance, **When** a Document Control user opens its detail page, **Then** the `IntegratedBanner` displays the correct doc number, status, `workflowState`, and the available action buttons (e.g., APPROVE, REJECT).
2. **Given** a Transmittal where the current user is not the assigned handler, **When** they open the detail page, **Then** no action buttons are shown in the banner.
3. **Given** a Transmittal detail page, **When** the user clicks the Workflow tab, **Then** a vertical timeline displays all history steps with actor, date, and comment. The most recent step is highlighted.
---
### User Story 2 — Circulation Workflow-Wired Detail Page (Priority: P1) 🎯 MVP
A Document Control officer opens a Circulation Sheet and sees the circulation number, linked Correspondence, all assignees with their status, a deadline (with Overdue badge if past), and the full workflow timeline — all in one screen. Assignees can mark their task complete via the `IntegratedBanner` actions.
**Why this priority**: Circulation drives internal task tracking. Without live data wiring the page is read-only and useless for task management.
**Independent Test**: Navigate to `/circulation/{uuid}`. Verify: (1) `IntegratedBanner` displays `circulationNo`, `statusCode`; (2) Assignees card shows all routings with status; (3) Overdue badge appears when `deadline_date` is past; (4) Workflow tab shows history.
**Acceptance Scenarios**:
1. **Given** an OPEN Circulation with a past deadline, **When** a user opens the detail page, **Then** an Overdue badge is displayed and the deadline date is highlighted in red.
2. **Given** a Circulation with multiple assignees, **When** an assignee marks their task complete, **Then** their routing status updates to COMPLETED and the page refreshes.
3. **Given** a Circulation where all Main/Action assignees are COMPLETED, **When** the Document Control user views the page, **Then** a "Close Circulation" action is available.
---
### User Story 3 — Transmittal List Page with Search & Filter (Priority: P1)
A Document Control officer browses all Transmittals for a project, filters by purpose (FOR_APPROVAL, FOR_REVIEW, etc.), and searches by document number or subject.
**Why this priority**: The list page is the entry point for the module. Without working filters it cannot be used in production.
**Independent Test**: Navigate to `/transmittals`. Verify: paginated list loads; purpose filter updates results; search input filters by doc number/subject; clicking a row navigates to the detail page.
**Acceptance Scenarios**:
1. **Given** the Transmittals list page, **When** a user selects purpose "FOR_APPROVAL", **Then** only Transmittals with that purpose are shown.
2. **Given** the Transmittals list page, **When** a user types in the search box, **Then** results are filtered to matching document numbers or subjects within 500ms.
3. **Given** no Transmittals in the project, **When** the list page loads, **Then** an "empty state" message is shown.
---
### User Story 4 — Transmittal EC-RFA-004 Submit Validation (Priority: P2)
A Document Control officer tries to submit a Transmittal whose items include a DRAFT correspondence. The system blocks the submission with a clear, actionable error message.
**Why this priority**: EC-RFA-004 is a business integrity rule. Submitting a Transmittal with unsubmitted items violates the document lifecycle and must be blocked.
**Independent Test**: Create a Transmittal with one DRAFT item. Attempt to submit. Verify: 422 response with message "RFA [doc number] ยังอยู่ใน Draft กรุณา Submit ก่อน"; item is highlighted in the UI.
**Acceptance Scenarios**:
1. **Given** a Transmittal containing a DRAFT correspondence, **When** a user submits the Transmittal, **Then** the system returns an error identifying which item is in DRAFT status.
2. **Given** all Transmittal items are in SUBMITTED/APPROVED status, **When** a user submits the Transmittal, **Then** the submission succeeds and the status updates.
---
### User Story 5 — Circulation Edge Cases: Re-assign & Force Close (Priority: P2)
Document Control can re-assign a Circulation when an assignee is deactivated (EC-CIRC-001), and can force-close a Circulation with a mandatory reason when some assignees have not responded (EC-CIRC-002).
**Why this priority**: Without these controls, Circulations can get permanently stuck, blocking downstream work.
**Independent Test**: Deactivate an assignee in an OPEN Circulation. Verify: Document Control sees a "Re-assign" button for that routing. Force-close a Circulation with partial responses; verify reason is recorded in audit log.
**Acceptance Scenarios**:
1. **Given** an OPEN Circulation where one assignee has been deactivated, **When** Document Control opens the page, **Then** a "Re-assign" action is available for that assignee's routing.
2. **Given** an OPEN Circulation where some assignees have not responded, **When** Document Control performs Force Close with a reason, **Then** the Circulation status changes to CANCELLED, all pending routings are force-closed, and the reason is logged.
---
### Edge Cases
- **EC-RFA-004**: Transmittal with DRAFT items cannot be submitted → `422 Unprocessable Entity` with item identification.
- **EC-CIRC-001**: Assignee deactivated before responding → Document Control can re-assign.
- **EC-CIRC-002**: Multi-assignee, some not responded → Document Control can Force Close with mandatory reason.
- **EC-CIRC-003**: Deadline = today `23:59:59`; Overdue Badge the following day at `00:00`.
- **EC-CORR-001**: Cancelling a Correspondence with open Circulations → all Circulations force-closed + audit log.
- Transmittal `workflowInstanceId` is `null` when no workflow has been started (Draft state) → banner shows status only, no action buttons.
- Circulation data is scoped to the user's organization — users from other organizations must receive a 403 response.
- Duplicate `Idempotency-Key` on workflow transition → return cached response, no re-processing.
---
## Requirements _(mandatory)_
### Functional Requirements
**Transmittal Module:**
- **FR-T01**: The Transmittal detail page MUST display `workflowState`, `availableActions`, and action buttons via `IntegratedBanner` using the live workflow instance.
- **FR-T02**: The Transmittal detail page Workflow tab MUST render `WorkflowLifecycle` wired to the workflow history of the Transmittal's workflow instance.
- **FR-T03**: The Transmittal list page MUST support pagination, search by document number/subject, and filter by `purpose`.
- **FR-T04**: The Transmittal `Transmittal` frontend type MUST include `workflowInstanceId?: string` and `workflowState?: string` fields (ADR-019: string UUID only).
- **FR-T05**: The `transmittalService.getByUuid()` response MUST include `workflowInstanceId` from the backend.
- **FR-T06**: A dedicated `useTransmittal(uuid)` TanStack Query hook MUST be created for the detail page.
- **FR-T07**: Submitting a Transmittal with DRAFT items MUST return a `422` error identifying the offending item (EC-RFA-004).
**Circulation Module:**
- **FR-C01**: The Circulation detail page MUST display `workflowState`, `availableActions`, and action buttons via `IntegratedBanner` using the live workflow instance.
- **FR-C02**: The Circulation detail page Workflow tab MUST render `WorkflowLifecycle` wired to the workflow history.
- **FR-C03**: The Circulation detail page assignee section MUST display deadline per assignee type and an Overdue badge when `NOW() > deadline_date + 1 day` (EC-CIRC-003).
- **FR-C04**: The Circulation `Circulation` frontend type MUST include `workflowInstanceId?: string` and `workflowState?: string`.
- **FR-C05**: The `circulationService.getByUuid()` response MUST include `workflowInstanceId` from the backend.
- **FR-C06**: A dedicated `useCirculation(uuid)` TanStack Query hook MUST be created for the detail page.
- **FR-C07**: Document Control MUST be able to re-assign a routing when the assignee is deactivated (EC-CIRC-001).
- **FR-C08**: Document Control MUST be able to Force Close a Circulation with a mandatory reason; all pending routings are force-closed and the reason is logged in the audit trail (EC-CIRC-002).
**Cross-Cutting:**
- **FR-X01**: All new API calls MUST use `publicId` (UUIDv7 string) — no `parseInt` on UUID values (ADR-019).
- **FR-X02**: All new frontend types MUST NOT use `any` — strict TypeScript required.
- **FR-X03**: All backend responses for these modules MUST include `workflowInstanceId?: string` in the data shape.
- **FR-X04**: All new user-facing strings MUST use i18n keys — no hardcoded Thai/English text in JSX.
### Key Entities
- **Transmittal**: Extends Correspondence (`type_code = 'TRANSMITTAL'`). Has `purpose`, `remarks`, and a list of `transmittal_items`. Has one `WorkflowInstance` via the Unified Workflow Engine.
- **TransmittalItem**: Links a Transmittal to the document it carries (`correspondences` M:N). Has `quantity`, `itemType`, `remarks`.
- **Circulation**: Internal task-tracking document linked 1:1 to a Correspondence per organization. Has `statusCode`, `deadline`, and a list of `routings` (assignees with type: Main/Action/Information).
- **CirculationRouting**: A single assignee entry in a Circulation. Has `assigneeType`, `status`, `deadline`, `comments`.
---
## Assumptions
- ADR-021 backend is fully deployed — `workflow_history_id` column exists on `attachments`, `workflowInstanceId` is exposed from the Workflow Engine module.
- The backend `transmittal` module already has a `workflowInstance` relation or can join it via the Correspondence FK chain.
- The backend `circulation` module already has a `workflowInstance` relation available.
- The Unified Workflow Engine (`WorkflowEngineService`) is the single source of truth for state and transitions — Transmittal and Circulation statuses are NOT independently maintained once a workflow is started.
---
## Success Criteria _(mandatory)_
### Measurable Outcomes
- **SC-001**: Both Transmittal and Circulation detail pages display live workflow state and action buttons in under 1 second after page load (TanStack Query with staleTime 60s).
- **SC-002**: Submitting a Transmittal with a DRAFT item is rejected 100% of the time with a user-readable error message identifying the offending document.
- **SC-003**: Force-closing a Circulation with partial responses succeeds in a single action with the mandatory reason captured in the audit log every time.
- **SC-004**: All new TypeScript code passes `pnpm tsc --noEmit` with zero errors and `pnpm lint` with zero warnings.
- **SC-005**: No hardcoded Thai or English text in any new JSX component — verified by grep.
- **SC-006**: Unit test coverage ≥ 80% on new business logic (EC-RFA-004 validation, EC-CIRC-001/002/003 handlers).
- **SC-007**: Overdue Badge appears correctly when `NOW() > deadline_date + 1 day` — verified by unit test with mocked date.
---
## Clarifications
### Session 2026-04-12
- Q: Should the Transmittal "Submit" action go through the Workflow Engine transition (ADR-021 pattern), or does it remain a direct status update on the `correspondences` table? → A: Submit uses the Workflow Engine transition (`action: 'SUBMIT'`), consistent with ADR-001 Unified Workflow Engine for all document types. EC-RFA-004 validation fires as a pre-transition check in the service.
- Q: Should Circulation `routings` "Complete" action be a Workflow Engine transition or a direct routing status update? → A: Direct routing status update (not a full Workflow Engine transition) because Circulation workflow state is controlled at the Circulation level, not per-routing. The overall Circulation transitions (OPEN → IN_REVIEW → COMPLETED) go through the Workflow Engine.
- Q: For `workflowInstanceId` in the Transmittal/Circulation API response — should it be added to the existing response shape or is a dedicated `/workflow` sub-resource needed? → A: Add `workflowInstanceId` directly to the existing `findOneByUuid` response shape (additive, backward-compatible). Consistent with how RFA/Correspondence expose it.
+182
View File
@@ -0,0 +1,182 @@
# Tasks: Transmittals + Circulation Complete Integration (v1.8.7 Post-ADR-021)
**Branch**: `001-transmittals-circulation` | **Total Tasks**: 18 | **Phase**: Implementation
---
## Phase 1 — Backend Foundation (Critical — blocks all frontend work)
### B1 — WorkflowEngineService: Add `getInstanceByEntity()`
- **File**: `backend/src/modules/workflow-engine/workflow-engine.service.ts`
- **Action**: Add method that queries `WorkflowInstance` by `entityType + entityId`; returns `{ id, currentState, availableActions? } | null`
- **Dependencies**: none
- **Status**: [ ]
### B2 — TransmittalService: Expose `workflowInstanceId` in `findOneByUuid()`
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
- **Action**: Call `workflowEngine.getInstanceByEntity('transmittal', correspondenceId.toString())` and merge `workflowInstanceId`, `workflowState` into response
- **Dependencies**: B1
- **Status**: [ ]
### B3 — TransmittalService: Add `purpose` filter to `findAll()`
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
- **Action**: Add `purpose?: string` to `SearchTransmittalDto` and apply `andWhere` in `findAll()`
- **Dependencies**: none (parallel with B1)
- **Status**: [ ]
### B4 — TransmittalService: Add `submit()` with EC-RFA-004 validation
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
- **Action**: New `submit(uuid, user)` method; fetches all `transmittal_items`, checks each item's correspondence current revision status — throws `422 ValidationException` if any is `DRAFT`; then calls `workflowEngine.createInstance('TRANSMITTAL_FLOW_V1', 'transmittal', ...)` and transitions with `SUBMIT`
- **Dependencies**: B1
- **Status**: [ ]
### B5 — TransmittalController: Add `POST /:uuid/submit` endpoint
- **File**: `backend/src/modules/transmittal/transmittal.controller.ts`
- **Action**: Add endpoint with `@RequirePermission('document.manage')`, `@Audit('transmittal.submit', 'transmittal')`
- **Dependencies**: B4
- **Status**: [ ]
### B6 — CirculationService: Expose `workflowInstanceId` in `findOneByUuid()`
- **File**: `backend/src/modules/circulation/circulation.service.ts`
- **Action**: Call `workflowEngine.getInstanceByEntity('circulation', circulation.id.toString())`, merge into response; also compute `isOverdue` per routing based on `deadline_date`
- **Dependencies**: B1
- **Status**: [ ]
### B7 — CirculationService: Add `reassignRouting()` (EC-CIRC-001)
- **File**: `backend/src/modules/circulation/circulation.service.ts`
- **Action**: Fetch routing, verify user has Document Control permission, resolve `newAssigneeUuid` → INT via `uuidResolver.resolveUserId()`, update `routing.assignedTo`, write audit log
- **Dependencies**: none
- **Status**: [ ]
### B8 — CirculationService: Add `forceClose()` (EC-CIRC-002)
- **File**: `backend/src/modules/circulation/circulation.service.ts`
- **Action**: Require `reason` (non-empty), update all PENDING routings to `CANCELLED`, set `circulation.statusCode = 'CANCELLED'`, write audit log entry; use `queryRunner` for atomicity
- **Dependencies**: none
- **Status**: [ ]
### B9 — CirculationController: Add reassign + force-close endpoints
- **File**: `backend/src/modules/circulation/circulation.controller.ts`
- **Action**:
- `PATCH /:uuid/routing/:routingId/reassign``@RequirePermission('circulation.manage')`
- `POST /:uuid/force-close``@RequirePermission('circulation.manage')`
- **Dependencies**: B7, B8
- **Status**: [ ]
---
## Phase 2 — Frontend Types & Hooks (Important — depends on Phase 1)
### F1 — Update `types/transmittal.ts`
- **File**: `frontend/types/transmittal.ts`
- **Action**: Add `workflowInstanceId?: string`, `workflowState?: string`, `availableActions?: string[]` to `Transmittal` interface; add `purpose?: string` to `SearchTransmittalDto`; no `any` types (ADR-019)
- **Dependencies**: none (parallel with Phase 1)
- **Status**: [ ]
### F2 — Update `types/circulation.ts`
- **File**: `frontend/types/circulation.ts`
- **Action**: Add `workflowInstanceId?: string`, `workflowState?: string`, `availableActions?: string[]` to `Circulation`; add `deadline?: string`, `assigneeType?: 'MAIN' | 'ACTION' | 'INFORMATION'` to `CirculationRouting`
- **Dependencies**: none
- **Status**: [ ]
### F3 — Create `hooks/use-transmittal.ts`
- **File**: `frontend/hooks/use-transmittal.ts`
- **Action**: Create `useTransmittal(uuid: string | undefined)` with `queryKey: ['transmittal', uuid]`, `staleTime: 60_000`; export `transmittalKeys` query key factory
- **Dependencies**: F1
- **Status**: [ ]
### F4 — Update `hooks/use-circulation.ts`
- **File**: `frontend/hooks/use-circulation.ts`
- **Action**: Add `useCirculation(uuid: string | undefined)` hook with `queryKey: ['circulation', uuid]`, `staleTime: 60_000`
- **Dependencies**: F2
- **Status**: [ ]
---
## Phase 3 — Frontend Detail Pages (Important — depends on Phase 2 + Phase 1 deployed)
### F5 — Wire Transmittal detail page
- **File**: `frontend/app/(dashboard)/transmittals/[uuid]/page.tsx`
- **Action**:
- Replace inline `useQuery` with `useTransmittal(uuid)`
- Add `useWorkflowHistory(transmittal?.workflowInstanceId)`
- Add `const [pendingAttachmentIds, setPendingAttachmentIds] = useState<string[]>([])`
- Pass `instanceId`, `workflowState`, `availableActions`, `pendingAttachmentIds` to `IntegratedBanner`
- Pass `history`, `currentState`, `isLoading`, `error`, `onAttachmentsChange` to `WorkflowLifecycle` in Workflow tab
- **Dependencies**: F3, F1
- **Status**: [ ]
### F6 — Wire Circulation detail page
- **File**: `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
- **Action**:
- Replace inline `useQuery` with `useCirculation(uuid)`
- Add `useWorkflowHistory(circulation?.workflowInstanceId)`
- Add `isOverdue(deadline?)` helper function
- Wire `IntegratedBanner` with `instanceId`, `workflowState`, `availableActions`
- Wire `WorkflowLifecycle` with history in Workflow tab
- Add Overdue badge to routing rows where `isOverdue(routing.deadline)` is true
- Replace hardcoded "Complete" button with proper workflow action
- **Dependencies**: F4, F2
- **Status**: [ ]
---
## Phase 4 — List Page & i18n (Guidelines)
### F7 — Transmittal list page: add purpose filter
- **File**: `frontend/app/(dashboard)/transmittals/page.tsx`
- **Action**: Add `purpose` select filter (FOR_APPROVAL / FOR_INFORMATION / FOR_REVIEW / OTHER) passing to `transmittalService.getAll()`. Read current page to assess if pagination works.
- **Dependencies**: F1
- **Status**: [ ]
### I1 — i18n keys for Transmittal/Circulation workflow
- **Files**: `public/locales/th/*.json`, `public/locales/en/*.json`
- **Action**: Check `use-translations.ts` for key lookup pattern; add missing keys: `transmittal.purpose.*`, `circulation.status.*`, `circulation.overdue`, `circulation.forceClose.*`, `circulation.reassign.*`
- **Dependencies**: F5, F6
- **Status**: [ ]
---
## Phase 5 — Tests (Tier 2 — required before merge)
### T1 — Transmittal service EC-RFA-004 unit test
- **File**: `backend/src/modules/transmittal/transmittal.service.spec.ts` (create if needed)
- **Action**: Test `submit()` throws `ValidationException` when item correspondence is DRAFT; test passes when all items are SUBMITTED
- **Dependencies**: B4
- **Status**: [ ]
### T2 — Circulation service edge-case unit tests
- **File**: `backend/src/modules/circulation/circulation.service.spec.ts` (create if needed)
- **Action**: Test `reassignRouting()` — permission check, assignment update; test `forceClose()` — all pending routings cancelled, reason logged; test `isOverdue` helper (EC-CIRC-003)
- **Dependencies**: B7, B8
- **Status**: [ ]
---
## Execution Order
```
B1 (parallel: B3, F1, F2)
→ B2, B4 (parallel), B6 (parallel)
→ B5 → T1
→ B7, B8 (parallel)
→ B9 → T2
→ F3, F4 (parallel after F1, F2)
→ F5, F6 (parallel after F3, F4)
→ F7, I1 (polish)
```
---
## Commit Message Convention
```
feat(transmittal): expose workflowInstanceId in findOneByUuid response
feat(circulation): expose workflowInstanceId + overdue in findOneByUuid
feat(circulation): add reassignRouting EC-CIRC-001 handler
feat(circulation): add forceClose EC-CIRC-002 handler
feat(transmittal): add submit endpoint with EC-RFA-004 validation
feat(frontend): wire WorkflowLifecycle in transmittal detail page
feat(frontend): wire WorkflowLifecycle + overdue badge in circulation detail
test(transmittal): EC-RFA-004 submit validation unit tests
test(circulation): EC-CIRC-001/002/003 edge case unit tests
```