690504:1641 Update specs [skip ci]
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
# Implementation Plan: Transmittals + Circulation Complete Integration (v1.8.8 with Revision Refactor)
|
||||
|
||||
**Branch**: `001-transmittals-circulation` | **Date**: 2026-04-29 | **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'`; 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'`; 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 (`ability.can('reassign', 'Circulation')`). Updates `routing.assignedTo` to the new user's INT id.
|
||||
|
||||
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) — 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.
|
||||
|
||||
---
|
||||
|
||||
## 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": [],
|
||||
"isOverdue": false,
|
||||
"routings": [
|
||||
{
|
||||
"assigneePublicId": "...",
|
||||
"assigneeType": "MAIN",
|
||||
"deadline": "2026-04-20T00:00:00.000Z",
|
||||
"isOverdue": true,
|
||||
"status": "PENDING",
|
||||
...
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ `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**:
|
||||
```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**: `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` with `ability.can('forceClose', 'Circulation')`
|
||||
**Response**: `{ "success": true }`
|
||||
**Behaviour**: Synchronous. Single DB transaction (all routing updates + audit log). BullMQ notification per assignee enqueued post-commit. **SLA: ≤ 3 seconds** for ≤ 50 routings.
|
||||
|
||||
#### `POST /circulation/:id/close` (NEW)
|
||||
|
||||
**Guards**: `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` with `ability.can('close', 'Circulation')` — **Document Control only**
|
||||
**Pre-condition**: All Main/Action routings must be in COMPLETED state (server validates; 422 if not)
|
||||
**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 (backend-computed, frontend renders)
|
||||
```ts
|
||||
// ✅ CORRECT — render from backend field only
|
||||
const isRoutingOverdue = routing.isOverdue; // boolean from API
|
||||
|
||||
// ❌ FORBIDDEN — frontend must NOT recompute
|
||||
// const isOverdue = new Date() > new Date(routing.deadline);
|
||||
```
|
||||
The `isOverdue` field is computed in `CirculationService.findOneByUuid()` via:
|
||||
```ts
|
||||
const isOverdue = new Date() > addDays(new Date(routing.deadline), 1);
|
||||
```
|
||||
Unit test: mock `Date` in `circulation.service.spec.ts` (or inject `ClockService`).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Task Breakdown
|
||||
|
||||
### Critical (Backend)
|
||||
|
||||
| Task | File | Description |
|
||||
|------|------|-------------|
|
||||
| B1 | `workflow-engine.service.ts` | Add `getInstanceByEntity(entityType, entityId)` — join `workflow_instances WHERE entity_type = ? AND entity_id = ?`; returns `{ id, currentState }` or null |
|
||||
| B2 | `transmittal.service.ts` | `findOneByUuid`: join workflow instance via `entity_type='TRANSMITTAL'`, `entity_id=correspondences.id`; add `workflowInstanceId` to response |
|
||||
| B3 | `transmittal.service.ts` | `findAll`: add `purpose` filter |
|
||||
| B4 | `transmittal.service.ts` | Add `submit(uuid, user)` with EC-RFA-004 validation (pre-transition check) |
|
||||
| B5 | `transmittal.controller.ts` | Add `POST /:uuid/submit` endpoint with `CaslAbilityGuard` |
|
||||
| B6 | `circulation.service.ts` | `findOneByUuid`: join workflow instance via `entity_type='CIRCULATION'`, `entity_id=circulations.id`; compute `isOverdue: boolean` server-side per routing |
|
||||
| B7 | `circulation.service.ts` | Add `reassignRouting(routingId, newAssigneeUuid, user)` — guard `ability.can('reassign', 'Circulation')` |
|
||||
| B8 | `circulation.service.ts` | Add `forceClose(uuid, reason, user)` — single DB transaction; enqueue BullMQ `notification-queue` post-commit; SLA ≤ 3s / 50 routings |
|
||||
| B9 | `circulation.service.ts` | Add `close(uuid, user)` — Document Control only; pre-condition all Main/Action routings COMPLETED |
|
||||
| B10 | `circulation.controller.ts` | Add PATCH `/routing/:id/reassign` + POST `/force-close` + POST `/close` with CASL guards |
|
||||
| B11 | `correspondence.service.ts` | On cancel: force-close all OPEN Circulations + enqueue BullMQ `notification-queue` job per affected assignee (EC-CORR-001, FR-X05) |
|
||||
|
||||
### Important (Frontend)
|
||||
|
||||
| Task | File | Description |
|
||||
|------|------|-------------|
|
||||
| F1 | `types/transmittal.ts` | Add `workflowInstanceId?`, `workflowState?`, `availableActions?` |
|
||||
| F2 | `types/circulation.ts` | Add `workflowInstanceId?`, `workflowState?`, `isOverdue: boolean` per routing (backend-provided) |
|
||||
| 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 from `routing.isOverdue` (backend field — no client-side date math); show "Close Circulation" button only when `canClose && user.role === 'DOCUMENT_CONTROL'` |
|
||||
| F7 | `transmittals/page.tsx` | Verify list page works, add purpose filter |
|
||||
|
||||
### Guidelines (i18n + Tests)
|
||||
|
||||
| Task | File | Description |
|
||||
|------|------|-------------|
|
||||
| I1 | `public/locales/th/*.json` + `en/*.json` | Add missing transmittal/circulation workflow keys (force-close modal, overdue badge, close action) |
|
||||
| T1 | `transmittal.service.spec.ts` | Unit test EC-RFA-004 submit validation (422 on DRAFT item) |
|
||||
| T2 | `circulation.service.spec.ts` | Unit tests: EC-CIRC-001 reassign, EC-CIRC-002 force-close (transaction + BullMQ enqueue), EC-CIRC-003 `isOverdue` with mocked Date, EC-CORR-001 notification job enqueue |
|
||||
| T3 | `circulation.service.spec.ts` | Integration test: Force Close with 50 routings completes within 3s (SC-008) |
|
||||
|
||||
---
|
||||
|
||||
## 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 (`ability.can('reassign', 'Circulation')`)
|
||||
- [ ] Force Close endpoint: 403 if user is not Document Control or above (`ability.can('forceClose', 'Circulation')`)
|
||||
- [ ] Close Circulation endpoint: 403 for all non-Document Control roles (`ability.can('close', 'Circulation')`)
|
||||
- [ ] Close Circulation: 422 if any Main/Action routing is not COMPLETED
|
||||
- [ ] Workflow transition: `Idempotency-Key` header enforced
|
||||
- [ ] No `parseInt` on any UUID in new code
|
||||
- [ ] `workflowInstanceId` is string (not number) in all responses
|
||||
- [ ] EC-CORR-001: BullMQ notification job enqueued, NOT inline in request thread
|
||||
- [ ] `isOverdue` on Circulation response is boolean from backend — no frontend date math
|
||||
|
||||
---
|
||||
|
||||
## 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 | 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 — 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 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Transmittal Revision Refactor (v1.8.8)
|
||||
|
||||
**Based on**: Clarifications Session 2026-04-29
|
||||
**Goal**: Restructure Transmittal to follow Master-Revision Pattern like RFA/Correspondence
|
||||
|
||||
### Clarification Decisions Applied
|
||||
|
||||
| Decision | Implementation |
|
||||
|----------|----------------|
|
||||
| **B** — Reuse `correspondence_revisions` | Transmittal-specific data (`purpose`, `remarks`) stored in `correspondence_revisions.details` JSON field |
|
||||
| **A** — Copy items on revision | When creating new revision, clone all `transmittal_items` with new `revision_id` |
|
||||
| **B** — Add `revision_id` column | Schema change: `ALTER TABLE transmittal_items ADD revision_id INT NULL FK` |
|
||||
| **A** — Workflow binds to current revision | `workflow_instances.entity_id` references `correspondence_revisions.id` (not master) |
|
||||
| **B** — Keep same document number | Revision label (A, B, C) distinguishes versions; doc number unchanged |
|
||||
|
||||
### Schema Changes (ADR-009 — SQL Direct)
|
||||
|
||||
```sql
|
||||
-- 1. Add revision_id to transmittal_items (backward compatible)
|
||||
ALTER TABLE transmittal_items
|
||||
ADD COLUMN revision_id INT NULL COMMENT 'FK to correspondence_revisions' AFTER transmittal_id,
|
||||
ADD CONSTRAINT fk_transmittal_items_revision
|
||||
FOREIGN KEY (revision_id) REFERENCES correspondence_revisions(id) ON DELETE CASCADE,
|
||||
ADD INDEX idx_transmittal_items_revision (revision_id);
|
||||
|
||||
-- 2. Add item_type column to transmittal_items (H1 fix)
|
||||
ALTER TABLE transmittal_items
|
||||
ADD COLUMN item_type VARCHAR(50) NULL COMMENT 'ประเภทเอกสาร เช่น DRAWING, RFA, CORRESPONDENCE' AFTER item_correspondence_id;
|
||||
|
||||
-- 3. Drop deprecated columns from transmittals table (ADR-009)
|
||||
-- Run AFTER R14 (service updated to write purpose/remarks → revision.details) is deployed
|
||||
ALTER TABLE transmittals
|
||||
DROP COLUMN purpose,
|
||||
DROP COLUMN remarks;
|
||||
```
|
||||
|
||||
### Data Model Update
|
||||
|
||||
```
|
||||
correspondences (Master - type_code='TRANSMITTAL')
|
||||
└── correspondence_revisions (Revisions)
|
||||
├── details.purpose ← Transmittal purpose (JSON)
|
||||
├── details.remarks ← Transmittal remarks (JSON)
|
||||
└── transmittal_items (with revision_id FK)
|
||||
└── item_correspondence_id → correspondences.id
|
||||
```
|
||||
|
||||
### Backend Changes
|
||||
|
||||
| Task | File | Description |
|
||||
|------|------|-------------|
|
||||
| R1 | `transmittal.service.ts` | Update `findOneByUuid` to join `correspondence_revisions` and read `purpose`/`remarks` from `details` JSON |
|
||||
| R2 | `transmittal.service.ts` | Add `createRevision()` method — copy items automatically, link to new revision |
|
||||
| R3 | `transmittal.service.ts` | Update `submit()` to work with revision-scoped items (EC-RFA-004) |
|
||||
| R4 | `transmittal-item.entity.ts` | Add `revisionId` column (nullable, FK to `correspondence_revisions.id`) |
|
||||
| R5 | `transmittal.service.ts` | Add `copyItemsToRevision(oldRevisionId, newRevisionId)` helper |
|
||||
| R6 | `workflow-engine.service.ts` | Update `getInstanceByEntity` to support `entity_type='transmittal'` with `entity_id=revision.publicId` (UUID string, ADR-019 — NOT INT revision.id) |
|
||||
| R17 | `transmittal-item.entity.ts` + `transmittal.service.ts` + `schema-02-tables.sql` | Add `item_type VARCHAR(50) NULL` column to `transmittal_items` table (ADR-009 SQL), add TypeORM column, save `item.itemType` in `create()` — Fixes H1 |
|
||||
| R18 | `transmittal.service.ts` | Replace `ORG_CODE: 'ORG'` hardcode with real `organizationCode` lookup via `dataSource.manager.findOne(Organization, { where: { id: userOrgId } })` — Fixes M1 |
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
| Task | File | Description |
|
||||
|------|------|-------------|
|
||||
| R7 | `types/transmittal.ts` | Add `revisionId`, `revisionNumber`, `revisionLabel` to `Transmittal` type |
|
||||
| R8 | `types/transmittal.ts` | Add `revisionId` to `TransmittalItem` type |
|
||||
| R9 | `transmittals/[uuid]/page.tsx` | Show revision selector (like RFA) when multiple revisions exist |
|
||||
| R10 | `transmittals/[uuid]/page.tsx` | Display revision label (A, B, C) in banner |
|
||||
|
||||
### API Contract Changes
|
||||
|
||||
#### `GET /transmittals/:uuid` (Updated)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"publicId": "...",
|
||||
"revisionId": "019abc...",
|
||||
"revisionNumber": 1,
|
||||
"revisionLabel": "A",
|
||||
"purpose": "FOR_APPROVAL",
|
||||
"remarks": "...",
|
||||
"workflowInstanceId": "019def...",
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"revisionId": "019abc...",
|
||||
"itemCorrespondenceId": "...",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /transmittals/:uuid/revisions` (NEW)
|
||||
|
||||
**Request**: `{ "remarks": "optional revision reason" }`
|
||||
**Response**: `{ "revisionId": "...", "revisionNumber": 2, "revisionLabel": "B" }`
|
||||
**Action**: Creates new `correspondence_revisions` record, copies all items automatically
|
||||
**Guards**: Document Control or above
|
||||
|
||||
### Constitution Check (Revision Refactor)
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| ADR-009 — Schema changes via SQL | ✅ REQUIRED | Add `revision_id` via direct SQL, no TypeORM migration |
|
||||
| ADR-019 — UUID strings | ✅ REQUIRED | All IDs use `publicId` string |
|
||||
| ADR-021 — Workflow Context | ✅ REQUIRED | Workflow binds to `correspondence_revisions.publicId` (UUID string, ADR-019) |
|
||||
| Master-Revision Pattern | ✅ REQUIRED | Aligns with RFA/Correspondence pattern |
|
||||
|
||||
### Risk Register (Revision Refactor)
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|------------|--------|-----------|
|
||||
| Data migration complexity | Medium | High | Backward-compatible NULLable `revision_id`; migrate existing items post-deploy |
|
||||
| Workflow instance re-binding | Medium | High | New revision = new workflow target; historical revisions preserve state |
|
||||
| Correspondence type detection | Low | Medium | Ensure `type_code='TRANSMITTAL'` when querying revisions |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,204 @@
|
||||
# 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-001–003), 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** a **Document Control** user views the page, **Then** a "Close Circulation" action is available (other roles must NOT see this button).
|
||||
|
||||
---
|
||||
|
||||
### 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`. 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.
|
||||
|
||||
---
|
||||
|
||||
## 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` 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).
|
||||
|
||||
**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 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. 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:**
|
||||
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
- **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` (DRAWING / RFA / CORRESPONDENCE), `remarks`, and `revisionId` (FK to `correspondence_revisions` — added in v1.8.8).
|
||||
- **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 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.
|
||||
|
||||
---
|
||||
|
||||
## 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**: `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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
### Session 2026-04-29 — Transmittal Revision Refactor Clarifications
|
||||
|
||||
- Q: Revision Table Strategy — should Transmittal have its own revisions table or reuse `correspondence_revisions`? → A: **B** — Reuse `correspondence_revisions` with JSON `details` field for transmittal-specific data (purpose, remarks), following Correspondence pattern.
|
||||
- Q: Transmittal Items Revision Handling — how should items behave when creating a new revision? → A: **A** — Copy all current items to new revision automatically, preserving the exact state at each revision point. Users can then add/remove items in the new revision.
|
||||
- Q: `transmittal_items` Schema Change — how to support item revisioning? → A: **B** — Add `revision_id` column to existing `transmittal_items` table (NULLable FK to `correspondence_revisions.id`), backward compatible per ADR-009.
|
||||
- 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.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,279 @@
|
||||
# Tasks: Transmittals + Circulation Complete Integration (v1.8.8 + Session 2026-05-03 Clarifications)
|
||||
|
||||
**Branch**: `001-transmittals-circulation`
|
||||
**Total Tasks**: 46 (27 v1.8.7 + 19 v1.8.8 Phase 4) | **Spec**: `specs/001-transmittals-circulation/spec.md`
|
||||
**Last Updated**: 2026-05-03 — Added B9c, B10, B11, T3, expanded T2/I1 from Session 2026-05-03 clarifications
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Backend Foundation (Critical — blocks all frontend work)
|
||||
|
||||
> **Goal**: Expose `workflowInstanceId` from both backend services; implement all EC handlers.
|
||||
> **Join pattern**: `workflow_instances WHERE entity_type = ? AND entity_id = ?` (string) — no new FK columns.
|
||||
|
||||
- [x] T001 Implement `WorkflowEngineService.getInstanceByEntity(entityType, entityId)` — query `workflow_instances WHERE entity_type = ? AND entity_id = ?`; return `{ id, currentState, availableActions? } | null` in `backend/src/modules/workflow-engine/workflow-engine.service.ts`
|
||||
|
||||
- [x] T002 [P] Update `TransmittalService.findOneByUuid()` — call `getInstanceByEntity('TRANSMITTAL', correspondences.id)`, merge `workflowInstanceId`, `workflowState` into response in `backend/src/modules/transmittal/transmittal.service.ts`
|
||||
|
||||
- [x] T003 [P] Add `purpose?: string` to `SearchTransmittalDto` and apply `andWhere` filter in `TransmittalService.findAll()` in `backend/src/modules/transmittal/transmittal.service.ts`
|
||||
|
||||
- [x] T004 Add `TransmittalService.submit(uuid, user)` — pre-check all `transmittal_items` for DRAFT correspondence (EC-RFA-004); throw `422 ValidationException` identifying offending doc; then call `workflowEngine.createInstance` + transition `SUBMIT` in `backend/src/modules/transmittal/transmittal.service.ts`
|
||||
|
||||
- [x] T005 Add `POST /:uuid/submit` endpoint with `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` + `@Audit('transmittal.submit', 'transmittal')` in `backend/src/modules/transmittal/transmittal.controller.ts`
|
||||
|
||||
- [x] T006 [P] Update `CirculationService.findOneByUuid()` — call `getInstanceByEntity('CIRCULATION', circulation.id)`, merge `workflowInstanceId`, `workflowState`; compute `isOverdue: boolean` **server-side** per routing (`NOW() > deadline_date + INTERVAL 1 DAY`) in `backend/src/modules/circulation/circulation.service.ts`
|
||||
|
||||
- [x] T007 [P] Add `CirculationService.reassignRouting(routingId, newAssigneeUuid, user)` — verify `ability.can('reassign', 'Circulation')` (Document Control+); resolve UUID→INT via `uuidResolver`; update `routing.assignedTo`; write audit log in `backend/src/modules/circulation/circulation.service.ts`
|
||||
|
||||
- [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`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Frontend Types & Hooks (Important — depends on Phase 1 API shape)
|
||||
|
||||
- [x] T013 [P] Update `Transmittal` interface — add `workflowInstanceId?: string`, `workflowState?: string`, `availableActions?: string[]`; add `isOverdue?: boolean` to `CirculationRouting` (backend-provided, no client computation); no `any` types (ADR-019) in `frontend/types/transmittal.ts`
|
||||
|
||||
- [x] T014 [P] Update `Circulation` and `CirculationRouting` interfaces — add `workflowInstanceId?: string`, `workflowState?: string`, `availableActions?: string[]`; add `isOverdue: boolean`, `deadline?: string`, `assigneeType?: 'MAIN' | 'ACTION' | 'INFORMATION'` to `CirculationRouting` in `frontend/types/circulation.ts`
|
||||
|
||||
- [x] T015 Create `useTransmittal(uuid: string | undefined)` TanStack Query hook — `queryKey: ['transmittal', uuid]`, `staleTime: 60_000`, export `transmittalKeys` factory in `frontend/hooks/use-transmittal.ts`
|
||||
|
||||
- [x] T016 Add `useCirculation(uuid: string | undefined)` hook — `queryKey: ['circulation', uuid]`, `staleTime: 60_000` in `frontend/hooks/use-circulation.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — US1: Transmittal Workflow-Wired Detail Page (P1 🎯 MVP)
|
||||
|
||||
> **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.
|
||||
|
||||
- [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`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — US2: Circulation Workflow-Wired Detail Page (P1 🎯 MVP)
|
||||
|
||||
> **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.
|
||||
|
||||
- [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`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — US3: Transmittal List Page with Search & Filter (P1)
|
||||
|
||||
> **Story Goal**: Doc Control browses transmittals, filters by `purpose`, searches by doc number/subject within 500ms.
|
||||
> **Independent Test**: Navigate to `/transmittals` — purpose filter updates list; search filters within 500ms; empty state shown when no results.
|
||||
|
||||
- [x] T021 [US3] Add `purpose` select filter (FOR_APPROVAL / FOR_INFORMATION / FOR_REVIEW / OTHER) and verify pagination + search in `frontend/app/(dashboard)/transmittals/page.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — US4: EC-RFA-004 Submit Validation (P2)
|
||||
|
||||
> **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`
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Tests (Tier 2 — required before merge)
|
||||
|
||||
- [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`
|
||||
|
||||
- [ ] T037 Update `TransmittalItem` entity — add nullable `revisionId` column + `@ManyToOne` relation to `CorrespondenceRevision`; add `itemType?: string` column in `backend/src/modules/transmittal/entities/transmittal-item.entity.ts`
|
||||
|
||||
- [ ] T038 Update `TransmittalService.findOneByUuid()` — join `correspondence_revisions`, read `purpose`/`remarks` from `details` JSON; include `revisionId`, `revisionNumber`, `revisionLabel` in response in `backend/src/modules/transmittal/transmittal.service.ts`
|
||||
|
||||
- [ ] T039 Add `TransmittalService.copyItemsToRevision(oldRevisionId, newRevisionId)` helper — bulk clone `transmittal_items` rows in single atomic transaction in `backend/src/modules/transmittal/transmittal.service.ts`
|
||||
|
||||
- [ ] T040 Add `TransmittalService.createRevision(uuid, user)` — create `correspondence_revisions` record; auto-copy all items via `copyItemsToRevision()` in `backend/src/modules/transmittal/transmittal.service.ts`
|
||||
|
||||
- [ ] T041 Add `POST /:uuid/revisions` endpoint with `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` + `@Audit('transmittal.create-revision', 'transmittal')`; returns `{ revisionId, revisionNumber, revisionLabel }` in `backend/src/modules/transmittal/transmittal.controller.ts`
|
||||
|
||||
- [ ] 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`
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1 (Backend Foundation):
|
||||
T001 → T002 [P], T006 [P] (workflow instance join)
|
||||
T001 → T004 → T005 (submit + EC-RFA-004)
|
||||
T007 [P], T008 [P] (reassign + force-close — parallel)
|
||||
T009 → T011 (close Circulation — Document Control only)
|
||||
T012 (EC-CORR-001 cascade — no Phase 1 deps)
|
||||
|
||||
Phase 2 (Types & Hooks):
|
||||
T013 [P], T014 [P] (types — parallel, no Phase 1 deps)
|
||||
T013 → T015 (useTransmittal hook)
|
||||
T014 → T016 (useCirculation hook)
|
||||
|
||||
Phase 3 (US1 — Transmittal Detail):
|
||||
T015, T002 → T017
|
||||
|
||||
Phase 4 (US2 — Circulation Detail):
|
||||
T016, T006, T009, T011 → T018, T019, T020
|
||||
|
||||
Phase 5 (US3 — List):
|
||||
T013, T003 → T021
|
||||
|
||||
Phase 6 (US4 — EC-RFA-004 UI):
|
||||
T005, T017 → T022
|
||||
|
||||
Phase 7 (US5 — EC-CIRC-001/002):
|
||||
T007, T008, T018 → T023, T024
|
||||
|
||||
Phase 8 (Tests):
|
||||
T004 → T025
|
||||
T007 → T026
|
||||
T008 → T027
|
||||
T006 → T028
|
||||
T009 → T029
|
||||
T012 → T030
|
||||
T008, T031 (integration — 50 routings ≤3s)
|
||||
T019 → T032
|
||||
T020 → T033
|
||||
|
||||
Phase 10 (Revision Refactor):
|
||||
T036 → T037 → T038 → T039, T040 → T041
|
||||
T038 → T042 (submit revision-scoped)
|
||||
T038 → T043 (create writes to details JSON)
|
||||
T043 deployed → T044 (drop columns)
|
||||
T045 (UUID fix — parallel)
|
||||
T046 [P] → T047
|
||||
T038, T046 → T048 (ADR-019 scan)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parallel Execution Opportunities
|
||||
|
||||
| Group | Tasks | Condition |
|
||||
|---|---|---|
|
||||
| Backend foundation | T002, T003, T006, T007, T008, T012 | All start after T001 |
|
||||
| Types | T013, T014 | Immediately (no Phase 1 deps) |
|
||||
| Hooks | T015, T016 | After respective types |
|
||||
| Detail pages | T017, T018 | After hooks + backend |
|
||||
| Tests | T025–T031 | After their respective service methods |
|
||||
| i18n | T034, T035 | After Phase 4 UI complete |
|
||||
| Revision types | T046 | Parallel with schema (T036) |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy (MVP → Full)
|
||||
|
||||
| Scope | Tasks | Deliverable |
|
||||
|---|---|---|
|
||||
| **MVP** (US1 + US2 core) | T001–T009, T013–T020 | Both detail pages live with workflow data |
|
||||
| **P1 Complete** | + T021, T022 | List page + submit validation |
|
||||
| **P2 Complete** | + T023, T024, T028–T033 | EC edge cases + all tests |
|
||||
| **Full** | + T034–T048 | i18n polish + Revision Refactor |
|
||||
|
||||
---
|
||||
|
||||
## Commit Message Convention
|
||||
|
||||
```
|
||||
feat(workflow-engine): add getInstanceByEntity() for Transmittal+Circulation join
|
||||
feat(transmittal): expose workflowInstanceId via entity_type join in findOneByUuid
|
||||
feat(transmittal): add submit endpoint with EC-RFA-004 DRAFT item validation
|
||||
feat(circulation): expose workflowInstanceId + server-side isOverdue in findOneByUuid
|
||||
feat(circulation): add reassignRouting EC-CIRC-001 handler with CASL guard
|
||||
feat(circulation): add forceClose EC-CIRC-002 — single transaction + BullMQ post-commit
|
||||
feat(circulation): add close endpoint — Document Control only (FR-C09)
|
||||
feat(correspondence): add EC-CORR-001 cascade — force-close Circulations + BullMQ notify (FR-X05)
|
||||
feat(frontend): wire WorkflowLifecycle in transmittal detail page (US1)
|
||||
feat(frontend): wire WorkflowLifecycle + server-side overdue badge in circulation detail (US2)
|
||||
feat(frontend): add Close Circulation button — Document Control role guard (FR-C09)
|
||||
test(circulation): isOverdue server-side unit test with mocked Date (SC-007)
|
||||
test(circulation): close() RBAC + pre-condition unit tests
|
||||
test(circulation): forceClose integration test ≤3s / 50 routings (SC-008)
|
||||
test(correspondence): EC-CORR-001 BullMQ notification enqueue test
|
||||
feat(schema): add revision_id + item_type to transmittal_items (ADR-009)
|
||||
fix(transmittal): TransmittalItemDto.itemId INT→UUID string (ADR-019)
|
||||
fix(transmittal): replace hardcoded ORG_CODE with real organizationCode lookup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Verification Checklist
|
||||
|
||||
- [ ] `ability.can('reassign', 'Circulation')` — 403 for non-Document Control (T007)
|
||||
- [ ] `ability.can('forceClose', 'Circulation')` — 403 for non-Document Control (T008)
|
||||
- [ ] `ability.can('close', 'Circulation')` — 403 for all non-Document Control roles (T009)
|
||||
- [ ] Close Circulation pre-condition: 422 if any Main/Action routing not COMPLETED (T009)
|
||||
- [ ] `Idempotency-Key` header enforced on all workflow transitions
|
||||
- [ ] `workflowInstanceId` is `string` (UUID) — never `number` in any response (ADR-019)
|
||||
- [ ] EC-CORR-001 notification is BullMQ job — NOT inline in request thread (ADR-008)
|
||||
- [ ] Frontend `isOverdue` from backend field only — no `new Date()` comparison in JSX (T019)
|
||||
- [ ] No `parseInt` / `Number()` / `+` on any UUID in new code (ADR-019)
|
||||
- [ ] Two-phase file upload via `StorageService` for any new file operations (ADR-016)
|
||||
@@ -0,0 +1,37 @@
|
||||
# Test Report
|
||||
|
||||
**Date**: 2026-05-03
|
||||
**Framework**: Jest
|
||||
**Status**: PASS (Thresholds partially unmet)
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Value |
|
||||
| ----------- | ----- |
|
||||
| Total Tests | 340 |
|
||||
| Passed | 340 |
|
||||
| Failed | 0 |
|
||||
| Skipped | 0 |
|
||||
| Duration | 40.7s |
|
||||
| Coverage | Varies (some thresholds unmet) |
|
||||
|
||||
## Failed Tests
|
||||
|
||||
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`).
|
||||
|
||||
## Coverage by File (Notable Exceptions)
|
||||
|
||||
| File | Lines | Branches | Functions |
|
||||
| ----------- | ----- | -------- | --------- |
|
||||
| `src/modules/json-schema/services/virtual-column.service.ts` | 22.97% | < 80% | 0% |
|
||||
| `src/modules/monitoring/services/metrics.service.ts` | 68.75% | 0% | 0% |
|
||||
| `src/common/guards/maintenance-mode.guard.ts` | 0% | 0% | 0% |
|
||||
| `src/common/interceptors/idempotency.interceptor.ts` | >90% | 88.23% | >90% |
|
||||
| `src/common/interceptors/performance.interceptor.ts` | >90% | 88.88% | >90% |
|
||||
|
||||
## Next Actions
|
||||
|
||||
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