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'`.
2.**Transmittal entityType**: Use `'transmittal'`; join via `workflow_instances WHERE entity_type = 'TRANSMITTAL' AND entity_id = correspondences.id` (string). Consistent with RFA pattern. **No separate FK column added.**
3.**Circulation entityType**: Use `'circulation'` (entity ID = `circulation.id.toString()`). The Circulation entity already extends `UuidBaseEntity` (has `publicId`).
3.**Circulation entityType**: Use `'circulation'`; join via `workflow_instances WHERE entity_type = 'CIRCULATION' AND entity_id = circulations.id` (string). 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.
6.**EC-CIRC-001 (Reassign)**: New `CirculationService.reassignRouting(routingId, newAssigneeUuid, user)`. Requires `Document Control` or above role (`ability.can('reassign', 'Circulation')`). 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.
7.**EC-CIRC-002 (Force Close) — Synchronous, ≤3s SLA**: New `CirculationService.forceClose(uuid, reason, user)`. Requires `Document Control` or above (`ability.can('forceClose', 'Circulation')`). All routing status updates + audit log committed in **a single DB transaction**; respond `200 OK` after commit. BullMQ notification jobs enqueued **post-commit** (not inside transaction). SLA: ≤ 3 seconds for ≤ 50 routings.
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.
8.**EC-CIRC-003 (Overdue) — Server-side**: `CirculationService.findOneByUuid()` computes `isOverdue = NOW() > deadline_date + INTERVAL 1 DAY` per routing and returns`isOverdue: boolean` in the response. **Frontend MUST NOT compute overdue independently** — render badge based solely on backend field. Unit test: `CirculationService` spec with mocked `Date`/`ClockService`.
9.**EC-CORR-001 (Cancel Correspondence → Force-close Circulations)**: When Correspondence is cancelled, all OPEN Circulations are force-closed + audit log written. A BullMQ job is enqueued **per affected assignee** with pending routing on queue `notification-queue`. Job payload: `{ circulationNo, correspondenceNo, cancellationReason }`. No inline notification.
10.**Close Circulation (FR-C09) RBAC**: The "Close Circulation" action (all Main/Action assignees COMPLETED) is exposed **only to Document Control** — guarded by `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` with `ability.can('close', 'Circulation')`. Frontend hides button for all other roles.
---
@@ -136,11 +140,14 @@ The `WorkflowInstance` table already exists with `entity_type VARCHAR`, `entity_
"workflowInstanceId":"019def...",
"workflowState":"OPEN",
"availableActions":[],
"isOverdue":false,
"routings":[
{
"id":1,
"assigneePublicId":"...",
"assigneeType":"MAIN",
"deadline":"2026-04-20T00:00:00.000Z",
"isOverdue":true,
"status":"PENDING",
...
}
],
@@ -149,6 +156,8 @@ The `WorkflowInstance` table already exists with `entity_type VARCHAR`, `entity_
}
```
> ⚠️ `isOverdue` is computed **server-side** in `CirculationService` (`NOW() > deadline_date + INTERVAL 1 DAY`). Frontend must NOT recompute this field.
#### `POST /transmittals/:uuid/submit` (NEW)
**Request**:
@@ -166,7 +175,14 @@ The `WorkflowInstance` table already exists with `entity_type VARCHAR`, `entity_
| F6 | `circulation/[uuid]/page.tsx` | Wire banner + lifecycle + Overdue badge from `routing.isOverdue` (backend field — no client-side date math); show "Close Circulation" button only when `canClose && user.role === 'DOCUMENT_CONTROL'` |
| `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 |
| `entity_type` mismatch — Transmittal uses wrong entityType in WF instance | Medium | High | Join via `entity_type='TRANSMITTAL'` + `entity_id=correspondences.id` consistently in `getInstanceByEntity()` |
| 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 |
| EC-CIRC-003 timezone — server TZ differs from Bangkok | Low | High | Compute `isOverdue` in MariaDB with `NOW()` (server TZ is UTC+7 per docker-compose); verify in unit test with fixed timestamp |
| Force Close latency > 3s with 50+ routings | Low | Medium | Use bulk UPDATE query (not loop), wrap in single transaction; add integration test SC-008 |
| EC-CORR-001 notification lost if BullMQ unavailable | Low | High | BullMQ persistence (Redis AOF) ensures job survives restarts; dead-letter queue `notification-queue-failed` alerts ops |
@@ -47,7 +47,7 @@ A Document Control officer opens a Circulation Sheet and sees the circulation nu
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.
3.**Given** a Circulation where all Main/Action assignees are COMPLETED, **When**a **Document Control** user views the page, **Then** a "Close Circulation" action is available (other roles must NOT see this button).
---
@@ -102,8 +102,8 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
- **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.
- **EC-CIRC-003**: Deadline = today `23:59:59`; Overdue Badge the following day at `00:00`. Overdue is computed **server-side** — backend returns `isOverdue: boolean` in the Circulation response. Client renders badge based solely on this field (no client-side date math).
- **EC-CORR-001**: Cancelling a Correspondence with open Circulations → all Circulations force-closed + audit log + **BullMQ notification** (email + in-app) dispatched to all affected assignees with pending routings (ADR-008). No inline notification — must be queued job.
- 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.
@@ -120,7 +120,7 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
- **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-T05**: The `transmittalService.getByUuid()` response MUST include `workflowInstanceId`resolved via `workflow_instances JOIN ON reference_type = 'TRANSMITTAL' AND reference_id = correspondences.id` — no separate FK column on the Transmittal entity.
- **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).
@@ -128,12 +128,13 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
- **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-C03**: The Circulation detail page assignee section MUST display deadline per assignee type and an Overdue badge based on the backend-provided `isOverdue: boolean` field in the API response (EC-CIRC-003). The frontend MUST NOT compute overdue status independently.
- **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).
- **FR-C08**: Document Control MUST be able to Force Close a Circulation with a mandatory reason. The operation MUST be **synchronous** — all routing status updates and the audit log entry are committed in a single DB transaction before the `200 OK` response is returned. BullMQ notification jobs are enqueued **after** the transaction commits. **SLA: ≤ 3 seconds** for a Circulation with up to 50 routings (EC-CIRC-002).
- **FR-C09**: The "Close Circulation" action (triggered when all Main/Action assignees are COMPLETED) is available to **Document Control only** — guarded by `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` with `ability.can('close', 'Circulation')`. Assignees and other roles must NOT see the button.
**Cross-Cutting:**
@@ -141,6 +142,7 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
- **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.
- **FR-X05**: When a Correspondence is cancelled and open Circulations are force-closed (EC-CORR-001), a BullMQ notification job MUST be enqueued for each affected assignee with a pending routing — delivering both email and in-app notifications (ADR-008). The job payload MUST include `circulationNo`, `correspondenceNo`, and `cancellationReason`.
### Key Entities
@@ -154,8 +156,8 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
## 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 backend `transmittal` module resolves`workflowInstanceId`by joining `workflow_instances` on `reference_type = 'TRANSMITTAL'` and `reference_id = correspondences.id` — **no separate FK column** is required on the Transmittal side.
- The backend `circulation` module resolves `workflowInstanceId` by joining`workflow_instances`on `reference_type = 'CIRCULATION'` and `reference_id = circulations.id`.
- 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.
---
@@ -170,7 +172,8 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
- **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.
- **SC-007**: `isOverdue: boolean` is computed correctly on the backend when `NOW() > deadline_date + 1 day` — verified by a **backend unit test** in `CirculationService` with mocked `Date` (or injected `ClockService`). Frontend unit test verifies badge renders when `isOverdue === true`.
- **SC-008**: Force Close Circulation (FR-C08) completes within **3 seconds** under a Circulation with 50 routings — verified by a backend integration test measuring total transaction time.
---
@@ -190,4 +193,12 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC
- Q: Workflow Instance Binding — which revision should the workflow bind to? → A: **A** — Bind to current revision only. New revision becomes the new workflow target. Historical revisions remain for audit but are no longer part of active workflow.
- Q: Document Numbering on Revision — should doc number change on revision? → A: **B** — Keep same document number, revision label distinguishes versions (follows Correspondence pattern).
### Session 2026-05-03
- Q: Who can trigger the "Close Circulation" action when all Main/Action assignees are COMPLETED? → A: **A** — Document Control only, consistent with ADR-016 and FR-C08. Guarded by `CaslAbilityGuard` with `ability.can('close', 'Circulation')`.
- Q: Should EC-CORR-001 (cancel Correspondence → force-close Circulations) trigger notifications to affected assignees? → A: **A** — Yes, BullMQ notification (email + in-app) to all affected assignees with pending routings (ADR-008). Job payload: `circulationNo`, `correspondenceNo`, `cancellationReason`. See FR-X05.
- Q: Where should the Overdue determination for Circulation deadline (EC-CIRC-003) be computed? → A: **A** — Server-side. Backend returns `isOverdue: boolean` in the Circulation response; client renders badge based solely on this field. Unit test targets `CirculationService` with mocked server time. See FR-C03, SC-007.
- Q: Where does `workflowInstanceId` bind in the data model for a Transmittal? → A: **A** — On the `correspondences` row; join via `workflow_instances ON reference_type = 'TRANSMITTAL' AND reference_id = correspondences.id`. No new FK column needed. See FR-T05 and updated Assumptions.
- Q: Should Force Close Circulation (FR-C08) be synchronous or asynchronous, and what is the latency SLA? → A: **A** — Synchronous; all routing updates + audit log in a single DB transaction; `200 OK` after commit; BullMQ notifications enqueued post-commit. SLA: ≤ 3 seconds for up to 50 routings. See FR-C08, SC-008.
- **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**: [x]
- [x] T003 [P] Add `purpose?: string` to `SearchTransmittalDto` and apply `andWhere` filter in `TransmittalService.findAll()` in `backend/src/modules/transmittal/transmittal.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**: [x]
- [x] T005 Add `POST /:uuid/submit` endpoint with `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` + `@Audit('transmittal.submit', 'transmittal')` in `backend/src/modules/transmittal/transmittal.controller.ts`
- **Action**: Fetch routing, verify user has Document Control permission, resolve `newAssigneeUuid` → INT via `uuidResolver.resolveUserId()`, update `routing.assignedTo`, write audit log
- **Action**: Require `reason` (non-empty), update all PENDING routings to `CANCELLED`, set `circulation.statusCode = 'CANCELLED'`, write audit log entry; use `queryRunner` for atomicity
- [x] T008 [P] Add `CirculationService.forceClose(uuid, reason, user)` — single `queryRunner` transaction: update all PENDING routings to `CANCELLED`, set `circulation.statusCode = 'CANCELLED'`, write audit log; enqueue BullMQ `notification-queue` job **post-commit** per affected assignee (payload: `{ circulationNo, correspondenceNo, cancellationReason }`); verify `ability.can('forceClose', 'Circulation')` in `backend/src/modules/circulation/circulation.service.ts`
- [x] T009 Add `CirculationService.close(uuid, user)` — verify `ability.can('close', 'Circulation')` (Document Control only); pre-condition check: ALL Main/Action routings must be COMPLETED (throw `422` if not); update `circulation.statusCode = 'CLOSED'`; write audit log in `backend/src/modules/circulation/circulation.service.ts`
- [x] T010 Add PATCH `/:uuid/routing/:routingId/reassign` + POST `/:uuid/force-close`endpoints with `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` in `backend/src/modules/circulation/circulation.controller.ts`
- [x] T011 Add POST `/:uuid/close` endpoint with `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` (`ability.can('close', 'Circulation')`) + `@Audit('circulation.close', 'circulation')` in `backend/src/modules/circulation/circulation.controller.ts`
- [x] T012 Add EC-CORR-001 cascade handler in `CorrespondenceService.cancel()` — on cancel: find all OPEN Circulations for this correspondence; call `CirculationService.forceClose()` per Circulation; enqueue BullMQ `notification-queue` job per **affected assignee with pending routing** (payload: `{ circulationNo, correspondenceNo, cancellationReason }`); write combined audit log in `backend/src/modules/correspondence/correspondence.service.ts`
- Pass `instanceId`, `workflowState`, `availableActions`, `pendingAttachmentIds` to `IntegratedBanner`
- Pass `history`, `currentState`, `isLoading`, `error`, `onAttachmentsChange` to `WorkflowLifecycle` in Workflow tab
- **Dependencies**: F3, F1
- **Status**: [x]
> **Story Goal**: Doc Control officer sees live doc number, `workflowState`, and action buttons in `IntegratedBanner`; Workflow tab shows full timeline.
> **Independent Test**: Navigate to `/transmittals/{uuid}` — verify `IntegratedBanner` shows real state + actions; Workflow tab renders at least creation step; `pnpm tsc --noEmit` zero errors.
- 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**: [x]
- [x] T017 [US1] Wire `IntegratedBanner` with `instanceId={transmittal.workflowInstanceId}`, `workflowState`, `availableActions` from `useTransmittal()` hook; wire `WorkflowLifecycle` in Workflow tab with `useWorkflowHistory(instanceId)` in `frontend/app/(dashboard)/transmittals/[uuid]/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**: [x]
> **Story Goal**: Doc Control sees circulation number, assignees with deadline, Overdue badge (from backend `isOverdue` field), full workflow timeline, and "Close Circulation" button (Document Control only when all Main/Action COMPLETED).
> **Independent Test**: Navigate to `/circulation/{uuid}` — verify `IntegratedBanner` shows `circulationNo`, `statusCode`; Overdue badge appears when `routing.isOverdue === true`; "Close Circulation" hidden for non-Document Control users.
### I1 — i18n keys for Transmittal/Circulation workflow
- [x] T018 [US2] Wire `IntegratedBanner` + `WorkflowLifecycle` from `useCirculation()` + `useWorkflowHistory()` in `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
- [ ] T019 [US2] Add Overdue badge to routing rows — render badge when `routing.isOverdue === true` (backend field only; **FORBIDDEN: no client-side `new Date()` comparison**); highlight `deadline` date in red in `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
- [ ] T020 [US2] Show "Close Circulation" button conditionally — visible only when `user.role === 'DOCUMENT_CONTROL'` AND all Main/Action routings are `COMPLETED`; calls `POST /:uuid/close`; hide completely for all other roles in `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
> **Story Goal**: Doc Control blocked from submitting Transmittal with DRAFT items; 422 error clearly identifies the offending document.
> **Independent Test**: Create Transmittal with DRAFT item → submit → expect 422 with offending doc number in error message; item highlighted in UI.
- [x] T022 [US4] Verify UI shows 422 error from `POST /transmittals/:uuid/submit` with item-level identification — display `userMessage` from ADR-007 error response in `frontend/app/(dashboard)/transmittals/[uuid]/page.tsx`
---
## Phase 7 — US5: Circulation Edge Cases — Re-assign & Force Close (P2)
> **Story Goal**: Doc Control can re-assign deactivated assignee (EC-CIRC-001) and force-close stuck Circulation with mandatory reason (EC-CIRC-002).
> **Independent Test**: Deactivate assignee in OPEN Circulation → Re-assign button visible. Force-close with reason → status CANCELLED, reason in audit log.
- [x] T023 [US5] Add Re-assign UI — show "Re-assign" action button for deactivated assignee routings; open modal with user search; calls `PATCH /:uuid/routing/:routingId/reassign` in `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
- [x] T024 [US5] Add Force Close UI — "Force Close" button (Document Control only); modal requires mandatory reason field; calls `POST /:uuid/force-close`; invalidate `['circulation', uuid]` query on success in `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
- [x] T025 Unit test `TransmittalService.submit()` — throws `ValidationException` (422) when any item correspondence is DRAFT; passes when all items are SUBMITTED/APPROVED in `backend/src/modules/transmittal/transmittal.service.spec.ts`
- [x] T026 Unit test `CirculationService.reassignRouting()` — permission check throws 403 for non-Document Control; updates `routing.assignedTo` correctly in `backend/src/modules/circulation/circulation.service.spec.ts`
- [x] T027 Unit test `CirculationService.forceClose()` — all PENDING routings set to CANCELLED in single transaction; mandatory reason logged; BullMQ `notification-queue` job enqueued post-commit (verify with mock queue) in `backend/src/modules/circulation/circulation.service.spec.ts`
- [ ] T028 Unit test `CirculationService.findOneByUuid()` — `isOverdue: boolean` computed correctly via server-side logic with mocked `Date` (or injected `ClockService`): `true` when `now > deadline + 1 day`, `false` when `now <= deadline + 1 day`, `false` when `deadline` is null (SC-007) in `backend/src/modules/circulation/circulation.service.spec.ts`
- [ ] T029 Unit test `CirculationService.close()` — throws 403 for non-Document Control; throws 422 when any Main/Action routing is not COMPLETED; succeeds and sets status to CLOSED when all COMPLETED in `backend/src/modules/circulation/circulation.service.spec.ts`
- [ ] T030 Unit test EC-CORR-001 — `CorrespondenceService.cancel()` enqueues BullMQ notification job per affected assignee; payload includes `circulationNo`, `correspondenceNo`, `cancellationReason`; audit log written in `backend/src/modules/correspondence/correspondence.service.spec.ts`
- [ ] T031 Integration test `CirculationService.forceClose()` with 50 routings — total transaction time ≤ 3 seconds (SC-008); use bulk UPDATE query not loop in `backend/src/modules/circulation/circulation.service.spec.ts`
- [ ] T032 Frontend unit test — Overdue badge renders when `routing.isOverdue === true` (no client-side date math); badge absent when `routing.isOverdue === false`; snapshot test for both states in `frontend/app/(dashboard)/circulation/[uuid]/__tests__/page.test.tsx`
- [ ] T033 Frontend unit test — "Close Circulation" button visible for `DOCUMENT_CONTROL` role only; hidden for `SUPERVISOR`, `ASSIGNEE`, `VIEWER` roles in `frontend/app/(dashboard)/circulation/[uuid]/__tests__/page.test.tsx`
---
## Phase 9 — i18n Polish (Guidelines)
- [ ] T034 [P] Add Thai i18n keys for force-close modal, close action, overdue badge, EC-CORR-001 notification in `frontend/public/locales/th/circulation.json` — keys: `circulation.forceClose.title`, `circulation.forceClose.reason`, `circulation.close.confirm`, `circulation.overdue`, `circulation.notification.cancelledByCorrespondence`
- [ ] T035 [P] Add English i18n keys matching Thai keys above in `frontend/public/locales/en/circulation.json`
---
## Phase 10 — Transmittal Revision Refactor (v1.8.8 — Based on Clarifications 2026-04-29)
> **Goal**: Restructure Transmittal to follow Master-Revision Pattern (like RFA/Correspondence).
- [ ] T036 Schema: Add `revision_id INT NULL` with FK to `correspondence_revisions(id)` + index to `transmittal_items`; add `item_type VARCHAR(50) NULL` column (ADR-009 direct SQL) in `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`
- [ ] T042 Update `TransmittalService.submit()` — EC-RFA-004 checks current revision items only; workflow instance binds to `correspondence_revisions.publicId` (UUID string, ADR-019 — NOT revision.id INT) in `backend/src/modules/transmittal/transmittal.service.ts`
- [ ] T043 Update `TransmittalService.create()` — write `purpose`/`remarks` to `CorrespondenceRevision.details` JSON; replace hardcoded `ORG_CODE: 'ORG'` with real org lookup (`organizationCode` from `Organization` entity, pattern: `correspondence.service.ts:263-269`); save `itemType` from DTO in `backend/src/modules/transmittal/transmittal.service.ts`
- [ ] T044 Schema: Drop `purpose` and `remarks` from `transmittals` table (ADR-009 direct SQL — deploy AFTER T043 is live); remove corresponding TypeORM columns from entity in `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` + `backend/src/modules/transmittal/transmittal.entity.ts`
- [ ] T045 Fix `TransmittalItemDto.itemId` — change `itemId: number` + `@IsInt()` → `itemId: string` + `@IsUUID()`; resolve UUID→INT via `uuidResolver.resolveCorrespondenceId()` in service (ADR-019 CRITICAL) in `backend/src/modules/transmittal/dto/create-transmittal.dto.ts` + `backend/src/modules/transmittal/transmittal.service.ts`
- [ ] T046 [P] Add revision fields to frontend types — `revisionId?: string`, `revisionNumber?: number`, `revisionLabel?: string` to `Transmittal`; `revisionId?: string` to `TransmittalItem`; update `useTransmittal(uuid, revisionId?)` hook in `frontend/types/transmittal.ts` + `frontend/hooks/use-transmittal.ts`
- [ ] T047 Add revision selector to Transmittal detail page — dropdown when multiple revisions exist (pattern: RFA detail page); display `revisionLabel` (A, B, C) in `IntegratedBanner` in `frontend/app/(dashboard)/transmittals/[uuid]/page.tsx`
- [ ] T048 ADR-019 compliance scan — verify all revision-related fields use `publicId` (string UUID) not `id` (INT) in both backend responses and frontend types; run `grep -rn "parseInt\|Number(\|\.id[^a-zA-Z]"` on new code in `backend/src/modules/transmittal/` + `frontend/types/transmittal.ts`
- **Action**: Clone all `transmittal_items` where `revision_id = oldRevisionId`, insert new records with `revision_id = newRevisionId`. **Success Criteria**: (1) Item count in new revision equals old revision, (2) All `quantity` values preserved, (3) `item_correspondence_id` FK constraints pass, (4) Atomic transaction (rollback on failure).
- **Action**: Support `entity_type='transmittal'` with `entity_id=revision.publicId` (UUID string, NOT revision.id INT). Ensure workflow instance stores and retrieves using UUIDv7 string per ADR-019.
- **Dependencies**: R3
- **Status**: [ ]
- **ADR-019 Check**: Use `revision.publicId` (string) — never `revision.id` (INT) for entity binding
### R14 — Backend Service: Update `create()` to write `purpose`/`remarks` to `details` JSON
- **Action**: In `create()`, stop writing `purpose`/`remarks` to `Transmittal` entity; instead store them in `CorrespondenceRevision.details = { purpose, remarks }` JSON field. Remove `purpose`/`remarks` from `queryRunner.manager.create(Transmittal, {...})` call.
- **Dependencies**: R3 (findOneByUuid reads from details)
- **Status**: [ ]
- **Note**: Must deploy BEFORE step 3 SQL (DROP COLUMN) in schema-02-tables.sql
### R15 — Schema: Drop `purpose` and `remarks` from `transmittals` table
- **Action**: `ALTER TABLE transmittals DROP COLUMN purpose, DROP COLUMN remarks;` per ADR-009. Also remove corresponding TypeORM columns from `transmittal.entity.ts`.
- **Dependencies**: R14 (must be fully deployed first)
- **Status**: [ ]
- **ADR-009**: Direct SQL only — no TypeORM migration file
### R16 — DTO: Fix `TransmittalItemDto.itemId` to UUID (ADR-019)
- **Action**: Verify all revision-related fields use `publicId` (string UUID) not `id` (INT): `revisionId`, `workflowInstanceId`, `transmittalId` in responses. Run `grep -n "parseInt\|Number(\|\.id[^a-zA-Z]"` to catch violations.
- **Dependencies**: R2, R3, R12
- **Status**: [ ]
- **ADR-019**: CRITICAL — Zero tolerance for INT ID exposure in API responses
No tests failed. All 340 tests passed successfully.
However, the test suite exited with code `1` because some files did not meet the configured coverage thresholds (e.g., `virtual-column.service.ts`, `metrics.service.ts`, `maintenance-mode.guard.ts`).
1. The test coverage for recently modified files (`correspondence.service.ts`, `circulation.service.ts`, `circulation.controller.ts`) is passing the tests correctly, but the overall project thresholds are still failing in some unrelated utility modules.
2. Consider increasing coverage in `virtual-column.service.ts` and `metrics.service.ts` to satisfy global coverage thresholds.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.