From d239b583870383ecc8a1df4aada47f8e1ceb2b1d Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 29 Apr 2026 16:52:08 +0700 Subject: [PATCH] 260429:1652 update Infras #07 --- specs/001-transmittals-circulation/plan.md | 129 ++++++++++++++- specs/001-transmittals-circulation/spec.md | 12 +- specs/001-transmittals-circulation/tasks.md | 169 +++++++++++++++++++- 3 files changed, 304 insertions(+), 6 deletions(-) diff --git a/specs/001-transmittals-circulation/plan.md b/specs/001-transmittals-circulation/plan.md index 2937931..a87ee05 100644 --- a/specs/001-transmittals-circulation/plan.md +++ b/specs/001-transmittals-circulation/plan.md @@ -1,6 +1,6 @@ -# Implementation Plan: Transmittals + Circulation Complete Integration (v1.8.7 Post-ADR-021) +# Implementation Plan: Transmittals + Circulation Complete Integration (v1.8.8 with Revision Refactor) -**Branch**: `001-transmittals-circulation` | **Date**: 2026-04-12 | **Spec**: `specs/001-transmittals-circulation/spec.md` +**Branch**: `001-transmittals-circulation` | **Date**: 2026-04-29 | **Spec**: `specs/001-transmittals-circulation/spec.md` --- @@ -182,7 +182,7 @@ useQuery({ ``` Returns: `{ transmittal, isLoading, error }` — replaces inline `useQuery` in detail page. -#### `useCirculation(uuid)` hook +#### `useCirculation(uuid)` hook ```ts useQuery({ queryKey: ['circulation', uuid], @@ -331,3 +331,126 @@ ADR-021 (IntegratedBanner/WorkflowLifecycle components) | 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 | + +--- + +## 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 | + +--- diff --git a/specs/001-transmittals-circulation/spec.md b/specs/001-transmittals-circulation/spec.md index a16add0..a784d41 100644 --- a/specs/001-transmittals-circulation/spec.md +++ b/specs/001-transmittals-circulation/spec.md @@ -145,7 +145,7 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC ### Key Entities - **Transmittal**: Extends Correspondence (`type_code = 'TRANSMITTAL'`). Has `purpose`, `remarks`, and a list of `transmittal_items`. Has one `WorkflowInstance` via the Unified Workflow Engine. -- **TransmittalItem**: Links a Transmittal to the document it carries (`correspondences` M:N). Has `quantity`, `itemType`, `remarks`. +- **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`. @@ -181,3 +181,13 @@ Document Control can re-assign a Circulation when an assignee is deactivated (EC - 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). + +--- diff --git a/specs/001-transmittals-circulation/tasks.md b/specs/001-transmittals-circulation/tasks.md index 5f85067..aa9a4ac 100644 --- a/specs/001-transmittals-circulation/tasks.md +++ b/specs/001-transmittals-circulation/tasks.md @@ -1,6 +1,6 @@ -# Tasks: Transmittals + Circulation Complete Integration (v1.8.7 Post-ADR-021) +# Tasks: Transmittals + Circulation Complete Integration (v1.8.8 with Revision Refactor) -**Branch**: `001-transmittals-circulation` | **Total Tasks**: 18 | **Phase**: ✅ Complete (v1.8.7) +**Branch**: `001-transmittals-circulation` | **Total Tasks**: 36 (18 v1.8.7 + 18 v1.8.8 Phase 4) | **Phase**: Phase 4 Ready — Revision Refactor --- @@ -180,3 +180,168 @@ 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 NULL` column with FK to `correspondence_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 `revisionId` column (nullable), add `@ManyToOne` relation to `CorrespondenceRevision` +- **Dependencies**: R1 +- **Status**: [ ] + +### R3 — Backend Service: Update `findOneByUuid` to read from revision +- **File**: `backend/src/modules/transmittal/transmittal.service.ts` +- **Action**: Join `correspondence_revisions`, read `purpose`/`remarks` from `details` JSON field; include `revisionId`, `revisionNumber`, `revisionLabel` in response +- **Dependencies**: R2 +- **Status**: [ ] + +### R4 — Backend Service: Add `createRevision()` method +- **File**: `backend/src/modules/transmittal/transmittal.service.ts` +- **Action**: Create new `correspondence_revisions` record, copy all items from current revision to new revision via `copyItemsToRevision()` helper +- **Dependencies**: R3 +- **Status**: [ ] + +### R5 — Backend Service: Add `copyItemsToRevision()` helper +- **File**: `backend/src/modules/transmittal/transmittal.service.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). +- **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')`, calls `createRevision()`, 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?: string` to `Transmittal` interface +- **Dependencies**: none (parallel) +- **Status**: [ ] + +### R9 — Frontend Types: Add `revisionId` to `TransmittalItem` +- **File**: `frontend/types/transmittal.ts` +- **Action**: Add `revisionId?: string` to `TransmittalItem` interface +- **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 `revisionId` parameter 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'` 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 +- **File**: `backend/src/modules/transmittal/transmittal.service.ts` +- **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 +- **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 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) +- **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()`. In `create()`, replace direct assignment with `uuidResolver.resolveCorrespondenceId(item.itemId)` before saving `itemCorrespondenceId`. +- **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) Service `create()`: save `itemType: item.itemType` from DTO (field already exists in `TransmittalItemDto`). +- **Dependencies**: R1 +- **Status**: [ ] +- **Note**: Fixes H1 — DTO had `itemType` but 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 replace `ORG_CODE: 'ORG'` with `ORG_CODE: orgCode`. Pattern matches `correspondence.service.ts` line 263-269. +- **Dependencies**: none (parallel) +- **Status**: [ ] +- **Note**: Fixes M1 — `Organization.organizationCode` field confirmed at `organization.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) 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 + +--- + +## 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) +```