18 KiB
18 KiB
Tasks: Transmittals + Circulation Complete Integration (v1.8.8 with Revision Refactor)
Branch: 001-transmittals-circulation | Total Tasks: 36 (18 v1.8.7 + 18 v1.8.8 Phase 4) | Phase: Phase 4 Ready — Revision Refactor
Phase 1 — Backend Foundation (Critical — blocks all frontend work)
B1 — WorkflowEngineService: Add getInstanceByEntity()
- File:
backend/src/modules/workflow-engine/workflow-engine.service.ts - Action: Add method that queries
WorkflowInstancebyentityType + entityId; returns{ id, currentState, availableActions? } | null - Dependencies: none
- Status: [x]
B2 — TransmittalService: Expose workflowInstanceId in findOneByUuid()
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: Call
workflowEngine.getInstanceByEntity('transmittal', correspondenceId.toString())and mergeworkflowInstanceId,workflowStateinto response - Dependencies: B1
- Status: [x]
B3 — TransmittalService: Add purpose filter to findAll()
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: Add
purpose?: stringtoSearchTransmittalDtoand applyandWhereinfindAll() - Dependencies: none (parallel with B1)
- Status: [x]
B4 — TransmittalService: Add submit() with EC-RFA-004 validation
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: New
submit(uuid, user)method; fetches alltransmittal_items, checks each item's correspondence current revision status — throws422 ValidationExceptionif any isDRAFT; then callsworkflowEngine.createInstance('TRANSMITTAL_FLOW_V1', 'transmittal', ...)and transitions withSUBMIT - Dependencies: B1
- Status: [x]
B5 — TransmittalController: Add POST /:uuid/submit endpoint
- File:
backend/src/modules/transmittal/transmittal.controller.ts - Action: Add endpoint with
@RequirePermission('document.manage'),@Audit('transmittal.submit', 'transmittal') - Dependencies: B4
- Status: [x]
B6 — CirculationService: Expose workflowInstanceId in findOneByUuid()
- File:
backend/src/modules/circulation/circulation.service.ts - Action: Call
workflowEngine.getInstanceByEntity('circulation', circulation.id.toString()), merge into response; also computeisOverdueper routing based ondeadline_date - Dependencies: B1
- Status: [x]
B7 — CirculationService: Add reassignRouting() (EC-CIRC-001)
- File:
backend/src/modules/circulation/circulation.service.ts - Action: Fetch routing, verify user has Document Control permission, resolve
newAssigneeUuid→ INT viauuidResolver.resolveUserId(), updaterouting.assignedTo, write audit log - Dependencies: none
- Status: [x]
B8 — CirculationService: Add forceClose() (EC-CIRC-002)
- File:
backend/src/modules/circulation/circulation.service.ts - Action: Require
reason(non-empty), update all PENDING routings toCANCELLED, setcirculation.statusCode = 'CANCELLED', write audit log entry; usequeryRunnerfor atomicity - Dependencies: none
- Status: [x]
B9 — CirculationController: Add reassign + force-close endpoints
- File:
backend/src/modules/circulation/circulation.controller.ts - Action:
PATCH /:uuid/routing/:routingId/reassign—@RequirePermission('circulation.manage')POST /:uuid/force-close—@RequirePermission('circulation.manage')
- Dependencies: B7, B8
- Status: [x]
Phase 2 — Frontend Types & Hooks (Important — depends on Phase 1)
F1 — Update types/transmittal.ts
- File:
frontend/types/transmittal.ts - Action: Add
workflowInstanceId?: string,workflowState?: string,availableActions?: string[]toTransmittalinterface; addpurpose?: stringtoSearchTransmittalDto; noanytypes (ADR-019) - Dependencies: none (parallel with Phase 1)
- Status: [x]
F2 — Update types/circulation.ts
- File:
frontend/types/circulation.ts - Action: Add
workflowInstanceId?: string,workflowState?: string,availableActions?: string[]toCirculation; adddeadline?: string,assigneeType?: 'MAIN' | 'ACTION' | 'INFORMATION'toCirculationRouting - Dependencies: none
- Status: [x]
F3 — Create hooks/use-transmittal.ts
- File:
frontend/hooks/use-transmittal.ts - Action: Create
useTransmittal(uuid: string | undefined)withqueryKey: ['transmittal', uuid],staleTime: 60_000; exporttransmittalKeysquery key factory - Dependencies: F1
- Status: [x]
F4 — Update hooks/use-circulation.ts
- File:
frontend/hooks/use-circulation.ts - Action: Add
useCirculation(uuid: string | undefined)hook withqueryKey: ['circulation', uuid],staleTime: 60_000 - Dependencies: F2
- Status: [x]
Phase 3 — Frontend Detail Pages (Important — depends on Phase 2 + Phase 1 deployed)
F5 — Wire Transmittal detail page
- File:
frontend/app/(dashboard)/transmittals/[uuid]/page.tsx - Action:
- Replace inline
useQuerywithuseTransmittal(uuid) - Add
useWorkflowHistory(transmittal?.workflowInstanceId) - Add
const [pendingAttachmentIds, setPendingAttachmentIds] = useState<string[]>([]) - Pass
instanceId,workflowState,availableActions,pendingAttachmentIdstoIntegratedBanner - Pass
history,currentState,isLoading,error,onAttachmentsChangetoWorkflowLifecyclein Workflow tab
- Replace inline
- Dependencies: F3, F1
- Status: [x]
F6 — Wire Circulation detail page
- File:
frontend/app/(dashboard)/circulation/[uuid]/page.tsx - Action:
- Replace inline
useQuerywithuseCirculation(uuid) - Add
useWorkflowHistory(circulation?.workflowInstanceId) - Add
isOverdue(deadline?)helper function - Wire
IntegratedBannerwithinstanceId,workflowState,availableActions - Wire
WorkflowLifecyclewith history in Workflow tab - Add Overdue badge to routing rows where
isOverdue(routing.deadline)is true - Replace hardcoded "Complete" button with proper workflow action
- Replace inline
- Dependencies: F4, F2
- Status: [x]
Phase 4 — List Page & i18n (Guidelines)
F7 — Transmittal list page: add purpose filter
- File:
frontend/app/(dashboard)/transmittals/page.tsx - Action: Add
purposeselect filter (FOR_APPROVAL / FOR_INFORMATION / FOR_REVIEW / OTHER) passing totransmittalService.getAll(). Read current page to assess if pagination works. - Dependencies: F1
- Status: [x]
I1 — i18n keys for Transmittal/Circulation workflow
- Files:
public/locales/th/*.json,public/locales/en/*.json - Action: Check
use-translations.tsfor key lookup pattern; add missing keys:transmittal.purpose.*,circulation.status.*,circulation.overdue,circulation.forceClose.*,circulation.reassign.* - Dependencies: F5, F6
- Status: [ ] (low priority — pending)
Phase 5 — Tests (Tier 2 — required before merge)
T1 — Transmittal service EC-RFA-004 unit test
- File:
backend/src/modules/transmittal/transmittal.service.spec.ts(create if needed) - Action: Test
submit()throwsValidationExceptionwhen item correspondence is DRAFT; test passes when all items are SUBMITTED - Dependencies: B4
- Status: [x]
T2 — Circulation service edge-case unit tests
- File:
backend/src/modules/circulation/circulation.service.spec.ts(create if needed) - Action: Test
reassignRouting()— permission check, assignment update; testforceClose()— all pending routings cancelled, reason logged; testisOverduehelper (EC-CIRC-003) - Dependencies: B7, B8
- Status: [x]
Execution Order
B1 (parallel: B3, F1, F2)
→ B2, B4 (parallel), B6 (parallel)
→ B5 → T1
→ B7, B8 (parallel)
→ B9 → T2
→ F3, F4 (parallel after F1, F2)
→ F5, F6 (parallel after F3, F4)
→ F7, I1 (polish)
Commit Message Convention
feat(transmittal): expose workflowInstanceId in findOneByUuid response
feat(circulation): expose workflowInstanceId + overdue in findOneByUuid
feat(circulation): add reassignRouting EC-CIRC-001 handler
feat(circulation): add forceClose EC-CIRC-002 handler
feat(transmittal): add submit endpoint with EC-RFA-004 validation
feat(frontend): wire WorkflowLifecycle in transmittal detail page
feat(frontend): wire WorkflowLifecycle + overdue badge in circulation detail
test(transmittal): EC-RFA-004 submit validation unit tests
test(circulation): EC-CIRC-001/002/003 edge case unit tests
Phase 4 — Transmittal Revision Refactor (v1.8.8)
Based on: Clarifications Session 2026-04-29 Goal: Restructure Transmittal to follow Master-Revision Pattern
R1 — Schema: Add revision_id to transmittal_items
- File:
specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql - Action: Add
revision_id INT NULLcolumn with FK tocorrespondence_revisions(id), create index per ADR-009 - Dependencies: none
- Status: [ ]
R2 — Backend Entity: Update TransmittalItem with revisionId
- File:
backend/src/modules/transmittal/entities/transmittal-item.entity.ts - Action: Add
revisionIdcolumn (nullable), add@ManyToOnerelation toCorrespondenceRevision - Dependencies: R1
- Status: [ ]
R3 — Backend Service: Update findOneByUuid to read from revision
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: Join
correspondence_revisions, readpurpose/remarksfromdetailsJSON field; includerevisionId,revisionNumber,revisionLabelin response - Dependencies: R2
- Status: [ ]
R4 — Backend Service: Add createRevision() method
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: Create new
correspondence_revisionsrecord, copy all items from current revision to new revision viacopyItemsToRevision()helper - Dependencies: R3
- Status: [ ]
R5 — Backend Service: Add copyItemsToRevision() helper
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: Clone all
transmittal_itemswhererevision_id = oldRevisionId, insert new records withrevision_id = newRevisionId. Success Criteria: (1) Item count in new revision equals old revision, (2) Allquantityvalues preserved, (3)item_correspondence_idFK constraints pass, (4) Atomic transaction (rollback on failure). - Dependencies: R2
- Status: [ ]
R6 — Backend Controller: Add POST /:uuid/revisions endpoint
- File:
backend/src/modules/transmittal/transmittal.controller.ts - Action: New endpoint with
@RequirePermission('document.manage')(ADR-016),@Audit('transmittal.create-revision', 'transmittal'), callscreateRevision(), returns{ revisionId, revisionNumber, revisionLabel } - Dependencies: R4
- Status: [ ]
- Security: CASL Guard required — Document Control role or above
R7 — Backend Service: Update submit() for revision-scoped items
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: EC-RFA-004 validation checks items for current revision only; workflow instance binds to
correspondence_revisions.id - Dependencies: R3, R4
- Status: [ ]
R8 — Frontend Types: Add revision fields to Transmittal
- File:
frontend/types/transmittal.ts - Action: Add
revisionId?: string,revisionNumber?: number,revisionLabel?: stringtoTransmittalinterface - Dependencies: none (parallel)
- Status: [ ]
R9 — Frontend Types: Add revisionId to TransmittalItem
- File:
frontend/types/transmittal.ts - Action: Add
revisionId?: stringtoTransmittalIteminterface - Dependencies: R8
- Status: [ ]
R10 — Frontend Page: Add revision selector to detail page
- File:
frontend/app/(dashboard)/transmittals/[uuid]/page.tsx - Action: Show revision dropdown when multiple revisions exist (like RFA pattern), display
revisionLabel(A, B, C) in banner - Dependencies: R8, R9
- Status: [ ]
R11 — Frontend Hook: Update useTransmittal for revision context
- File:
frontend/hooks/use-transmittal.ts - Action: Add optional
revisionIdparameter to fetch specific revision; default to current revision - Dependencies: R8
- Status: [ ]
R12 — Workflow Engine: Update getInstanceByEntity for revision binding (ADR-019)
- File:
backend/src/modules/workflow-engine/workflow-engine.service.ts - Action: Support
entity_type='transmittal'withentity_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) — neverrevision.id(INT) for entity binding
R14 — Backend Service: Update create() to write purpose/remarks to details JSON
- File:
backend/src/modules/transmittal/transmittal.service.ts - Action: In
create(), stop writingpurpose/remarkstoTransmittalentity; instead store them inCorrespondenceRevision.details = { purpose, remarks }JSON field. Removepurpose/remarksfromqueryRunner.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
- File:
specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql - Action:
ALTER TABLE transmittals DROP COLUMN purpose, DROP COLUMN remarks;per ADR-009. Also remove corresponding TypeORM columns fromtransmittal.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)
- File:
backend/src/modules/transmittal/dto/create-transmittal.dto.ts+backend/src/modules/transmittal/transmittal.service.ts - Action: Change
itemId: number+@IsInt()→itemId: string+@IsUUID(). Increate(), replace direct assignment withuuidResolver.resolveCorrespondenceId(item.itemId)before savingitemCorrespondenceId. - Dependencies: R1 (schema must be stable)
- Status: [ ]
- ADR-019: CRITICAL — Frontend must send
publicId(UUID string), not INT id
R17 — Schema + Entity: Add itemType column to transmittal_items (H1)
- Files:
specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql+backend/src/modules/transmittal/entities/transmittal-item.entity.ts+backend/src/modules/transmittal/transmittal.service.ts - Action: (1) SQL:
ALTER TABLE transmittal_items ADD COLUMN item_type VARCHAR(50) NULL COMMENT 'ประเภทเอกสาร เช่น DRAWING, RFA, CORRESPONDENCE' AFTER item_correspondence_id;(ADR-009). (2) Entity: add@Column({ name: 'item_type', nullable: true }) itemType?: string;. (3) Servicecreate(): saveitemType: item.itemTypefrom DTO (field already exists inTransmittalItemDto). - Dependencies: R1
- Status: [ ]
- Note: Fixes H1 — DTO had
itemTypebut it was never persisted to DB
R18 — Service: Fix ORG_CODE hardcode in create() (M1)
- Files:
backend/src/modules/transmittal/transmittal.service.ts - Action: Before
generateNextNumber(), fetch originator org:const originatorOrg = await this.dataSource.manager.findOne(Organization, { where: { id: userOrgId } }); const orgCode = originatorOrg?.organizationCode ?? 'UNK';— then replaceORG_CODE: 'ORG'withORG_CODE: orgCode. Pattern matchescorrespondence.service.tsline 263-269. - Dependencies: none (parallel)
- Status: [ ]
- Note: Fixes M1 —
Organization.organizationCodefield confirmed atorganization.entity.ts:24
R13 — Validation: ADR-019 UUID Compliance Check
- File:
backend/src/modules/transmittal/+frontend/types/transmittal.ts - Action: Verify all revision-related fields use
publicId(string UUID) notid(INT):revisionId,workflowInstanceId,transmittalIdin responses. Rungrep -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
Phase 4 Execution Order
R1 (schema)
→ R2 (entity)
→ R3 (service findOneByUuid) ─┬→ R4 (createRevision) → R6 (controller endpoint)
│ → R5 (copyItems helper)
├→ R7 (submit update)
├→ R12 (workflow binding update)
└→ R13 (ADR-019 validation)
R8 (frontend types) ─┬→ R9 (item types)
→ R11 (hook update) │
→ R10 (page update) ─┘
R3 → R14 (create() writes to details JSON)
→ R15 (DROP COLUMN purpose/remarks) ← deploy R14 first
R1 → R17 (add item_type column + entity + save in create())
R18 (fix ORG_CODE — no dependencies, parallel safe)
Phase 4 Commit Message Convention
feat(schema): add revision_id to transmittal_items table (ADR-009)
feat(transmittal): add revisionId column to TransmittalItem entity
feat(transmittal): update findOneByUuid to read from correspondence_revisions
feat(transmittal): add createRevision with automatic item copying
feat(transmittal): add copyItemsToRevision helper method
feat(transmittal): add POST /:uuid/revisions endpoint
feat(transmittal): update submit for revision-scoped items (EC-RFA-004)
feat(frontend): add revision fields to Transmittal types
feat(frontend): add revision selector to transmittal detail page
feat(workflow-engine): update getInstanceByEntity for revision binding
chore(validation): ADR-019 UUID compliance check for revision refactor
fix(transmittal): change TransmittalItemDto.itemId from INT to UUID string (ADR-019)
feat(transmittal): add item_type column to transmittal_items and persist from DTO (H1)
fix(transmittal): replace hardcoded ORG_CODE with real organizationCode lookup (M1)