From aaa5da3ec19595ce6839c835acad66ef78a264d0 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 17 Dec 2025 17:04:06 +0700 Subject: [PATCH] 251217:1704 Docunment Number: Update to 1.6.2 --- .aignore | 1 + .prettierignore | 2 + .../document-numbering.service.ts | 2 + {infrastructure => docs}/0.html | 0 .../Markdown/0_Requirements_V1_4_3.md | 0 .../Markdown/0_Requirements_V1_4_4.md | 0 .../Markdown/1_FullStackJS_V1_4_3.md | 0 .../Markdown/1_FullStackJS_V1_4_4.md | 0 .../Markdown/2_Backend_Plan_Phase6A_V1_4_3.md | 0 .../Markdown/2_Backend_Plan_V1_4_3.md | 0 .../Markdown/2_Backend_Plan_V1_4_4.Phase6A.md | 0 .../2_Backend_Plan_V1_4_4.Phase_Addition.md | 0 .../Markdown/2_Backend_Plan_V1_4_4.md | 0 .../Markdown/3_Frontend_Plan_V1_4_3.md | 0 .../Markdown/3_Frontend_Plan_V1_4_4.md | 0 .../Markdown/4_Data_Dictionary_V1_4_3.md | 0 .../Markdown/4_Data_Dictionary_V1_4_4.md | 0 .../Markdown/FullStackJS_Guidelines.md | 0 .../Markdown/FullStackJS_Guidelines01.md | 0 ...LCBP3-DMS Backend Documentation (ฉบับสมบูรณ์) | 0 ...CBP3-DMS Frontend Documentation (ฉบับสมบูรณ์) | 0 ...CBP3-DMS Requirements Specification (v2.0) | 0 .../LCBP3-DMS Requirements Specification.bak | 0 ...P3-DMS V1_1_0_application _requirements.md | 0 .../Markdown/LCBP3-DMS V1_1_1_FullStackJS.md | 0 ...P3-DMS V1_1_1_application _requirements.md | 0 ...Task Breakdown สำหรับ Phase 2A–2C (v1.4.2).md | 0 .../LCBP3-DMS_V1_1_0_application _database.md | 0 .../LCBP3-DMS_V1_2_0_Data_Dictionary.ิbak | 0 .../LCBP3-DMS_V1_2_1_Data_Dictionary.md | 0 .../Markdown/LCBP3-DMS_V1_2_1_FullStackJS.md | 0 ...P3-DMS_V1_2_1_application _requirements.md | 0 .../LCBP3-DMS_V1_3_0_Data_Dictionary.md | 0 .../Markdown/LCBP3-DMS_V1_3_0_FullStackJS.md | 0 .../Markdown/LCBP3-DMS_V1_3_0_Test_Plan_TH.md | 0 .../LCBP3-DMS_V1_3_0_backend_dev_plan.md | 0 .../LCBP3-DMS_V1_3_0_frontend_dev_plan.md | 0 .../Markdown/LCBP3-DMS_V1_3_0_requirements.md | 0 ...BP3-DMS_V1_4_0_Backend_Development_Plan.md | 0 .../LCBP3-DMS_V1_4_0_Data_Dictionary.bak.md | 0 .../LCBP3-DMS_V1_4_0_Data_Dictionary.md | 0 ...P3-DMS_V1_4_0_Frontend_Development_Plan.md | 0 .../Markdown/LCBP3-DMS_V1_4_0_FullStackJS.md | 0 .../Markdown/LCBP3-DMS_V1_4_0_requirements.md | 0 ...BP3-DMS_V1_4_1_Backend_Development_Plan.md | 0 ...MS_V1_4_1_Backend_Development_Plan_Grok.md | 0 .../LCBP3-DMS_V1_4_1_Data_Dictionary.md | 0 ...P3-DMS_V1_4_1_Frontend_Development_Plan.md | 0 .../Markdown/LCBP3-DMS_V1_4_1_FullStackJS.md | 0 .../Markdown/LCBP3-DMS_V1_4_1_Requirements.md | 0 ..._V1_4_2_Backend_Development_Plan (Patched) | 0 .../LCBP3-DMS_V1_4_2_FullStackJS (Patched) | 0 .../LCBP3-DMS_V1_4_2_Requirements (Patched) | 0 .../LCBP3-DMS_V1_4_2_Requirements..bak.md | 0 {infrastructure => docs}/Markdown/icon.md | 0 ...BP3-DMS_V1_4_2_Backend_Development_Plan.md | 0 .../Project/T0-0 Setting Project.md | 0 .../Project/T1-0 Setting Project.md | 0 .../Project/T2-0 Setting Project.md | 0 .../Project/T2-Postman.md | 0 .../Project/T3-0 Setting Project.md | 0 .../Project/T3-Postman.md | 0 {infrastructure => docs}/Project/V1_4_2.zip | Bin .../SQL/01_dms_v1_0_0.bak.sql | 0 .../SQL/01_dms_v1_0_0.sql | 0 .../SQL/01_dms_v1_0_0_patch.sql | 0 .../SQL/01_dms_v1_0_1.sql | 0 .../SQL/01_lcbp3_v1_1_0.sql | 0 .../SQL/01_lcbp3_v1_1_1.sql | 0 .../SQL/01_lcbp3_v1_2_0.sql | 0 .../SQL/01_lcbp3_v1_3_0.sql | 0 .../SQL/01_lcbp3_v1_3_0.txt | 0 .../SQL/01_lcbp3_v1_4_0 copy.sql | 0 .../SQL/01_lcbp3_v1_4_0.sql | 0 .../SQL/01_lcbp3_v1_4_0.txt | 0 .../SQL/01_lcbp3_v1_4_1.sql | 0 .../SQL/01_lcbp3_v1_4_3.sql | 0 .../SQL/8_lcbp3_v1_4_4.sql | 0 .../SQL/8_lcbp3_v1_4_4_seed.sql | 0 {infrastructure => docs}/SQL/Cluad.sql | 0 {infrastructure => docs}/SQL/seed01.sql | 0 {infrastructure => docs}/SQL/seed02.sql | 0 {infrastructure => docs}/SQL/temp.sql | 0 {infrastructure => docs}/SQL/triggers.sql | 0 docs/backup/03.11-document-numbering-add.md | 682 +++++ docs/backup/03.11-document-numbering.md | 1871 +++++++++++++ ...03.11-document-numbering_schema_section.md | 0 {infrastructure => docs}/backup/1.bak | 0 .../backup/DMS README.bak | 0 {infrastructure => docs}/backup/NestJS01.bak | 0 {infrastructure => docs}/backup/NextJS01.bak | 0 .../backup/backend_setup.bak | 0 .../backup/data-dictionary-v1.5.1.md | 0 docs/backup/document-numbering-add.md | 1813 ++++++++++++ docs/backup/document-numbering.md | 813 ++++++ {infrastructure => docs}/backup/features.bak | 0 .../backup/lcbp3-v1.5.1-schema.sql | 0 .../backup/lcbp3-v1.5.1-seed-basic.sql | 0 .../lcbp3-v1.5.1-seed-contractdrawing.sql | 0 .../backup/lcbp3-v1.5.1-seed-permissions.sql | 0 {infrastructure => docs}/backup/workflow.bak | 0 .../03.11-document-numbering.md | 2473 +++++++---------- specs/01-requirements/file.tmp | 0 specs/02-architecture/README.md | 4 +- specs/02-architecture/system-architecture.md | 81 +- specs/03-implementation/document-numbering.md | 1017 ++++--- ...ck-js-v1.5.0.md => fullftack-js-v1.6.2.md} | 14 +- .../document-numbering-operations.md | 16 +- .../ADR-002-document-numbering-strategy.md | 25 +- ...TASK-BE-017-document-numbering-refactor.md | 166 ++ ...TASK-FE-017-document-numbering-refactor.md | 192 ++ .../08-infrastructure}/Git_command.md | 0 .../08-infrastructure}/Gitea_setting.md | 0 .../08-infrastructure/Infrastructure Setup.md | 957 +++++++ .../08-infrastructure}/MariaDB_setting.md | 0 .../08-infrastructure}/NPM_setting.md | 0 .../08-infrastructure}/Securities.md | 0 .../08-infrastructure}/Service_setting.md | 0 .../08-infrastructure}/n8n_setting.md | 0 .../08-infrastructure}/แผนผัง Network.md | 0 ...12-17-document-numbering-v162-alignment.md | 46 + 121 files changed, 8072 insertions(+), 2103 deletions(-) rename {infrastructure => docs}/0.html (100%) rename {infrastructure => docs}/Markdown/0_Requirements_V1_4_3.md (100%) rename {infrastructure => docs}/Markdown/0_Requirements_V1_4_4.md (100%) rename {infrastructure => docs}/Markdown/1_FullStackJS_V1_4_3.md (100%) rename {infrastructure => docs}/Markdown/1_FullStackJS_V1_4_4.md (100%) rename {infrastructure => docs}/Markdown/2_Backend_Plan_Phase6A_V1_4_3.md (100%) rename {infrastructure => docs}/Markdown/2_Backend_Plan_V1_4_3.md (100%) rename {infrastructure => docs}/Markdown/2_Backend_Plan_V1_4_4.Phase6A.md (100%) rename {infrastructure => docs}/Markdown/2_Backend_Plan_V1_4_4.Phase_Addition.md (100%) rename {infrastructure => docs}/Markdown/2_Backend_Plan_V1_4_4.md (100%) rename {infrastructure => docs}/Markdown/3_Frontend_Plan_V1_4_3.md (100%) rename {infrastructure => docs}/Markdown/3_Frontend_Plan_V1_4_4.md (100%) rename {infrastructure => docs}/Markdown/4_Data_Dictionary_V1_4_3.md (100%) rename {infrastructure => docs}/Markdown/4_Data_Dictionary_V1_4_4.md (100%) rename {infrastructure => docs}/Markdown/FullStackJS_Guidelines.md (100%) rename {infrastructure => docs}/Markdown/FullStackJS_Guidelines01.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS Backend Documentation (ฉบับสมบูรณ์) (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS Frontend Documentation (ฉบับสมบูรณ์) (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS Requirements Specification (v2.0) (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS Requirements Specification.bak (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS V1_1_0_application _requirements.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS V1_1_1_FullStackJS.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS V1_1_1_application _requirements.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS — Task Breakdown สำหรับ Phase 2A–2C (v1.4.2).md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_1_0_application _database.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_2_0_Data_Dictionary.ิbak (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_2_1_Data_Dictionary.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_2_1_FullStackJS.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_2_1_application _requirements.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_3_0_Data_Dictionary.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_3_0_FullStackJS.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_3_0_Test_Plan_TH.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_3_0_backend_dev_plan.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_3_0_frontend_dev_plan.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_3_0_requirements.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_0_Backend_Development_Plan.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.bak.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_0_Frontend_Development_Plan.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_0_FullStackJS.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_0_requirements.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan_Grok.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_1_Data_Dictionary.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_1_Frontend_Development_Plan.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_1_FullStackJS.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_1_Requirements.md (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_2_Backend_Development_Plan (Patched) (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_2_FullStackJS (Patched) (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_2_Requirements (Patched) (100%) rename {infrastructure => docs}/Markdown/LCBP3-DMS_V1_4_2_Requirements..bak.md (100%) rename {infrastructure => docs}/Markdown/icon.md (100%) rename {infrastructure => docs}/Markdown/working LCBP3-DMS_V1_4_2_Backend_Development_Plan.md (100%) rename {infrastructure => docs}/Project/T0-0 Setting Project.md (100%) rename {infrastructure => docs}/Project/T1-0 Setting Project.md (100%) rename {infrastructure => docs}/Project/T2-0 Setting Project.md (100%) rename {infrastructure => docs}/Project/T2-Postman.md (100%) rename {infrastructure => docs}/Project/T3-0 Setting Project.md (100%) rename {infrastructure => docs}/Project/T3-Postman.md (100%) rename {infrastructure => docs}/Project/V1_4_2.zip (100%) rename {infrastructure => docs}/SQL/01_dms_v1_0_0.bak.sql (100%) rename {infrastructure => docs}/SQL/01_dms_v1_0_0.sql (100%) rename {infrastructure => docs}/SQL/01_dms_v1_0_0_patch.sql (100%) rename {infrastructure => docs}/SQL/01_dms_v1_0_1.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_1_0.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_1_1.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_2_0.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_3_0.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_3_0.txt (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_4_0 copy.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_4_0.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_4_0.txt (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_4_1.sql (100%) rename {infrastructure => docs}/SQL/01_lcbp3_v1_4_3.sql (100%) rename {infrastructure => docs}/SQL/8_lcbp3_v1_4_4.sql (100%) rename {infrastructure => docs}/SQL/8_lcbp3_v1_4_4_seed.sql (100%) rename {infrastructure => docs}/SQL/Cluad.sql (100%) rename {infrastructure => docs}/SQL/seed01.sql (100%) rename {infrastructure => docs}/SQL/seed02.sql (100%) rename {infrastructure => docs}/SQL/temp.sql (100%) rename {infrastructure => docs}/SQL/triggers.sql (100%) create mode 100644 docs/backup/03.11-document-numbering-add.md create mode 100644 docs/backup/03.11-document-numbering.md rename {specs/01-requirements => docs/backup}/03.11-document-numbering_schema_section.md (100%) rename {infrastructure => docs}/backup/1.bak (100%) rename {infrastructure => docs}/backup/DMS README.bak (100%) rename {infrastructure => docs}/backup/NestJS01.bak (100%) rename {infrastructure => docs}/backup/NextJS01.bak (100%) rename {infrastructure => docs}/backup/backend_setup.bak (100%) rename {infrastructure => docs}/backup/data-dictionary-v1.5.1.md (100%) create mode 100644 docs/backup/document-numbering-add.md create mode 100644 docs/backup/document-numbering.md rename {infrastructure => docs}/backup/features.bak (100%) rename {infrastructure => docs}/backup/lcbp3-v1.5.1-schema.sql (100%) rename {infrastructure => docs}/backup/lcbp3-v1.5.1-seed-basic.sql (100%) rename {infrastructure => docs}/backup/lcbp3-v1.5.1-seed-contractdrawing.sql (100%) rename {infrastructure => docs}/backup/lcbp3-v1.5.1-seed-permissions.sql (100%) rename {infrastructure => docs}/backup/workflow.bak (100%) create mode 100644 specs/01-requirements/file.tmp rename specs/03-implementation/{fullftack-js-v1.5.0.md => fullftack-js-v1.6.2.md} (99%) create mode 100644 specs/06-tasks/TASK-BE-017-document-numbering-refactor.md create mode 100644 specs/06-tasks/TASK-FE-017-document-numbering-refactor.md rename {infrastructure => specs/08-infrastructure}/Git_command.md (100%) rename {infrastructure => specs/08-infrastructure}/Gitea_setting.md (100%) create mode 100644 specs/08-infrastructure/Infrastructure Setup.md rename {infrastructure => specs/08-infrastructure}/MariaDB_setting.md (100%) rename {infrastructure => specs/08-infrastructure}/NPM_setting.md (100%) rename {infrastructure => specs/08-infrastructure}/Securities.md (100%) rename {infrastructure => specs/08-infrastructure}/Service_setting.md (100%) rename {infrastructure => specs/08-infrastructure}/n8n_setting.md (100%) rename {infrastructure => specs/08-infrastructure}/แผนผัง Network.md (100%) create mode 100644 specs/09-history/2025-12-17-document-numbering-v162-alignment.md diff --git a/.aignore b/.aignore index c7ab3de..2596f67 100644 --- a/.aignore +++ b/.aignore @@ -5,5 +5,6 @@ backend/dist/ frontend/dist/ backend/build/ frontend/build/ +docs/backup/ .git/ *.log diff --git a/.prettierignore b/.prettierignore index d08fec6..84d18ed 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,6 @@ node_modules +backend/node_modules/ +frontend/node_modules/ dist build *.min.js diff --git a/backend/src/modules/document-numbering/document-numbering.service.ts b/backend/src/modules/document-numbering/document-numbering.service.ts index 011eb6f..bf74b5a 100644 --- a/backend/src/modules/document-numbering/document-numbering.service.ts +++ b/backend/src/modules/document-numbering/document-numbering.service.ts @@ -51,10 +51,12 @@ export class DocumentNumberingService implements OnModuleInit { onModuleInit() { const host = this.configService.get('REDIS_HOST', 'localhost'); const port = this.configService.get('REDIS_PORT', 6379); + const password = this.configService.get('REDIS_PASSWORD'); this.redisClient = new Redis({ host, port, + password, retryStrategy: (times) => Math.min(times * 50, 2000), maxRetriesPerRequest: 3, }); diff --git a/infrastructure/0.html b/docs/0.html similarity index 100% rename from infrastructure/0.html rename to docs/0.html diff --git a/infrastructure/Markdown/0_Requirements_V1_4_3.md b/docs/Markdown/0_Requirements_V1_4_3.md similarity index 100% rename from infrastructure/Markdown/0_Requirements_V1_4_3.md rename to docs/Markdown/0_Requirements_V1_4_3.md diff --git a/infrastructure/Markdown/0_Requirements_V1_4_4.md b/docs/Markdown/0_Requirements_V1_4_4.md similarity index 100% rename from infrastructure/Markdown/0_Requirements_V1_4_4.md rename to docs/Markdown/0_Requirements_V1_4_4.md diff --git a/infrastructure/Markdown/1_FullStackJS_V1_4_3.md b/docs/Markdown/1_FullStackJS_V1_4_3.md similarity index 100% rename from infrastructure/Markdown/1_FullStackJS_V1_4_3.md rename to docs/Markdown/1_FullStackJS_V1_4_3.md diff --git a/infrastructure/Markdown/1_FullStackJS_V1_4_4.md b/docs/Markdown/1_FullStackJS_V1_4_4.md similarity index 100% rename from infrastructure/Markdown/1_FullStackJS_V1_4_4.md rename to docs/Markdown/1_FullStackJS_V1_4_4.md diff --git a/infrastructure/Markdown/2_Backend_Plan_Phase6A_V1_4_3.md b/docs/Markdown/2_Backend_Plan_Phase6A_V1_4_3.md similarity index 100% rename from infrastructure/Markdown/2_Backend_Plan_Phase6A_V1_4_3.md rename to docs/Markdown/2_Backend_Plan_Phase6A_V1_4_3.md diff --git a/infrastructure/Markdown/2_Backend_Plan_V1_4_3.md b/docs/Markdown/2_Backend_Plan_V1_4_3.md similarity index 100% rename from infrastructure/Markdown/2_Backend_Plan_V1_4_3.md rename to docs/Markdown/2_Backend_Plan_V1_4_3.md diff --git a/infrastructure/Markdown/2_Backend_Plan_V1_4_4.Phase6A.md b/docs/Markdown/2_Backend_Plan_V1_4_4.Phase6A.md similarity index 100% rename from infrastructure/Markdown/2_Backend_Plan_V1_4_4.Phase6A.md rename to docs/Markdown/2_Backend_Plan_V1_4_4.Phase6A.md diff --git a/infrastructure/Markdown/2_Backend_Plan_V1_4_4.Phase_Addition.md b/docs/Markdown/2_Backend_Plan_V1_4_4.Phase_Addition.md similarity index 100% rename from infrastructure/Markdown/2_Backend_Plan_V1_4_4.Phase_Addition.md rename to docs/Markdown/2_Backend_Plan_V1_4_4.Phase_Addition.md diff --git a/infrastructure/Markdown/2_Backend_Plan_V1_4_4.md b/docs/Markdown/2_Backend_Plan_V1_4_4.md similarity index 100% rename from infrastructure/Markdown/2_Backend_Plan_V1_4_4.md rename to docs/Markdown/2_Backend_Plan_V1_4_4.md diff --git a/infrastructure/Markdown/3_Frontend_Plan_V1_4_3.md b/docs/Markdown/3_Frontend_Plan_V1_4_3.md similarity index 100% rename from infrastructure/Markdown/3_Frontend_Plan_V1_4_3.md rename to docs/Markdown/3_Frontend_Plan_V1_4_3.md diff --git a/infrastructure/Markdown/3_Frontend_Plan_V1_4_4.md b/docs/Markdown/3_Frontend_Plan_V1_4_4.md similarity index 100% rename from infrastructure/Markdown/3_Frontend_Plan_V1_4_4.md rename to docs/Markdown/3_Frontend_Plan_V1_4_4.md diff --git a/infrastructure/Markdown/4_Data_Dictionary_V1_4_3.md b/docs/Markdown/4_Data_Dictionary_V1_4_3.md similarity index 100% rename from infrastructure/Markdown/4_Data_Dictionary_V1_4_3.md rename to docs/Markdown/4_Data_Dictionary_V1_4_3.md diff --git a/infrastructure/Markdown/4_Data_Dictionary_V1_4_4.md b/docs/Markdown/4_Data_Dictionary_V1_4_4.md similarity index 100% rename from infrastructure/Markdown/4_Data_Dictionary_V1_4_4.md rename to docs/Markdown/4_Data_Dictionary_V1_4_4.md diff --git a/infrastructure/Markdown/FullStackJS_Guidelines.md b/docs/Markdown/FullStackJS_Guidelines.md similarity index 100% rename from infrastructure/Markdown/FullStackJS_Guidelines.md rename to docs/Markdown/FullStackJS_Guidelines.md diff --git a/infrastructure/Markdown/FullStackJS_Guidelines01.md b/docs/Markdown/FullStackJS_Guidelines01.md similarity index 100% rename from infrastructure/Markdown/FullStackJS_Guidelines01.md rename to docs/Markdown/FullStackJS_Guidelines01.md diff --git a/infrastructure/Markdown/LCBP3-DMS Backend Documentation (ฉบับสมบูรณ์) b/docs/Markdown/LCBP3-DMS Backend Documentation (ฉบับสมบูรณ์) similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS Backend Documentation (ฉบับสมบูรณ์) rename to docs/Markdown/LCBP3-DMS Backend Documentation (ฉบับสมบูรณ์) diff --git a/infrastructure/Markdown/LCBP3-DMS Frontend Documentation (ฉบับสมบูรณ์) b/docs/Markdown/LCBP3-DMS Frontend Documentation (ฉบับสมบูรณ์) similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS Frontend Documentation (ฉบับสมบูรณ์) rename to docs/Markdown/LCBP3-DMS Frontend Documentation (ฉบับสมบูรณ์) diff --git a/infrastructure/Markdown/LCBP3-DMS Requirements Specification (v2.0) b/docs/Markdown/LCBP3-DMS Requirements Specification (v2.0) similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS Requirements Specification (v2.0) rename to docs/Markdown/LCBP3-DMS Requirements Specification (v2.0) diff --git a/infrastructure/Markdown/LCBP3-DMS Requirements Specification.bak b/docs/Markdown/LCBP3-DMS Requirements Specification.bak similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS Requirements Specification.bak rename to docs/Markdown/LCBP3-DMS Requirements Specification.bak diff --git a/infrastructure/Markdown/LCBP3-DMS V1_1_0_application _requirements.md b/docs/Markdown/LCBP3-DMS V1_1_0_application _requirements.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS V1_1_0_application _requirements.md rename to docs/Markdown/LCBP3-DMS V1_1_0_application _requirements.md diff --git a/infrastructure/Markdown/LCBP3-DMS V1_1_1_FullStackJS.md b/docs/Markdown/LCBP3-DMS V1_1_1_FullStackJS.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS V1_1_1_FullStackJS.md rename to docs/Markdown/LCBP3-DMS V1_1_1_FullStackJS.md diff --git a/infrastructure/Markdown/LCBP3-DMS V1_1_1_application _requirements.md b/docs/Markdown/LCBP3-DMS V1_1_1_application _requirements.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS V1_1_1_application _requirements.md rename to docs/Markdown/LCBP3-DMS V1_1_1_application _requirements.md diff --git a/infrastructure/Markdown/LCBP3-DMS — Task Breakdown สำหรับ Phase 2A–2C (v1.4.2).md b/docs/Markdown/LCBP3-DMS — Task Breakdown สำหรับ Phase 2A–2C (v1.4.2).md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS — Task Breakdown สำหรับ Phase 2A–2C (v1.4.2).md rename to docs/Markdown/LCBP3-DMS — Task Breakdown สำหรับ Phase 2A–2C (v1.4.2).md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_1_0_application _database.md b/docs/Markdown/LCBP3-DMS_V1_1_0_application _database.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_1_0_application _database.md rename to docs/Markdown/LCBP3-DMS_V1_1_0_application _database.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_2_0_Data_Dictionary.ิbak b/docs/Markdown/LCBP3-DMS_V1_2_0_Data_Dictionary.ิbak similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_2_0_Data_Dictionary.ิbak rename to docs/Markdown/LCBP3-DMS_V1_2_0_Data_Dictionary.ิbak diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_2_1_Data_Dictionary.md b/docs/Markdown/LCBP3-DMS_V1_2_1_Data_Dictionary.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_2_1_Data_Dictionary.md rename to docs/Markdown/LCBP3-DMS_V1_2_1_Data_Dictionary.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_2_1_FullStackJS.md b/docs/Markdown/LCBP3-DMS_V1_2_1_FullStackJS.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_2_1_FullStackJS.md rename to docs/Markdown/LCBP3-DMS_V1_2_1_FullStackJS.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_2_1_application _requirements.md b/docs/Markdown/LCBP3-DMS_V1_2_1_application _requirements.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_2_1_application _requirements.md rename to docs/Markdown/LCBP3-DMS_V1_2_1_application _requirements.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_3_0_Data_Dictionary.md b/docs/Markdown/LCBP3-DMS_V1_3_0_Data_Dictionary.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_3_0_Data_Dictionary.md rename to docs/Markdown/LCBP3-DMS_V1_3_0_Data_Dictionary.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_3_0_FullStackJS.md b/docs/Markdown/LCBP3-DMS_V1_3_0_FullStackJS.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_3_0_FullStackJS.md rename to docs/Markdown/LCBP3-DMS_V1_3_0_FullStackJS.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_3_0_Test_Plan_TH.md b/docs/Markdown/LCBP3-DMS_V1_3_0_Test_Plan_TH.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_3_0_Test_Plan_TH.md rename to docs/Markdown/LCBP3-DMS_V1_3_0_Test_Plan_TH.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_3_0_backend_dev_plan.md b/docs/Markdown/LCBP3-DMS_V1_3_0_backend_dev_plan.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_3_0_backend_dev_plan.md rename to docs/Markdown/LCBP3-DMS_V1_3_0_backend_dev_plan.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_3_0_frontend_dev_plan.md b/docs/Markdown/LCBP3-DMS_V1_3_0_frontend_dev_plan.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_3_0_frontend_dev_plan.md rename to docs/Markdown/LCBP3-DMS_V1_3_0_frontend_dev_plan.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_3_0_requirements.md b/docs/Markdown/LCBP3-DMS_V1_3_0_requirements.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_3_0_requirements.md rename to docs/Markdown/LCBP3-DMS_V1_3_0_requirements.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_0_Backend_Development_Plan.md b/docs/Markdown/LCBP3-DMS_V1_4_0_Backend_Development_Plan.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_0_Backend_Development_Plan.md rename to docs/Markdown/LCBP3-DMS_V1_4_0_Backend_Development_Plan.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.bak.md b/docs/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.bak.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.bak.md rename to docs/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.bak.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.md b/docs/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.md rename to docs/Markdown/LCBP3-DMS_V1_4_0_Data_Dictionary.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_0_Frontend_Development_Plan.md b/docs/Markdown/LCBP3-DMS_V1_4_0_Frontend_Development_Plan.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_0_Frontend_Development_Plan.md rename to docs/Markdown/LCBP3-DMS_V1_4_0_Frontend_Development_Plan.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_0_FullStackJS.md b/docs/Markdown/LCBP3-DMS_V1_4_0_FullStackJS.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_0_FullStackJS.md rename to docs/Markdown/LCBP3-DMS_V1_4_0_FullStackJS.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_0_requirements.md b/docs/Markdown/LCBP3-DMS_V1_4_0_requirements.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_0_requirements.md rename to docs/Markdown/LCBP3-DMS_V1_4_0_requirements.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan.md b/docs/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan.md rename to docs/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan_Grok.md b/docs/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan_Grok.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan_Grok.md rename to docs/Markdown/LCBP3-DMS_V1_4_1_Backend_Development_Plan_Grok.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_1_Data_Dictionary.md b/docs/Markdown/LCBP3-DMS_V1_4_1_Data_Dictionary.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_1_Data_Dictionary.md rename to docs/Markdown/LCBP3-DMS_V1_4_1_Data_Dictionary.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_1_Frontend_Development_Plan.md b/docs/Markdown/LCBP3-DMS_V1_4_1_Frontend_Development_Plan.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_1_Frontend_Development_Plan.md rename to docs/Markdown/LCBP3-DMS_V1_4_1_Frontend_Development_Plan.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_1_FullStackJS.md b/docs/Markdown/LCBP3-DMS_V1_4_1_FullStackJS.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_1_FullStackJS.md rename to docs/Markdown/LCBP3-DMS_V1_4_1_FullStackJS.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_1_Requirements.md b/docs/Markdown/LCBP3-DMS_V1_4_1_Requirements.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_1_Requirements.md rename to docs/Markdown/LCBP3-DMS_V1_4_1_Requirements.md diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_2_Backend_Development_Plan (Patched) b/docs/Markdown/LCBP3-DMS_V1_4_2_Backend_Development_Plan (Patched) similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_2_Backend_Development_Plan (Patched) rename to docs/Markdown/LCBP3-DMS_V1_4_2_Backend_Development_Plan (Patched) diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_2_FullStackJS (Patched) b/docs/Markdown/LCBP3-DMS_V1_4_2_FullStackJS (Patched) similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_2_FullStackJS (Patched) rename to docs/Markdown/LCBP3-DMS_V1_4_2_FullStackJS (Patched) diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_2_Requirements (Patched) b/docs/Markdown/LCBP3-DMS_V1_4_2_Requirements (Patched) similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_2_Requirements (Patched) rename to docs/Markdown/LCBP3-DMS_V1_4_2_Requirements (Patched) diff --git a/infrastructure/Markdown/LCBP3-DMS_V1_4_2_Requirements..bak.md b/docs/Markdown/LCBP3-DMS_V1_4_2_Requirements..bak.md similarity index 100% rename from infrastructure/Markdown/LCBP3-DMS_V1_4_2_Requirements..bak.md rename to docs/Markdown/LCBP3-DMS_V1_4_2_Requirements..bak.md diff --git a/infrastructure/Markdown/icon.md b/docs/Markdown/icon.md similarity index 100% rename from infrastructure/Markdown/icon.md rename to docs/Markdown/icon.md diff --git a/infrastructure/Markdown/working LCBP3-DMS_V1_4_2_Backend_Development_Plan.md b/docs/Markdown/working LCBP3-DMS_V1_4_2_Backend_Development_Plan.md similarity index 100% rename from infrastructure/Markdown/working LCBP3-DMS_V1_4_2_Backend_Development_Plan.md rename to docs/Markdown/working LCBP3-DMS_V1_4_2_Backend_Development_Plan.md diff --git a/infrastructure/Project/T0-0 Setting Project.md b/docs/Project/T0-0 Setting Project.md similarity index 100% rename from infrastructure/Project/T0-0 Setting Project.md rename to docs/Project/T0-0 Setting Project.md diff --git a/infrastructure/Project/T1-0 Setting Project.md b/docs/Project/T1-0 Setting Project.md similarity index 100% rename from infrastructure/Project/T1-0 Setting Project.md rename to docs/Project/T1-0 Setting Project.md diff --git a/infrastructure/Project/T2-0 Setting Project.md b/docs/Project/T2-0 Setting Project.md similarity index 100% rename from infrastructure/Project/T2-0 Setting Project.md rename to docs/Project/T2-0 Setting Project.md diff --git a/infrastructure/Project/T2-Postman.md b/docs/Project/T2-Postman.md similarity index 100% rename from infrastructure/Project/T2-Postman.md rename to docs/Project/T2-Postman.md diff --git a/infrastructure/Project/T3-0 Setting Project.md b/docs/Project/T3-0 Setting Project.md similarity index 100% rename from infrastructure/Project/T3-0 Setting Project.md rename to docs/Project/T3-0 Setting Project.md diff --git a/infrastructure/Project/T3-Postman.md b/docs/Project/T3-Postman.md similarity index 100% rename from infrastructure/Project/T3-Postman.md rename to docs/Project/T3-Postman.md diff --git a/infrastructure/Project/V1_4_2.zip b/docs/Project/V1_4_2.zip similarity index 100% rename from infrastructure/Project/V1_4_2.zip rename to docs/Project/V1_4_2.zip diff --git a/infrastructure/SQL/01_dms_v1_0_0.bak.sql b/docs/SQL/01_dms_v1_0_0.bak.sql similarity index 100% rename from infrastructure/SQL/01_dms_v1_0_0.bak.sql rename to docs/SQL/01_dms_v1_0_0.bak.sql diff --git a/infrastructure/SQL/01_dms_v1_0_0.sql b/docs/SQL/01_dms_v1_0_0.sql similarity index 100% rename from infrastructure/SQL/01_dms_v1_0_0.sql rename to docs/SQL/01_dms_v1_0_0.sql diff --git a/infrastructure/SQL/01_dms_v1_0_0_patch.sql b/docs/SQL/01_dms_v1_0_0_patch.sql similarity index 100% rename from infrastructure/SQL/01_dms_v1_0_0_patch.sql rename to docs/SQL/01_dms_v1_0_0_patch.sql diff --git a/infrastructure/SQL/01_dms_v1_0_1.sql b/docs/SQL/01_dms_v1_0_1.sql similarity index 100% rename from infrastructure/SQL/01_dms_v1_0_1.sql rename to docs/SQL/01_dms_v1_0_1.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_1_0.sql b/docs/SQL/01_lcbp3_v1_1_0.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_1_0.sql rename to docs/SQL/01_lcbp3_v1_1_0.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_1_1.sql b/docs/SQL/01_lcbp3_v1_1_1.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_1_1.sql rename to docs/SQL/01_lcbp3_v1_1_1.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_2_0.sql b/docs/SQL/01_lcbp3_v1_2_0.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_2_0.sql rename to docs/SQL/01_lcbp3_v1_2_0.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_3_0.sql b/docs/SQL/01_lcbp3_v1_3_0.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_3_0.sql rename to docs/SQL/01_lcbp3_v1_3_0.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_3_0.txt b/docs/SQL/01_lcbp3_v1_3_0.txt similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_3_0.txt rename to docs/SQL/01_lcbp3_v1_3_0.txt diff --git a/infrastructure/SQL/01_lcbp3_v1_4_0 copy.sql b/docs/SQL/01_lcbp3_v1_4_0 copy.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_4_0 copy.sql rename to docs/SQL/01_lcbp3_v1_4_0 copy.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_4_0.sql b/docs/SQL/01_lcbp3_v1_4_0.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_4_0.sql rename to docs/SQL/01_lcbp3_v1_4_0.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_4_0.txt b/docs/SQL/01_lcbp3_v1_4_0.txt similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_4_0.txt rename to docs/SQL/01_lcbp3_v1_4_0.txt diff --git a/infrastructure/SQL/01_lcbp3_v1_4_1.sql b/docs/SQL/01_lcbp3_v1_4_1.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_4_1.sql rename to docs/SQL/01_lcbp3_v1_4_1.sql diff --git a/infrastructure/SQL/01_lcbp3_v1_4_3.sql b/docs/SQL/01_lcbp3_v1_4_3.sql similarity index 100% rename from infrastructure/SQL/01_lcbp3_v1_4_3.sql rename to docs/SQL/01_lcbp3_v1_4_3.sql diff --git a/infrastructure/SQL/8_lcbp3_v1_4_4.sql b/docs/SQL/8_lcbp3_v1_4_4.sql similarity index 100% rename from infrastructure/SQL/8_lcbp3_v1_4_4.sql rename to docs/SQL/8_lcbp3_v1_4_4.sql diff --git a/infrastructure/SQL/8_lcbp3_v1_4_4_seed.sql b/docs/SQL/8_lcbp3_v1_4_4_seed.sql similarity index 100% rename from infrastructure/SQL/8_lcbp3_v1_4_4_seed.sql rename to docs/SQL/8_lcbp3_v1_4_4_seed.sql diff --git a/infrastructure/SQL/Cluad.sql b/docs/SQL/Cluad.sql similarity index 100% rename from infrastructure/SQL/Cluad.sql rename to docs/SQL/Cluad.sql diff --git a/infrastructure/SQL/seed01.sql b/docs/SQL/seed01.sql similarity index 100% rename from infrastructure/SQL/seed01.sql rename to docs/SQL/seed01.sql diff --git a/infrastructure/SQL/seed02.sql b/docs/SQL/seed02.sql similarity index 100% rename from infrastructure/SQL/seed02.sql rename to docs/SQL/seed02.sql diff --git a/infrastructure/SQL/temp.sql b/docs/SQL/temp.sql similarity index 100% rename from infrastructure/SQL/temp.sql rename to docs/SQL/temp.sql diff --git a/infrastructure/SQL/triggers.sql b/docs/SQL/triggers.sql similarity index 100% rename from infrastructure/SQL/triggers.sql rename to docs/SQL/triggers.sql diff --git a/docs/backup/03.11-document-numbering-add.md b/docs/backup/03.11-document-numbering-add.md new file mode 100644 index 0000000..4c665ca --- /dev/null +++ b/docs/backup/03.11-document-numbering-add.md @@ -0,0 +1,682 @@ +# Document Numbering Requirements + +**Version**: 1.6.1 +**Last Updated**: 2025-01-16 +**Status**: draft +**Related ADRs**: ADR-018-document-numbering-strategy + +--- + +## 1. Overview + +### 1.1 Purpose +ระบบ Document Numbering สำหรับสร้างเลขที่เอกสารอัตโนมัติที่มีความเป็นเอกลักษณ์ (unique) และสามารถติดตามได้ (traceable) สำหรับเอกสารทุกประเภทในระบบ LCBP3-DMS + +### 1.2 Scope +- Auto-generation ของเลขที่เอกสารตามรูปแบบที่กำหนด +- Manual override สำหรับการ import เอกสารเก่า +- Cancelled number handling (ไม่ reuse) +- Void & Replace pattern สำหรับการแทนที่เอกสาร +- Distributed locking เพื่อป้องกัน race condition +- Complete audit trail สำหรับทุก operation + +### 1.3 Document Types Supported +- Correspondences (COR) +- Request for Approvals (RFA) +- Contract Drawings (CD) +- Shop Drawings (SD) +- Transmittals (TRN) +- Circulation Sheets (CIR) + +--- + +## 2. Functional Requirements + +### 2.1 Auto Number Generation + +#### FR-DN-001: Generate Sequential Number +**Priority**: CRITICAL +**Status**: Required + +**Description**: +ระบบต้องสามารถสร้างเลขที่เอกสารอัตโนมัติตามลำดับ (sequential) โดยไม่ซ้ำกัน + +**Acceptance Criteria**: +- เลขที่เอกสารต้องเป็น unique ใน scope ที่กำหนด +- ต้องเพิ่มขึ้นทีละ 1 (increment by 1) +- ต้องรองรับ concurrent requests โดยไม่มีเลขที่ซ้ำ +- Response time < 100ms (p95) + +**Example**: +``` +COR-00001-2025 +COR-00002-2025 +COR-00003-2025 +``` + +--- + +#### FR-DN-002: Configurable Number Format +**Priority**: HIGH +**Status**: Required + +**Description**: +ระบบต้องรองรับการกำหนดรูปแบบเลขที่เอกสารที่หลากหลาย + +**Format Tokens**: +- `{PREFIX}` - คำนำหน้าตามประเภทเอกสาร (e.g., COR, RFA) +- `{YYYY}` - ปี 4 หลัก (e.g., 2025) +- `{YY}` - ปี 2 หลัก (e.g., 25) +- `{MM}` - เดือน 2 หลัก (e.g., 01-12) +- `{SEQ:n}` - sequence number ความยาว n หลัก (e.g., {SEQ:5} = 00001) +- `{PROJECT}` - รหัสโครงการ +- `{CONTRACT}` - รหัสสัญญา + +**Acceptance Criteria**: +- รองรับ format tokens ที่ระบุ +- Admin สามารถกำหนด format ผ่าน UI ได้ +- Validate format ก่อน save +- แสดง preview ของเลขที่ที่จะถูกสร้าง + +**Examples**: +```typescript +// Correspondence format +"COR-{YYYY}-{SEQ:5}" +→ COR-2025-00001 + +// RFA format with project +"RFA-{PROJECT}-{YYYY}{MM}-{SEQ:4}" +→ RFA-LCBP3-202501-0001 + +// Drawing format +"{CONTRACT}-CD-{SEQ:6}" +→ C001-CD-000001 +``` + +--- + +#### FR-DN-003: Scope-based Sequences +**Priority**: HIGH +**Status**: Required + +**Description**: +ระบบต้องรองรับการสร้าง sequence ที่แยกตาม scope ที่ต่างกัน + +**Scopes**: +1. **Global**: Sequence ระดับระบบทั้งหมด +2. **Project**: Sequence แยกตามโครงการ +3. **Contract**: Sequence แยกตามสัญญา +4. **Yearly**: Sequence reset ทุกปี +5. **Monthly**: Sequence reset ทุกเดือน + +**Acceptance Criteria**: +- เลขที่ไม่ซ้ำภายใน scope เดียวกัน +- Scope ที่ต่างกันสามารถมีเลขที่เดียวกันได้ +- Support multiple active scopes + +**Example**: +``` +Project A: COR-A-2025-00001, COR-A-2025-00002 +Project B: COR-B-2025-00001, COR-B-2025-00002 + +Yearly Reset: +COR-2024-00999 (Dec 2024) +COR-2025-00001 (Jan 2025) +``` + +--- + +### 2.2 Manual Override + +#### FR-DN-004: Manual Number Assignment +**Priority**: HIGH +**Status**: Required + +**Description**: +ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override) + +**Use Cases**: +1. Import เอกสารเก่าจากระบบเดิม +2. External documents จาก client/consultant +3. Correction หลังพบความผิดพลาด + +**Acceptance Criteria**: +- ตรวจสอบ duplicate ก่อน save +- Validate format ตามรูปแบบที่กำหนด +- Auto-update sequence counter ถ้าเลขที่สูงกว่า current +- บันทึก audit log ว่าเป็น manual override +- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น + +**Validation Rules**: +```typescript +interface ManualNumberValidation { + format_match: boolean; // ตรง format หรือไม่ + not_duplicate: boolean; // ไม่ซ้ำ + in_valid_range: boolean; // อยู่ในช่วงที่กำหนด + permission_granted: boolean; // มีสิทธิ์ +} +``` + +--- + +#### FR-DN-005: Bulk Import Support +**Priority**: MEDIUM +**Status**: Required + +**Description**: +ระบบต้องรองรับการ import เอกสารหลายรายการพร้อมกัน + +**Acceptance Criteria**: +- รองรับไฟล์ CSV/Excel +- Validate ทุกรายการก่อน import +- แสดง preview ก่อน confirm +- Rollback ทั้งหมดถ้ามีรายการใดผิดพลาด (transactional) +- Auto-update sequence counters หลัง import +- Generate import report + +**CSV Format**: +```csv +document_type,document_number,created_at,metadata +COR,COR-2024-00001,2024-01-01,{"imported":true} +COR,COR-2024-00002,2024-01-05,{"imported":true} +``` + +--- + +### 2.3 Cancelled & Void Handling + +#### FR-DN-006: Skip Cancelled Numbers +**Priority**: HIGH +**Status**: Required + +**Description**: +เลขที่เอกสารที่ถูกยกเลิกต้องไม่ถูก reuse + +**Rationale**: +- รักษา audit trail ที่ชัดเจน +- ป้องกันความสับสน +- Legal compliance + +**Acceptance Criteria**: +- Cancelled number ยังคงอยู่ในฐานข้อมูลพร้อม status +- ระบบข้าม (skip) cancelled number เมื่อสร้างเลขที่ใหม่ +- บันทึกเหตุผลการยกเลิก +- แสดง cancelled numbers ใน audit trail + +**Example Timeline**: +``` +2025-00001 ✅ ACTIVE (created 2025-01-01) +2025-00002 ❌ CANCELLED (created 2025-01-02, cancelled 2025-01-03) +2025-00003 ✅ ACTIVE (created 2025-01-04) +``` + +--- + +#### FR-DN-007: Void and Replace +**Priority**: HIGH +**Status**: Required + +**Description**: +ระบบต้องรองรับการ void เอกสารและสร้างเอกสารใหม่แทน + +**Workflow**: +1. User เลือกเอกสารที่ต้องการ void +2. ระบุเหตุผล (required) +3. ระบบเปลี่ยน status เอกสารเดิมเป็น VOID +4. สร้างเอกสารใหม่ด้วยเลขที่ใหม่ +5. Link เอกสารใหม่กับเดิม (voided_from_id) + +**Acceptance Criteria**: +- เอกสารเดิม status = VOID (ไม่ลบ) +- เอกสารใหม่ได้เลขที่ต่อเนื่องจาก sequence +- มี reference link ระหว่างเอกสาร +- บันทึก void reason +- แสดง void history chain (A→B→C) + +**Database Relationship**: +```sql +-- Original document +id: 100 +document_number: COR-2025-00005 +status: VOID +void_reason: "ข้อมูลผิด" +voided_at: 2025-01-10 + +-- Replacement document +id: 101 +document_number: COR-2025-00006 +status: ACTIVE +voided_from_id: 100 +``` + +--- + +### 2.4 Concurrency & Performance + +#### FR-DN-008: Prevent Race Conditions +**Priority**: CRITICAL +**Status**: Required + +**Description**: +ระบบต้องป้องกันการสร้างเลขที่ซ้ำเมื่อมีการ request พร้อมกัน + +**Solution**: +- Distributed locking (Redlock) +- Database pessimistic locking +- Two-phase commit pattern + +**Acceptance Criteria**: +- Zero duplicate numbers ภายใต้ concurrent load (1000 req/s) +- Lock acquisition time < 50ms (avg) +- Automatic retry on lock failure (max 3 times) +- Timeout handling (30 seconds) + +**Load Test Requirements**: +```bash +# Must pass without duplicates +concurrent_users: 100 +requests_per_second: 500 +test_duration: 5 minutes +expected_duplicates: 0 +``` + +--- + +#### FR-DN-009: Two-Phase Commit +**Priority**: HIGH +**Status**: Required + +**Description**: +ใช้ Two-phase commit pattern เพื่อความสมบูรณ์ของข้อมูล + +**Phase 1: Reserve** +- ล็อกเลขที่และ reserve ไว้ชั่วคราว +- Set TTL 5 นาที +- Return reservation token + +**Phase 2: Confirm or Cancel** +- Confirm: บันทึกลงฐานข้อมูลถาวร +- Cancel: คืน lock และ reservation + +**Acceptance Criteria**: +- Reservation ต้อง expire หลัง 5 นาที +- Auto-cleanup expired reservations +- Support explicit cancel +- Idempotent confirmation + +**API Flow**: +```typescript +// Phase 1 +const { token, number } = await reserveNumber({ + document_type: 'COR', + project_id: 1 +}); + +// Do some work... + +// Phase 2 +await confirmReservation(token); +// OR +await cancelReservation(token); +``` + +--- + +### 2.5 Monitoring & Audit + +#### FR-DN-010: Complete Audit Trail +**Priority**: HIGH +**Status**: Required + +**Description**: +บันทึกทุก operation ที่เกิดขึ้นกับเลขที่เอกสาร + +**Events to Log**: +- Number reserved +- Number confirmed +- Number cancelled +- Manual override +- Void document +- Sequence adjusted +- Format changed + +**Audit Fields**: +```typescript +interface AuditLog { + id: number; + operation: string; + document_type: string; + document_number: string; + old_value?: any; + new_value?: any; + user_id: number; + ip_address: string; + user_agent: string; + timestamp: Date; + metadata: Record; +} +``` + +**Acceptance Criteria**: +- Log ทุก operation +- Searchable by user, date, type +- Export to CSV +- Retain for 7 years + +--- + +#### FR-DN-011: Metrics & Alerting +**Priority**: MEDIUM +**Status**: Required + +**Description**: +แสดงสถิติและส่ง alert เมื่อเกิดปัญหา + +**Metrics**: +- Sequence utilization (% of max) +- Average lock wait time +- Failed lock attempts +- Numbers generated per day +- Manual overrides per day + +**Alerts**: +- Sequence >90% used (WARNING) +- Sequence >95% used (CRITICAL) +- Lock wait time >1s (WARNING) +- Redis unavailable (CRITICAL) +- High error rate (WARNING) + +**Acceptance Criteria**: +- Real-time dashboard (Grafana) +- Email/LINE notifications +- Alert history tracking +- Configurable thresholds + +--- + +## 3. Non-Functional Requirements + +### 3.1 Performance + +#### NFR-DN-001: Response Time +- Number generation: <100ms (p95) +- Lock acquisition: <50ms (avg) +- Bulk import: <5s per 100 records + +#### NFR-DN-002: Throughput +- Support >500 req/s +- Scale horizontally (add Redis nodes) + +### 3.2 Reliability + +#### NFR-DN-003: Availability +- System uptime: 99.9% +- Graceful degradation (fallback to DB-only) +- Auto-recovery from Redis failure + +#### NFR-DN-004: Data Integrity +- Zero duplicate numbers (100% guarantee) +- ACID transactions +- Backup & restore procedures + +### 3.3 Security + +#### NFR-DN-005: Access Control +- Admin only: Format configuration, sequence adjustment +- Manager+: Manual override, void document +- User: Auto-generate only +- Audit all operations + +#### NFR-DN-006: Data Protection +- Encrypt sensitive data (audit logs) +- Secure Redis connections (TLS) +- Rate limiting (100 req/min per user) + +### 3.4 Scalability + +#### NFR-DN-007: Capacity Planning +- Support 10,000 documents/day +- Store 10M+ historical numbers +- Archive old audit logs (>2 years) + +### 3.5 Maintainability + +#### NFR-DN-008: Code Quality +- Unit test coverage: >70% +- Integration test coverage: >50% +- E2E test coverage: >20 critical paths +- Documentation: Complete API docs + +--- + +## 4. Business Rules + +### BR-DN-001: Sequence Scope Rules +- Correspondence: Project-level + Yearly reset +- RFA: Contract-level + Yearly reset +- Drawings: Contract-level + No reset +- Transmittal: Project-level + Monthly reset + +### BR-DN-002: Number Format Rules +- Min length: 10 characters +- Max length: 50 characters +- Must include {SEQ} token +- Must be ASCII only (no Thai/Chinese) + +### BR-DN-003: Manual Override Rules +- Only for document_types with allow_manual_override=true +- Must validate format +- Must check duplicate +- Requires Admin permission + +### BR-DN-004: Void Rules +- Can only void ACTIVE documents +- Cannot void already-VOID documents +- Must provide reason (min 10 chars) +- Replacement is optional + +--- + +## 5. Data Model + +### 5.1 Numbering Configuration +```sql +CREATE TABLE document_numbering_configs ( + id INT PRIMARY KEY AUTO_INCREMENT, + document_type VARCHAR(50) NOT NULL, + format VARCHAR(200) NOT NULL, + scope ENUM('GLOBAL','PROJECT','CONTRACT','YEARLY','MONTHLY'), + allow_manual_override BOOLEAN DEFAULT FALSE, + max_value INT DEFAULT 999999, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY (document_type, scope) +); +``` + +### 5.2 Sequence Counter +```sql +CREATE TABLE document_numbering_sequences ( + id INT PRIMARY KEY AUTO_INCREMENT, + config_id INT NOT NULL, + scope_value VARCHAR(50), -- project_id, contract_id, year, etc. + current_value INT DEFAULT 0, + last_used_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (config_id) REFERENCES document_numbering_configs(id), + UNIQUE KEY (config_id, scope_value) +); +``` + +### 5.3 Audit Log +```sql +CREATE TABLE document_numbering_audit_logs ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + operation VARCHAR(50) NOT NULL, + document_type VARCHAR(50), + document_number VARCHAR(50), + old_value TEXT, + new_value TEXT, + user_id INT NOT NULL, + ip_address VARCHAR(45), + user_agent VARCHAR(500), + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + metadata JSON, + INDEX idx_document_number (document_number), + INDEX idx_user_id (user_id), + INDEX idx_timestamp (timestamp) +); +``` + +--- + +## 6. API Specifications + +### 6.1 Reserve Number +```http +POST /api/document-numbering/reserve +Content-Type: application/json + +{ + "document_type": "COR", + "project_id": 1, + "contract_id": null, + "metadata": {} +} + +Response 201: +{ + "token": "uuid-v4", + "document_number": "COR-2025-00042", + "expires_at": "2025-01-16T10:30:00Z" +} +``` + +### 6.2 Confirm Reservation +```http +POST /api/document-numbering/confirm +Content-Type: application/json + +{ + "token": "uuid-v4" +} + +Response 200: +{ + "document_number": "COR-2025-00042", + "confirmed_at": "2025-01-16T10:25:00Z" +} +``` + +### 6.3 Manual Override +```http +POST /api/document-numbering/manual +Content-Type: application/json +Authorization: Bearer + +{ + "document_type": "COR", + "document_number": "COR-2024-99999", + "reason": "Import from legacy system", + "skip_validation": false +} + +Response 201: +{ + "document_number": "COR-2024-99999", + "is_manual": true, + "created_at": "2025-01-16T10:25:00Z" +} +``` + +--- + +## 7. Testing Requirements + +### 7.1 Unit Tests +- Format parsing and validation +- Sequence increment logic +- Manual override validation +- Scope resolution + +### 7.2 Integration Tests +- Redis locking mechanism +- Database transactions +- Two-phase commit flow +- Bulk import + +### 7.3 Load Tests +- Concurrent number generation (1000 req/s) +- Lock contention under load +- Redis failover scenarios +- Database connection pool exhaustion + +### 7.4 E2E Tests +- Complete document creation flow +- Void and replace workflow +- Bulk import with validation +- Admin configuration UI + +--- + +## 8. Migration Plan + +### 8.1 Legacy Data Import +1. Export existing document numbers from old system +2. Validate format and detect duplicates +3. Bulk import using manual override API +4. Update sequence counters to max values +5. Verify data integrity + +### 8.2 Rollout Strategy +- Week 1-2: Deploy to staging, test with dummy data +- Week 3: Deploy to production, enable for test project +- Week 4: Enable for all projects +- Week 5+: Monitor and optimize + +--- + +## 9. Success Criteria + +### 9.1 Functional Success +- ✅ All FRs implemented and tested +- ✅ Zero duplicate numbers in production +- ✅ Migration of 50,000+ legacy documents +- ✅ UAT approved by stakeholders + +### 9.2 Performance Success +- ✅ Response time <100ms (p95) +- ✅ Throughput >500 req/s +- ✅ Lock acquisition <50ms (avg) +- ✅ Zero downtime during deployment + +### 9.3 Business Success +- ✅ Document creation speed +30% +- ✅ Manual numbering errors -80% +- ✅ User satisfaction >4.5/5 +- ✅ System stability >99.9% + +--- + +## 10. Appendix + +### 10.1 Glossary +- **Sequence**: ลำดับตัวเลขที่เพิ่มขึ้นอัตโนมัติ +- **Scope**: ขอบเขตที่ sequence แยกตาม (project, contract, etc.) +- **Token**: Format placeholder (e.g., {YYYY}, {SEQ}) +- **Redlock**: Distributed locking algorithm สำหรับ Redis + +### 10.2 References +- [ADR-018: Document Numbering Strategy](../05-decisions/adr-018-document-numbering.md) +- [Two-Phase Commit Pattern](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) +- [Redlock Algorithm](https://redis.io/docs/manual/patterns/distributed-locks/) + +--- + +**Approval Sign-off**: + +| Role | Name | Date | Signature | +|------|------|------|-----------| +| Product Owner | ___________ | _______ | _________ | +| Tech Lead | ___________ | _______ | _________ | +| QA Lead | ___________ | _______ | _________ | diff --git a/docs/backup/03.11-document-numbering.md b/docs/backup/03.11-document-numbering.md new file mode 100644 index 0000000..040060b --- /dev/null +++ b/docs/backup/03.11-document-numbering.md @@ -0,0 +1,1871 @@ +# 3.11 Document Numbering Management (การจัดการเลขที่เอกสาร) + +--- +title: 'Functional Requirements: Document Numbering Management' +version: 1.6.0 +status: draft +owner: Nattanin Peancharoen +last_updated: 2025-12-02 +related: + +- specs/01-requirements/01-objectives.md +- specs/01-requirements/02-architecture.md +- specs/01-requirements/03-functional-requirements.md +- specs/03-implementation/document-numbering.md +- specs/04-operations/document-numbering-operations.md +- specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md + +--- + +> **📖 เอกสารที่เกี่ยวข้อง** +> +> - **Implementation Guide**: [document-numbering.md](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) - รายละเอียดการ implement ด้วย NestJS, TypeORM, Redis +> - **Operations Guide**: [document-numbering-operations.md](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) - Monitoring, Troubleshooting, Maintenance Procedures + +## 3.11.1. วัตถุประสงค์ + +- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติ, ที่มีความเป็นเอกลักษณ์ (unique) และยืดหยุ่นสูง +- ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร +- ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์ +- ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย + +## 3.11.2. Logic การนับเลข (Counter Logic) + +การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร +**Scopes**: +1. **Global**: Sequence ระดับระบบทั้งหมด +2. **Organization**: Sequence แยกตามองค์กรผู้ส่ง +3. **Project**: Sequence แยกตามโครงการ +4. **Contract**: Sequence แยกตามสัญญา +5. **Yearly**: Sequence reset ทุกปี + +### Counter Key Components + +| Component | Required? | Description | Database Source | Default if NULL | +| ---------------------------- | ---------------- | ------------------- | --------------------------------------------------------- | --------------- | +| `project_id` | ✅ Yes | ID โครงการ | Derived from user context or organization | - | +| `originator_organization_id` | ✅ Yes | ID องค์กรผู้ส่ง | `correspondences.originator_id` | - | +| `recipient_organization_id` | Depends on type | ID องค์กรผู้รับหลัก (TO) | `correspondence_recipients` where `recipient_type = 'TO'` | NULL for RFA | +| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | 0 | +| `sub_type_id` | TRANSMITTAL only | ID ประเภทย่อย | `correspondence_sub_types.id` | 0 | +| `rfa_type_id` | RFA only | ID ประเภท RFA | `rfa_types.id` | 0 | +| `discipline_id` | RFA only | ID สาขางาน | `disciplines.id` | 0 | +| `current_year` | ✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - | + +### Counter Key แยกตามประเภทเอกสาร (correspondence_type_id) + +#### **RFA / RFI / TRANSMITTAL / EMAIL / INSTRUCTION / LETTER / MEMO / MOM / NOTICE / OTHER**: + +``` +(project_id, originator_organization_id, recipient_organization_id, 0, 0, 0, 0, current_year) +``` + +**หมายเหตุ**: + - ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id`ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้ + - ถ้ามีการเพิ่ม - correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ + +#### **TRANSMITTAL**: + +``` +(project_id, originator_organization_id, recipient_organization_id, + correspondence_type_id, sub_type_id, 0, 0, current_year) +``` + +*หมายเหตุ*: ใช้ `sub_type_id` เพิ่มเติม + +#### **RFA**: + +``` +(project_id, originator_organization_id, NULL, + correspondence_type_id, 0, rfa_type_id, discipline_id, current_year) +``` + +*หมายเหตุ*: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER) + +##### วิธีการหา project_id + +เนื่องจาก Template ของ LETTER/TRANSMITTAL ไม่มี `{PROJECT}` token ระบบจะหา `project_id` จาก: + +1. **User Context** (แนะนำ): + - เมื่อ User สร้างเอกสาร UI จะให้เลือก Project/Contract ก่อน + - ใช้ `project_id` จาก Context ที่เลือก + +2. **จาก Organization**: + - Query `project_organizations` หรือ `contract_organizations` + - ใช้ `originator_organization_id` หา project ที่เกี่ยวข้อง + - ถ้ามีหลาย project ให้ User เลือก + +3. **Validation**: + - ตรวจสอบว่า organization มีสิทธิ์ใน project นั้น + - ตรวจสอบว่า project/contract เป็น active + +##### Fallback สำหรับค่า NULL + +- `correspondence_type_id`: ใช้ `0` (ไม่ระบุประเภทเอกสาร) +- `discipline_id`: ใช้ `0` (ไม่ระบุสาขางาน) +- `sub_type_id`: ใช้ `0` (ไม่มีประเภทย่อย) +- `rfa_type_id`: ใช้ `0` (ไม่ระบุประเภท RFA) +- `recipient_organization_id`: ใช้ `NULL` สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL + +## 3.11.3. Format Templates by Correspondence Type + +> **📝 หมายเหตุสำคัญ** +> - Templates ด้านล่างเป็น **ตัวอย่าง** สำหรับประเภทเอกสารหลัก +> - ระบบรองรับ **ทุกประเภทเอกสาร** ที่อยู่ใน `correspondence_types` table +> - หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ +> - Admin สามารถกำหนด Template เฉพาะสำหรับแต่ละประเภทผ่าน Admin Panel + +### 3.11.3.1. Global (correspondence_type_id = NULL) + +**Template**: + +``` +{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.} +``` + +**Example**: `คคง.-สคฉ.3-0001-2568` + +**Token Breakdown**: + +- `คคง.` = {ORIGINATOR} = รหัสองค์กรผู้ส่ง +- `สคฉ.3` = {RECIPIENT} = รหัสองค์กรผู้รับหลัก (TO) +- `0001` = {SEQ:4} = Running number (เริ่ม 0001, 0002, ...) +- `2568` = {YEAR:B.E.} = ปี พ.ศ. + +> **⚠️ Template vs Counter Separation** +> +> - {CORR_TYPE} **ไม่แสดง**ใน template เพื่อความกระชับ +> - แต่ระบบ**ยังใช้ correspondence_type_id ใน Counter Key** เพื่อแยก counter +> - LETTER, MEMO, RFI **มี counter แยกกัน** แม้ template format เหมือนกัน + +**Counter Key**: `(project_id, originator_org_id, recipient_org_id, correspondence_type_id, 0, 0, 0, year)` + +--- + +### 3.11.3.2. Transmittal (TYPE = TRANSMITTAL) + +**Template**: + +``` +{ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.} +``` + +**Example**: `คคง.-สคฉ.3-21-0117-2568` + +**Token Breakdown**: + +- `คคง.` = {ORIGINATOR} +- `สคฉ.3` = {RECIPIENT} +- `21` = {SUB_TYPE} = หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 14=MET, ...) +- `0117` = {SEQ:4} +- `2568` = {YEAR:B.E.} + +> **⚠️ Template vs Counter Separation** +> +> - {CORR_TYPE} **ไม่แสดง**ใน template (เหมือน LETTER) +> - TRANSMITTAL มี counter แยกจาก LETTER + +**Counter Key**: `(project_id, originator_org_id, recipient_org_id, correspondence_type_id, sub_type_id, 0, 0, year)` + +--- + +### 3.11.3.3. RFA (Request for Approval) + +**Template**: + +``` +{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV} +``` + +**Example**: `LCBP3-C2-RFA-TER-RPT-0001-A` + +**Token Breakdown**: + +- `LCBP3-C2` = {PROJECT} = รหัสโครงการ +- `RFA` = {CORR_TYPE} = ประเภทเอกสาร (**แสดง**ใน RFA template) +- `TER` = {DISCIPLINE} = รหัสสาขางาน (TER=Terminal, STR=Structure, ...) +- `RPT` = {RFA_TYPE} = ประเภท RFA (RPT=Report, SDW=Shop Drawing, ...) +- `0001` = {SEQ:4} +- `A` = {REV} = Revision code + +> **📋 RFA Workflow** +> +> - RFA เป็น **เอกสารโครงการ** (Project-level document) +> - Workflow: **CONTRACTOR → CONSULTANT → OWNER** +> - ไม่มี specific `recipient_id` เพราะเป็น workflow ที่กำหนดไว้แล้ว + +**Counter Key**: `(project_id, originator_org_id, NULL, correspondence_type_id, 0, rfa_type_id, discipline_id, year)` + +--- + +### 3.11.3.4. Drawing + +**Status**: 🚧 **To Be Determined** + +Drawing Numbering ยังไม่ได้กำหนด Template เนื่องจาก: + +- มีความซับซ้อนสูง (Contract Drawing และ Shop Drawing มีกฎต่างกัน) +- อาจต้องใช้ระบบ Numbering แยกต่างหาก +- ต้องพิจารณาร่วมกับ RFA ที่เกี่ยวข้อง + +--- + +## 3.11.4. Supported Token Types + +| Token | Description | Example | Database Source | +| -------------- | ---------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | +| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` | +| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` | +| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` where `recipient_type = 'TO'` | +| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` | +| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` | +| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` | +| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` | +| `{SEQ:n}` | Running number (n = จำนวนหลัก) | `0001`, `0029`, `0985` | Based on `document_number_counters.last_number + 1` | +| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `document_number_counters.current_year + 543` | +| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `document_number_counters.current_year` | +| `{REV}` | Revision Code | `A`, `B`, `AA` | `correspondence_revisions.revision_label` | + +### Token Usage Notes + +**{SEQ:n}**: + +- `n` = จำนวนหลักที่ต้องการ (typically 4) +- Counter **เริ่มจาก 0001** และเพิ่มทีละ 1 (0001, 0002, 0003, ...) +- Padding ด้วย 0 ทางซ้าย +- Reset ทุกปี (ตาม `current_year` ใน Counter Key) + +**{RECIPIENT}**: + +- ใช้เฉพาะผู้รับที่มี `recipient_type = 'TO'` เท่านั้น +- ถ้ามีหลาย TO ให้ใช้คนแรก (ตาม sort order) +- **ไม่ใช้สำหรับ RFA** (RFA ไม่มี {RECIPIENT} ใน template) + +**{CORR_TYPE}**: + +- รองรับทุกค่าจาก `correspondence_types.type_code` +- ถ้าม�การเพิ่มประเภทใหม่ จะใช้งานได้ทันที +- **แสดงใน template**: RFA only +- **ไม่แสดงแต่ใช้ใน counter**: LETTER, TRANSMITTAL, และ Other types + +**Deprecated Tokens** (ไม่ควรใช้): + +- ~~`{ORG}`~~ → ใช้ `{ORIGINATOR}` หรือ `{RECIPIENT}` แทน +- ~~`{TYPE}`~~ → ใช้ `{CORR_TYPE}`, `{SUB_TYPE}`, หรือ `{RFA_TYPE}` แทน (ตามบริบท) +- ~~`{CATEGORY}`~~ → ไม่ได้ใช้งานในระบบปัจจุบัน + +## 3.11.5. Security & Data Integrity Requirements + +### 3.11.5.1. Concurrency Control + +**Requirements:** + +- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน +- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์ +- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary +- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน + +**Implementation Details:** ดู [Implementation Guide - Section 2.3](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#23-redis-lock-service) + +### 3.11.5.2. Data Integrity + +**Requirements:** + +- ระบบ**ต้อง**ใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates +- ระบบ**ต้อง**มี database constraints เพื่อป้องกันข้อมูลผิดพลาด: + - Unique constraint บน `document_number` + - Foreign key constraints ทุก relationship + - Check constraints สำหรับ business rules + +### 3.11.5.3. Authorization + +**Requirements:** + +- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number +- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ +- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ (requires approval) + +## 3.11.6. Error Handling Requirements + +### 3.11.6.1. Retry Mechanism + +**Requirements:** + +ระบบ**ต้อง**จัดการ error scenarios ต่อไปนี้: + +| Scenario | Strategy | Max Retries | Expected Response | +| ------------------- | ------------------- | ----------- | ------------------------------- | +| Redis Unavailable | Fallback to DB Lock | 0 | Continue (degraded performance) | +| Lock Timeout | Exponential Backoff | 5 | HTTP 503 after final retry | +| Version Conflict | Immediate Retry | 2 | HTTP 409 after final retry | +| DB Connection Error | Exponential Backoff | 3 | HTTP 500 after final retry | + +**Implementation Details:** ดู [Implementation Guide - Section 2.5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#25-main-service-with-retry-logic) + +### 3.11.6.2. User Experience + +**Requirements:** + +- Error messages **ต้อง**เป็นภาษาไทย และเข้าใจง่าย +- HTTP status codes **ต้อง**สื่อความหมายที่ถูกต้อง +- Frontend **ควร**แสดง retry option สำหรับ transient errors + +## 3.11.7. Configuration Management Requirements + +### 3.11.7.1. Template Management + +**Requirements:** + +- Project Admin **ต้อง**สามารถกำหนด/แก้ไข template ผ่าน Admin Panel +- ระบบ**ต้อง**validate template ก่อนบันทึก +- การเปลี่ยนแปลง template **ต้องไม่**ส่งผลต่อเอกสารที่สร้างไว้แล้ว + +### 3.11.7.2. Template Versioning + +**Requirements:** + +- ระบบ**ต้อง**เก็บ history ของ template changes +- ระบบ**ต้อง**บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง +- ระบบ**ควร**สามารถ rollback ไปเวอร์ชันก่อนหน้าได้ + +### 3.11.7.3. Counter Reset Policy + +**Requirements:** + +- Counter **ต้อง**reset ตามปี (อัตโนมัติ) +- Admin **ต้อง**สามารถ manual reset counter ได้ (require approval + audit log) + +**Implementation Details:** ดู [Implementation Guide - Section 4](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#4-bullmq-job-for-counter-reset) + +### 3.11.7.4. Manual Override + +**Requirements:** +ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override) + +**Use Cases**: +- 1. Import เอกสารเก่าจากระบบเดิม +- 2. External documents จาก client/consultant +- 3. Correction หลังพบความผิดพลาด + +**Implementation Details:** +- ตรวจสอบ duplicate ก่อน save +- Validate format ตามรูปแบบที่กำหนด +- Auto-update sequence counter ถ้าเลขที่สูงกว่า current +- บันทึก audit log ว่าเป็น manual override +- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น + +### 3.11.7.5. Auto-Override + +**Requirements:** +ระบบต้องรองรับการกำหนดเลขที่เอกสารอัตโนมัติ (auto override) + +**Use Cases**: +- 1. Import เอกสารเก่าจากระบบเดิม +- 2. External documents จาก client/consultant +- 3. Correction หลังพบความผิดพลาด + +**Implementation Details:** +- ตรวจสอบ duplicate ก่อน save +- Validate format ตามรูปแบบที่กำหนด +- Auto-update sequence counter ถ้าเลขที่สูงกว่า current +- บันทึก audit log ว่าเป็น auto override +- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น + +### +## 3.11.9 Audit Trail Requirements + +### 3.11.9.1. Audit Logging + +**Requirements:** + +ระบบ**ต้อง**บันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation: + +- `document_id` - เอกสารที่ถูกสร้าง +- `generated_number` - เลขที่ถูกสร้าง +- `counter_key` - key ที่ใช้ในการนับ (JSON format) +- `template_used` - template ที่ใช้ +- `user_id` - ผู้ที่ request +- `ip_address` - IP address ของผู้ request +- `timestamp` - เวลาที่สร้าง +- `retry_count` - จำนวนครั้งที่ retry +- `performance_metrics` - Lock wait time, total duration + +### 3.11.9.2. Error Logging + +**Requirements:** + +- ระบบ**ต้อง**บันทึก error แยกต่างหาก พร้อม error type classification +- ระบบ**ควร**alert ops team สำหรับ critical errors + +### 3.11.9.3. Retention Policy + +**Requirements:** + +- Audit log **ต้อง**เก็บอย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) + +## 3.11.10 Performance Requirements + +### 3.11.10.1. Response Time + +**SLA Targets:** + +| Metric | Target | Notes | +| ---------------- | -------- | ------------------------ | +| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response | +| 99th percentile | ≤ 5 วินาที | รวม retry attempts | +| Normal operation | ≤ 500ms | ไม่มี retry | + +### 3.11.10.2. Throughput + +**Capacity Targets:** + +| Load Level | Target | Notes | +| ----------- | ----------- | --------- | +| Normal load | ≥ 50 req/s | ใช้งานปกติ | +| Peak load | ≥ 100 req/s | ช่วงเร่งงาน | + +### 3.11.10.3. Availability + +**SLA Targets:** + +- **Uptime**: ≥ 99.5% (excluding planned maintenance) +- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน +- **RTO**: ≤ 30 นาที +- **RPO**: ≤ 5 นาที + +**Operations Details:** ดู [Operations Guide - Section 1](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#1-performance-requirements) + +## 3.11.11 Monitoring & Alerting Requirements + +### 3.11.11.1. Metrics + +**Requirements:** + +ระบบ**ต้อง**collect metrics ต่อไปนี้: + +- Lock acquisition time (p50, p95, p99) +- Lock acquisition success/failure rate +- Counter generation latency +- Retry count distribution +- Redis connection status +- Database connection pool usage + +### 3.11.11.2. Alerts + +**Requirements:** + +ระบบ**ต้อง**alert สำหรับ conditions ต่อไปนี้: + +| Severity | Condition | Action | +| ---------- | ---------------------------- | ----------------- | +| 🔴 Critical | Redis unavailable > 1 minute | PagerDuty + Slack | +| 🔴 Critical | Lock failures > 10% in 5 min | PagerDuty + Slack | +| 🟡 Warning | Lock failures > 5% in 5 min | Slack | +| 🟡 Warning | Avg lock wait time > 1 sec | Slack | +| 🟡 Warning | Retry count > 100/hour | Slack | + +### 3.11.11.3. Dashboard + +**Requirements:** + +- Ops team **ต้อง**มี real-time dashboard แสดง: + - Lock acquisition success rate + - Lock wait time percentiles + - Generation rate (per minute) + - Error rate by type + - Connection health status + +**Operations Details:** ดู [Operations Guide - Section 3](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#3-monitoring--metrics) + + +## 3.11.12 API Reference + +เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้: + +### Document Number Generation + +```http +POST /api/v1/documents/{documentId}/generate-number +``` + +สร้างเลขที่เอกสารสำหรับ document ที่ระบุ + +**Request Body:** + +```json +{ + "counterKey": { + "projectId": 2, + "originatorOrgId": 22, + "recipientOrgId": 10, + "correspondenceTypeId": 6, + "subTypeId": 0, + "rfaTypeId": 0, + "disciplineId": 0, + "year": 2025 + } +} +``` + +**Response:** + +```json +{ + "documentNumber": "คคง.-สคฉ.3-0001-2568", + "generatedAt": "2025-12-02T15:30:00Z" +} +``` + +### Template Management + +```http +GET /api/v1/document-numbering/configs +``` + +ดูรายการ template configuration ทั้งหมด + +```http +PUT /api/v1/document-numbering/configs/{configId} +``` + +แก้ไข template (Project Admin only) + +```http +POST /api/v1/document-numbering/configs/{configId}/reset-counter +``` + +Reset counter (Super Admin only, requires approval) + +**รายละเอียดเพิ่มเติม:** ดู [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md) + +## 3.11.13 Database Schema Reference + +เอกสารนี้อ้างอิงถึง database tables ต่อไปนี้: + +### Core Tables + +- `document_number_counters` - เก็บ counter values และ template configuration +- `document_number_audit` - เก็บ audit trail ของการ generate เลขที่ +- `document_number_errors` - เก็บ error logs + +### Related Tables + +- `documents` - เก็บ document number ที่ถูกสร้าง (column: `document_number` UNIQUE) +- `correspondence_types` - ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.) +- `correspondence_sub_types` - ประเภทย่อย (สำหรับ TRANSMITTAL) +- `rfa_types` - ประเภท RFA (SHD, RPT, MAT, etc.) +- `disciplines` - สาขาวิชา (TER, STR, GEO, etc.) +- `projects` - โครงการ +- `organizations` - องค์กร + +**Schema Details:** ดู [Implementation Guide - Section 1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#1-database-implementation) + +## 3.11.14 Database Schema Requirements + +### 3.11.14.1 Counter Table Schema Requirements + +**Primary Table**: `document_number_counters` + +**Required Columns:** +- Composite primary key: `(project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)` +- `version` - สำหรับ optimistic locking +- `last_number` - counter value (เริ่มจาก 0) + +**Important Notes:** +- ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL +- Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน) +- ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน + +**Schema Details:** ดู [Implementation Guide - Section 1.1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#11-counter-table-schema) + +### 3.11.14.2 Audit Table Requirements + +**Primary Table**: `document_number_audit` + +**Required Columns:** +- `document_id`, `generated_number`, `counter_key` (JSON) +- `template_used`, `user_id`, `ip_address` +- Performance metrics: `retry_count`, `lock_wait_ms`, `total_duration_ms` +- `fallback_used` - tracking fallback scenarios + +**Retention:** ≥ 7 ปี + +### 3.11.14.3 Error Log Requirements + +**Primary Table**: `document_number_errors` + +**Required Columns:** +- `error_type` - ENUM classification +- `error_message`, `stack_trace`, `context_data` (JSON) +- `user_id`, `ip_address`, `created_at`, `resolved_at` + +## 3.11.15 Security Considerations + +### 3.11.15.1 Authorization + +- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number +- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ +- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ + +### 3.11.15.2 Rate Limiting + +**Requirements:** +- Limit ต่อ user: **10 requests/minute** (prevent abuse) +- Limit ต่อ IP: **50 requests/minute** + +**Implementation Details:** ดู [Implementation Guide - Section 5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#5-api-controller) + +### 3.11.15.3 Audit & Compliance + +**Requirements:** +- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering +- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) +- Audit log **ต้องไม่**สามารถแก้ไขได้ (immutable) + +--- + +## 3.11.16 References + +- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) +- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) +- [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md) +- [Data Dictionary](file:///e:/np-dms/lcbp3/specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md) + + +``` +lock:docnum:{project_id}:{org_id}:{recip_id}:{type_id}:{sub}:{rfa}:{disc}:{year} +``` + +**Lock Configuration**: + +- **TTL**: 5 วินาที (auto-release เมื่อ timeout) +- **Acquisition Timeout**: 10 วินาที +- **Retry Delay**: 100ms (exponential backoff) +- **Drift Factor**: 0.01 (Redlock algorithm) + +**Implementation (NestJS)**: + +```typescript +// src/document-numbering/services/document-numbering-lock.service.ts +import Redlock from 'redlock'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class DocumentNumberingLockService { + private redlock: Redlock; + + async acquireLock(counterKey: CounterKey): Promise { + const lockKey = this.buildLockKey(counterKey); + return await this.redlock.acquire([lockKey], 5000); // 5s TTL + } + + private buildLockKey(key: CounterKey): string { + return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + + `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + + `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; + } +} +``` + +## 3.11.17 Optimistic Locking + +ใช้ **TypeORM Optimistic Lock** ร่วมกับ `@Version()` decorator: + +**Entity Definition**: + +```typescript +// src/document-numbering/entities/document-number-counter.entity.ts +import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm'; + +@Entity('document_number_counters') +export class DocumentNumberCounter { + @PrimaryColumn({ name: 'project_id' }) + projectId: number; + + @PrimaryColumn({ name: 'originator_organization_id' }) + originatorOrganizationId: number; + + @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) + recipientOrganizationId: number | null; + + @PrimaryColumn({ name: 'correspondence_type_id' }) + correspondenceTypeId: number; + + @PrimaryColumn({ name: 'sub_type_id', default: 0 }) + subTypeId: number; + + @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) + rfaTypeId: number; + + @PrimaryColumn({ name: 'discipline_id', default: 0 }) + disciplineId: number; + + @PrimaryColumn({ name: 'current_year' }) + currentYear: number; + + @VersionColumn({ name: 'version' }) + version: number; + + @Column({ name: 'last_number', default: 0 }) + lastNumber: number; +} +``` + +**Transaction Handling**: + +```typescript +// ใช้ TypeORM Transaction + Optimistic Lock +await this.connection.transaction(async (manager) => { + const counter = await manager.findOne(DocumentNumberCounter, { + where: counterKey + }); + + counter.lastNumber += 1; + await manager.save(counter); // auto-check version +}); +``` + +หาก version conflict → TypeORM throw `OptimisticLockVersionMismatchError` → retry + +## 3.11.18 Database Constraints + +**Unique Constraints**: + +```sql +-- บน documents table +ALTER TABLE documents +ADD CONSTRAINT uq_document_number UNIQUE (document_number); +``` + +**Foreign Key Constraints**: + +- `project_id` → `projects(id)` ON DELETE CASCADE +- `originator_organization_id` → `organizations(id)` ON DELETE CASCADE +- `recipient_organization_id` → `organizations(id)` ON DELETE CASCADE +- `correspondence_type_id` → `correspondence_types(id)` ON DELETE CASCADE + +**Check Constraints**: + +```sql +-- ตรวจสอบว่า last_number ≥ 0 +ALTER TABLE document_number_counters +ADD CONSTRAINT chk_last_number_positive CHECK (last_number >= 0); + +-- ตรวจสอบว่า current_year เป็นปี ค.ศ. ที่สมเหตุสมผล +ALTER TABLE document_number_counters +ADD CONSTRAINT chk_current_year_valid +CHECK (current_year BETWEEN 2020 AND 2100); +``` + +## 3.11.19 Retry Mechanism & Error Handling + +### 3.11.19.1 Scenario 1: Redis Unavailable + +**Fallback Strategy**: Database-only Pessimistic Locking + +**Implementation**: + +```typescript +// src/document-numbering/services/document-numbering.service.ts +@Injectable() +export class DocumentNumberingService { + async generateDocumentNumber(dto: GenerateNumberDto): Promise { + try { + // พยายามใช้ Redis lock ก่อน + return await this.generateWithRedisLock(dto); + } catch (error) { + if (error instanceof RedisConnectionError) { + // Fallback: ใช้ database lock + this.logger.warn('Redis unavailable, falling back to DB lock'); + await this.alertOpsTeam('redis_unavailable'); + return await this.generateWithDbLock(dto); + } + throw error; + } + } + + private async generateWithDbLock(dto: GenerateNumberDto): Promise { + return await this.connection.transaction(async (manager) => { + // SELECT ... FOR UPDATE = Pessimistic Lock + const counter = await manager + .createQueryBuilder(DocumentNumberCounter, 'c') + .where(counterKeyCondition) + .setLock('pessimistic_write') + .getOne(); + + counter.lastNumber += 1; + await manager.save(counter); + return this.formatNumber(counter); + }); + } +} +``` + +**Monitoring**: + +- Log warning พร้อม context (project_id, user_id, timestamp) +- Alert Ops Team ผ่าน Slack/Email +- ระบบยังใช้งานได้แต่ performance อาจลดลง 30-50% + +### 3.11.19.2 Scenario 2: Lock Acquisition Timeout + +**Retry Strategy**: Exponential Backoff with Jitter + +```typescript +// ใช้ @nestjs/common Retry Decorator หรือ custom retry logic +import { retry } from 'rxjs/operators'; + +const RETRY_CONFIG = { + maxRetries: 5, + delays: [1000, 2000, 4000, 8000, 16000], // exponential backoff + jitter: 0.1 // เพิ่ม randomness ป้องกัน thundering herd +}; + +async acquireLockWithRetry(key: CounterKey): Promise { + for (let i = 0; i < RETRY_CONFIG.maxRetries; i++) { + try { + return await this.lockService.acquireLock(key); + } catch (error) { + if (i === RETRY_CONFIG.maxRetries - 1) { + throw new ServiceUnavailableException( + 'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง' + ); + } + const delay = RETRY_CONFIG.delays[i]; + const jitter = delay * RETRY_CONFIG.jitter * Math.random(); + await this.sleep(delay + jitter); + } + } +} +``` + +**Response**: + +- HTTP Status: `503 Service Temporarily Unavailable` +- Response Body: + + ```json + { + "statusCode": 503, + "message": "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง", + "error": "Service Unavailable", + "retryAfter": 30 + } + ``` + +### 3.11.19.3 Scenario 3: Version Conflict (Optimistic Lock) + +**Retry Strategy**: Immediate Retry (2 attempts) + +```typescript +async incrementCounter(counterKey: CounterKey): Promise { + const MAX_RETRIES = 2; + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + return await this.connection.transaction(async (manager) => { + const counter = await manager.findOne( + DocumentNumberCounter, + { where: counterKey } + ); + + counter.lastNumber += 1; + await manager.save(counter); // Version check ที่นี่ + return counter.lastNumber; + }); + } catch (error) { + if (error instanceof OptimisticLockVersionMismatchError) { + this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`); + if (attempt === MAX_RETRIES - 1) { + throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่'); + } + // Retry ทันที (ไม่มี delay) + continue; + } + throw error; + } + } +} +``` + +**Response**: + +- HTTP Status: `409 Conflict` +- Frontend Action: Auto-retry หรือแสดง toast notification + +### 3.11.19.4 Scenario 4: Database Connection Error + +**Retry Strategy**: Exponential Backoff (3 attempts) + +```typescript +const DB_RETRY_CONFIG = { + maxRetries: 3, + delays: [1000, 2000, 4000] +}; + +// TypeORM connection retry (กำหนดใน ormconfig) +{ + type: 'mysql', + extra: { + connectionLimit: 10, + acquireTimeout: 10000, + // Retry connection 3 ครั้ง + retryAttempts: 3, + retryDelay: 1000 + } +} +``` + +**Response**: + +- HTTP Status: `500 Internal Server Error` +- Response Body: + + ```json + { + "statusCode": 500, + "message": "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ", + "error": "Internal Server Error", + "ref": "ERR-20250102-1234-ABCD" + } + ``` + +- Alerting: ส่ง PagerDuty/Slack alert ทันที (severity: CRITICAL) + +## 3.11.20 Configuration Management + +### 3.11.20.1 Admin Panel Configuration + +**Features**: + +- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Web UI +- Preview document number ก่อนบันทึก +- Template validation แบบ real-time + +**Template Validation Logic**: + +```typescript +// src/document-numbering/validators/template.validator.ts +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TemplateValidator { + private readonly ALLOWED_TOKENS = [ + 'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE', + 'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV' + ]; + + validate(template: string, correspondenceType: string): ValidationResult { + const tokens = this.extractTokens(template); + const errors: string[] = []; + + // ตรวจสอบ Token ที่ไม่รู้จัก + for (const token of tokens) { + if (!this.ALLOWED_TOKENS.includes(token.name)) { + errors.push(`Unknown token: {${token.name}}`); + } + } + + // กฎพิเศษสำหรับแต่ละประเภท + if (correspondenceType === 'RFA') { + if (!tokens.some(t => t.name === 'PROJECT')) { + errors.push('RFA template ต้องมี {PROJECT}'); + } + } + + if (correspondenceType === 'TRANSMITTAL') { + if (!tokens.some(t => t.name === 'SUB_TYPE')) { + errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}'); + } + } + + return { valid: errors.length === 0, errors }; + } +} +``` + +**API Endpoint**: + +```typescript +// PUT /api/v1/document-numbering/configs/:configId +@Put('configs/:configId') +@Roles('PROJECT_ADMIN') +async updateTemplate( + @Param('configId') configId: number, + @Body() dto: UpdateTemplateDto +): Promise { + // Validate template + const validation = await this.templateValidator.validate( + dto.template, + dto.correspondenceType + ); + + if (!validation.valid) { + throw new BadRequestException(validation.errors); + } + + // บันทึก template (ไม่ส่งผลต่อเอกสารที่สร้างแล้ว) + return await this.configService.update(configId, dto); +} +``` + +### 3.11.20.2 Template Versioning + +**Database Table**: `document_number_config_history` + +```sql +CREATE TABLE document_number_config_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + config_id INT NOT NULL, + template_before TEXT, + template_after TEXT NOT NULL, + changed_by INT NOT NULL, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + change_reason TEXT, + + FOREIGN KEY (config_id) REFERENCES document_number_configs(id), + FOREIGN KEY (changed_by) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Template Change History'; +``` + +**Audit Trail Implementation**: + +```typescript +@Injectable() +export class ConfigHistoryService { + async recordChange( + configId: number, + oldTemplate: string, + newTemplate: string, + userId: number, + reason: string + ): Promise { + await this.historyRepo.save({ + configId, + templateBefore: oldTemplate, + templateAfter: newTemplate, + changedBy: userId, + changeReason: reason + }); + } + + async rollback(configId: number, historyId: number): Promise { + const history = await this.historyRepo.findOne({ where: { id: historyId }}); + await this.configService.update(configId, { + template: history.templateBefore + }); + } +} +``` + +### 3.11.20.3 Counter Reset Policy + +**Automatic Reset**: + +- **Yearly Reset**: ทุกวันที่ 1 มกราคม (00:00:00 ICT) + - ใช้ **BullMQ Cron Job**: + + ```typescript + // src/document-numbering/jobs/counter-reset.job.ts + @Processor('document-numbering') + export class CounterResetJob { + @Cron('0 0 1 1 *') // 1 Jan every year + async handleYearlyReset() { + const newYear = new Date().getFullYear(); + + // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว + // แค่เตรียม counter สำหรับปีใหม่ + this.logger.log(`Year changed to ${newYear}, counters ready`); + } + } + ``` + +**Manual Reset** (Admin only): + +```typescript +// POST /api/v1/document-numbering/configs/:configId/reset-counter +@Post('configs/:configId/reset-counter') +@Roles('SUPER_ADMIN') +@RequireApproval() // Custom decorator: ต้อง approve จาก 2 admins +async resetCounter( + @Param('configId') configId: number, + @Body() dto: ResetCounterDto +): Promise { + // Validate reason + if (!dto.reason || dto.reason.length < 20) { + throw new BadRequestException('ต้องระบุเหตุผลอย่างน้อย 20 ตัวอักษร'); + } + + // Audit log + await this.auditService.logCounterReset({ + configId, + userId: req.user.id, + reason: dto.reason, + previousValue: counter.lastNumber + }); + + // Reset + await this.counterService.reset(configId); +} +``` +## 3.11.21 Audit Trail + +### 3.11.21.1 การบันทึก Audit Log + +**Database Table**: `document_number_audit` + +```sql +CREATE TABLE document_number_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + document_id INT NOT NULL, + generated_number VARCHAR(100) NOT NULL, + counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)', + template_used VARCHAR(200) NOT NULL, + user_id INT NOT NULL, + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Performance & Error Tracking + retry_count INT DEFAULT 0, + lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds', + total_duration_ms INT COMMENT 'Total generation time', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE', + + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail'; +``` + +**Audit Service Implementation**: + +```typescript +// src/document-numbering/services/audit.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +@Injectable() +export class DocumentNumberAuditService { + async logGeneration(data: AuditLogData): Promise { + await this.auditRepo.save({ + documentId: data.documentId, + generatedNumber: data.number, + counterKey: JSON.stringify(data.counterKey), + templateUsed: data.template, + userId: data.userId, + ipAddress: data.ipAddress, + userAgent: data.userAgent, + retryCount: data.retryCount ?? 0, + lockWaitMs: data.lockWaitMs, + totalDurationMs: data.totalDurationMs, + fallbackUsed: data.fallbackUsed ?? 'NONE' + }); + } +} +``` + +**Usage in Service**: + +```typescript +@Injectable() +export class DocumentNumberingService { + async generateDocumentNumber(dto: GenerateNumberDto, req: Request) { + const startTime = Date.now(); + let lockWaitMs = 0; + let retryCount = 0; + let fallbackUsed = 'NONE'; + + try { + // ... generate logic ... + const number = await this.doGenerate(dto); + + // Audit log + await this.auditService.logGeneration({ + documentId: dto.documentId, + number, + counterKey: dto.counterKey, + template: config.template, + userId: req.user.id, + ipAddress: req.ip, + userAgent: req.headers['user-agent'], + retryCount, + lockWaitMs, + totalDurationMs: Date.now() - startTime, + fallbackUsed + }); + + return number; + } catch (error) { + // Log error separately + await this.errorLogService.log(error, dto); + throw error; + } + } +} +``` + +### 3.11.21.2 Conflict & Error Logging + +**Separate Error Log Table**: `document_number_errors` + +```sql +CREATE TABLE document_number_errors ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + error_type ENUM( + 'LOCK_TIMEOUT', + 'VERSION_CONFLICT', + 'DB_ERROR', + 'REDIS_ERROR', + 'VALIDATION_ERROR' + ) NOT NULL, + error_message TEXT, + stack_trace TEXT, + context_data JSON COMMENT 'Request context (user, project, etc.)', + user_id INT, + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB COMMENT='Document Numbering Error Log'; +``` + +**Error Logging Service**: + +```typescript +@Injectable() +export class ErrorLogService { + async log(error: Error, context: any): Promise { + const errorType = this.classifyError(error); + + await this.errorRepo.save({ + errorType, + errorMessage: error.message, + stackTrace: error.stack, + contextData: JSON.stringify(context), + userId: context.userId, + ipAddress: context.ipAddress + }); + + // Alert if critical + if (this.isCritical(errorType)) { + await this.alertService.sendAlert({ + severity: 'CRITICAL', + title: `Document Numbering Error: ${errorType}`, + details: error.message + }); + } + } + + private classifyError(error: Error): string { + if (error instanceof LockTimeoutError) return 'LOCK_TIMEOUT'; + if (error instanceof OptimisticLockVersionMismatchError) return 'VERSION_CONFLICT'; + if (error instanceof QueryFailedError) return 'DB_ERROR'; + if (error instanceof RedisConnectionError) return 'REDIS_ERROR'; + return 'UNKNOWN'; + } +} +``` +## 3.11.22 Performance Requirements + +### 3.11.22.1 Response Time + +**Target Response Times**: +- **95th percentile**: ≤ 2 วินาที +- **99th percentile**: ≤ 5 วินาที +- **Normal operation** (ไม่มี retry): ≤ 500ms + +**Performance Optimization Strategies**: + +```typescript +// 1. Database Connection Pooling +{ + type: 'mysql', + extra: { + connectionLimit: 20, // Pool size + queueLimit: 0, // Unlimited queue + acquireTimeout: 10000 // 10s timeout + } +} + +// 2. Redis Connection Pooling +import IORedis from 'ioredis'; + +const redis = new IORedis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT), + maxRetriesPerRequest: 3, + enableReadyCheck: true, + lazyConnect: false, + // Connection pool + poolSize: 10 +}); + +// 3. Query Optimization +// ใช้ Index-covered queries +const counter = await this.counterRepo + .createQueryBuilder('c') + .where('c.project_id = :projectId', { projectId }) + .andWhere('c.correspondence_type_id = :typeId', { typeId }) + .andWhere('c.current_year = :year', { year }) + .useIndex('idx_counter_lookup') // Force index usage + .getOne(); +``` + +**Performance Monitoring**: + +```typescript +// Prometheus metrics +import { Counter, Histogram } from 'prom-client'; + +const generationDuration = new Histogram({ + name: 'docnum_generation_duration_seconds', + help: 'Document number generation duration', + labelNames: ['project', 'type', 'status'], + buckets: [0.1, 0.5, 1, 2, 5, 10] +}); + +// Usage +const timer = generationDuration.startTimer(); +try { + const number = await this.generate(dto); + timer({ status: 'success' }); +} catch (error) { + timer({ status: 'error' }); + throw error; +} +``` + +### 3.11.22.2 Throughput + +**Capacity Requirements**: + +- **Normal load**: ≥ 50 requests/second +- **Peak load**: ≥ 100 requests/second (ช่วงเร่งงาน) +- **Burst capacity**: ≥ 200 requests/second (short duration) + +**Load Balancing Strategy**: + +```yaml +# docker-compose.yml +services: + backend: + image: lcbp3-backend:latest + deploy: + replicas: 3 # 3 instances + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + + nginx: + image: nginx:alpine + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + ports: + - "80:80" +``` + +```nginx +# nginx.conf - Load Balancing Configuration +upstream backend { + least_conn; # Least connections algorithm + server backend:3000 max_fails=3 fail_timeout=30s; + server backend:3001 max_fails=3 fail_timeout=30s; + server backend:3002 max_fails=3 fail_timeout=30s; +} + +server { + location /api/v1/documents/ { + proxy_pass http://backend; + proxy_next_upstream error timeout; + proxy_connect_timeout 10s; + proxy_read_timeout 30s; + } +} +``` + +**Rate Limiting**: + +```typescript +// ใช้ @nestjs/throttler +import { ThrottlerGuard } from '@nestjs/throttler'; + +@Controller('document-numbering') +@UseGuards(ThrottlerGuard) +export class DocumentNumberingController { + @Throttle(10, 60) // 10 requests per 60 seconds per user + @Post('generate') + async generate(@Body() dto: GenerateNumberDto) { + return await this.service.generate(dto); + } +} +``` + +### 3.11.22.3 Availability + +**SLA Targets**: + +- **Uptime**: ≥ 99.5% (excluding planned maintenance) +- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน +- **Recovery Time Objective (RTO)**: ≤ 30 นาที +- **Recovery Point Objective (RPO)**: ≤ 5 นาที + +**High Availability Setup**: + +```yaml +# High Availability Architecture +services: + # MariaDB - Master/Replica + mariadb-master: + image: mariadb:11.8 + environment: + MYSQL_REPLICATION_MODE: master + + mariadb-replica: + image: mariadb:11.8 + environment: + MYSQL_REPLICATION_MODE: slave + MYSQL_MASTER_HOST: mariadb-master + + # Redis - Sentinel Mode + redis-master: + image: redis:7-alpine + command: redis-server --appendonly yes + + redis-replica: + image: redis:7-alpine + command: redis-server --replicaof redis-master 6379 + + redis-sentinel: + image: redis:7-alpine + command: > + redis-sentinel /etc/redis/sentinel.conf + --sentinel monitor mymaster redis-master 6379 2 +``` + +**Health Checks**: + +```typescript +// src/health/health.controller.ts +import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus'; + +@Controller('health') +export class HealthController { + @Get() + @HealthCheck() + check() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => this.redis.pingCheck('redis'), + () => this.customHealthCheck() + ]); + } + + private async customHealthCheck() { + // ทดสอบ generate document number + const canGenerate = await this.testGeneration(); + return { documentNumbering: { status: canGenerate ? 'up' : 'down' }}; + } +} +``` +## 3.11.23 Monitoring & Alerting + +### 3.11.23.1 Metrics Collection + +**Prometheus Metrics Implementation**: + +```typescript +// src/document-numbering/metrics/metrics.service.ts +import { Injectable } from '@nestjs/common'; +import { Counter, Histogram, Gauge } from 'prom-client'; + +@Injectable() +export class DocumentNumberingMetrics { + // Lock acquisition metrics + private lockAcquisitionDuration = new Histogram({ + name: 'docnum_lock_acquisition_duration_ms', + help: 'Lock acquisition time in milliseconds', + labelNames: ['project', 'type'], + buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000] + }); + + private lockAcquisitionFailures = new Counter({ + name: 'docnum_lock_acquisition_failures_total', + help: 'Total number of lock acquisition failures', + labelNames: ['project', 'type', 'reason'] + }); + + // Generation metrics + private generationDuration = new Histogram({ + name: 'docnum_generation_duration_ms', + help: 'Total document number generation time', + labelNames: ['project', 'type', 'status'], + buckets: [100, 200, 500, 1000, 2000, 5000] + }); + + private retryCount = new Histogram({ + name: 'docnum_retry_count', + help: 'Number of retries per generation', + labelNames: ['project', 'type'], + buckets: [0, 1, 2, 3, 5, 10] + }); + + // Connection health + private redisConnectionStatus = new Gauge({ + name: 'docnum_redis_connection_status', + help: 'Redis connection status (1=up, 0=down)' + }); + + private dbConnectionPoolUsage = new Gauge({ + name: 'docnum_db_connection_pool_usage', + help: 'Database connection pool usage percentage' + }); +} +``` + +### 3.11.23.2 Alert Rules + +**Prometheus Alert Rules** (`prometheus/alerts.yml`): + +```yaml +groups: + - name: document_numbering_alerts + interval: 30s + rules: + # CRITICAL: Redis unavailable + - alert: RedisUnavailable + expr: docnum_redis_connection_status == 0 + for: 1m + labels: + severity: critical + component: document-numbering + annotations: + summary: "Redis is unavailable for document numbering" + description: "System is falling back to DB-only locking" + + # CRITICAL: High lock failure rate + - alert: HighLockFailureRate + expr: | + rate(docnum_lock_acquisition_failures_total[5m]) > 0.1 + for: 5m + labels: + severity: critical + annotations: + summary: "Lock acquisition failure rate > 10%" + description: "Check Redis and database performance" + + # WARNING: Elevated lock failure rate + - alert: ElevatedLockFailureRate + expr: | + rate(docnum_lock_acquisition_failures_total[5m]) > 0.05 + for: 5m + labels: + severity: warning + annotations: + summary: "Lock acquisition failure rate > 5%" + + # WARNING: Slow lock acquisition + - alert: SlowLockAcquisition + expr: | + histogram_quantile(0.95, + rate(docnum_lock_acquisition_duration_ms_bucket[5m]) + ) > 1000 + for: 5m + labels: + severity: warning + annotations: + summary: "P95 lock acquisition time > 1 second" + + # WARNING: High retry count + - alert: HighRetryCount + expr: | + sum by (project) ( + rate(docnum_retry_count_sum[1h]) + ) > 100 + for: 1h + labels: + severity: warning + annotations: + summary: "Retry count > 100 per hour in project {{ $labels.project }}" + + # WARNING: Slow generation + - alert: SlowDocumentNumberGeneration + expr: | + histogram_quantile(0.95, + rate(docnum_generation_duration_ms_bucket[5m]) + ) > 2000 + for: 5m + labels: + severity: warning + annotations: + summary: "P95 generation time > 2 seconds" +``` + +**AlertManager Configuration** (`alertmanager/config.yml`): + +```yaml +route: + group_by: ['alertname', 'severity'] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + receiver: 'ops-team' + routes: + # CRITICAL alerts → PagerDuty + Slack + - match: + severity: critical + receiver: 'pagerduty-critical' + continue: true + + # WARNING alerts → Slack only + - match: + severity: warning + receiver: 'slack-warnings' + +receivers: + - name: 'pagerduty-critical' + pagerduty_configs: + - service_key: + description: '{{ .CommonAnnotations.summary }}' + + - name: 'slack-warnings' + slack_configs: + - api_url: + channel: '#lcbp3-alerts' + title: '⚠️ {{ .GroupLabels.alertname }}' + text: '{{ .CommonAnnotations.description }}' + + - name: 'ops-team' + email_configs: + - to: 'ops@example.com' +``` + +### 3.11.23.3 Grafana Dashboard + +**Dashboard Configuration** (`grafana/dashboards/document-numbering.json`): + +```json +{ + "title": "Document Numbering Performance", + "panels": [ + { + "title": "Lock Acquisition Success Rate", + "targets": [{ + "expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))" + }], + "type": "graph", + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 } + }, + { + "title": "Lock Acquisition Time (Percentiles)", + "targets": [ + { + "expr": "histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", + "legendFormat": "P50" + }, + { + "expr": "histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", + "legendFormat": "P95" + }, + { + "expr": "histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", + "legendFormat": "P99" + } + ], + "type": "graph", + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 } + }, + { + "title": "Generation Rate (per minute)", + "targets": [{ + "expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60" + }], + "type": "stat", + "gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 } + }, + { + "title": "Redis Connection Status", + "targets": [{ + "expr": "docnum_redis_connection_status" + }], + "type": "stat", + "gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": 0, "color": "red" }, + { "value": 1, "color": "green" } + ] + } + }, + { + "title": "Error Rate by Type", + "targets": [{ + "expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))" + }], + "type": "graph", + "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 } + } + ] +} +``` + +**Key Dashboard Panels**: + +- **Lock Acquisition Success Rate**: Real-time success % +- **Lock Wait Time Percentiles**: P50, P95, P99 latency +- **Generation Rate**: Documents/minute +- **Error Breakdown**: By error type (LOCK_TIMEOUT, VERSION_CONFLICT, etc.) +- **Redis/DB Health**: Connection status +- **Retry Distribution**: Histogram of retry counts + +## 3.11.24 API Reference + +เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้ (รายละเอียดใน `specs/02-architecture/api-design.md`): + +- `POST /api/v1/documents/{documentId}/generate-number` - สร้างเลขที่เอกสาร +- `GET /api/v1/document-numbering/configs` - ดูการตั้งค่า template +- `PUT /api/v1/document-numbering/configs/{configId}` - แก้ไข template (Admin only) +- `POST /api/v1/document-numbering/configs/{configId}/reset-counter` - Reset counter (Admin only) + +## 3.11.25 Database Schema Reference + +เอกสารนี้อ้างอิงถึง tables ต่อไปนี้ (รายละเอียดใน `specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md`): + +- `document_number_configs` - เก็บ template และ counter configuration +- `document_number_counters` - เก็บ current counter value +- `document_number_audit` - เก็บ audit trail +- `documents` - เก็บ document number ที่ถูกสร้าง + +## 3.11.26 Database Schema Requirements + +### 3.11.26.1 Counter Table Schema + +ตาราง `document_number_counters` ต้องมีโครงสร้างดังนี้: + +```sql +CREATE TABLE document_number_counters ( + project_id INT NOT NULL, + originator_organization_id INT NOT NULL, + recipient_organization_id INT NULL, -- NULL for RFA + correspondence_type_id INT NOT NULL, + sub_type_id INT DEFAULT 0, -- for TRANSMITTAL + rfa_type_id INT DEFAULT 0, -- for RFA + discipline_id INT DEFAULT 0, -- for RFA + current_year INT NOT NULL, + version INT DEFAULT 0 NOT NULL, -- Optimistic Lock + last_number INT DEFAULT 0, + + PRIMARY KEY ( + project_id, + originator_organization_id, + COALESCE(recipient_organization_id, 0), + correspondence_type_id, + sub_type_id, + rfa_type_id, + discipline_id, + current_year + ), + + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci + COMMENT = 'ตารางเก็บ Running Number Counters'; +``` + +### 3.11.26.2 Index Requirements + +```sql +-- Index สำหรับ Performance +CREATE INDEX idx_counter_lookup +ON document_number_counters ( + project_id, + correspondence_type_id, + current_year +); + +-- Index สำหรับ Originator lookup +CREATE INDEX idx_counter_org +ON document_number_counters ( + originator_organization_id, + current_year +); +``` + +### 3.11.26.3 Important Notes + +> **💡 Counter Key Design** +> +> - ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL +> - `version` column สำหรับ Optimistic Locking (ป้องกัน race condition) +> - `last_number` เริ่มจาก 0 และเพิ่มขึ้นทีละ 1 +> - Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน) + +> **⚠️ Migration Notes** +> +> - ไม่มีข้อมูลเก่า ไม่ต้องทำ backward compatibility +> - สามารถสร้าง table ใหม่ได้เลยตาม schema ข้างต้น +> - ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน + +### 3.11.26.4 Example Counter Records + +```sql +-- Example: LETTER from คคง. to สคฉ.3 in LCBP3-C2 year 2025 +INSERT INTO document_number_counters ( + project_id, originator_organization_id, recipient_organization_id, + correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, + current_year, version, last_number +) VALUES ( + 2, -- LCBP3-C2 + 22, -- คคง. + 10, -- สคฉ.3 + 6, -- LETTER + 0, 0, 0, + 2025, 0, 0 +); + +-- Example: RFA from ผรม.2 in LCBP3-C2, discipline TER, type RPT, year 2025 +INSERT INTO document_number_counters ( + project_id, originator_organization_id, recipient_organization_id, + correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, + current_year, version, last_number +) VALUES ( + 2, -- LCBP3-C2 + 42, -- ผรม.2 + NULL, -- RFA ไม่มี specific recipient + 1, -- RFA + 0, + 18, -- RPT (Report) + 5, -- TER (Terminal) + 2025, 0, 0 +); +``` + +## 3.11.27 Security Considerations + +### 3.11.27.1 Authorization + +- เฉพาะ authenticated users เท่านั้นที่สามารถ request document number +- เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้ +- เฉพาะ Super Admin เท่านั้นที่ reset counter ได้ + +### 3.11.27.2 Rate Limiting + +- Limit ต่อ user: **10 requests/minute** (prevent abuse) +- Limit ต่อ IP: **50 requests/minute** + +### 3.11.27.3 Audit & Compliance + +- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering +- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) diff --git a/specs/01-requirements/03.11-document-numbering_schema_section.md b/docs/backup/03.11-document-numbering_schema_section.md similarity index 100% rename from specs/01-requirements/03.11-document-numbering_schema_section.md rename to docs/backup/03.11-document-numbering_schema_section.md diff --git a/infrastructure/backup/1.bak b/docs/backup/1.bak similarity index 100% rename from infrastructure/backup/1.bak rename to docs/backup/1.bak diff --git a/infrastructure/backup/DMS README.bak b/docs/backup/DMS README.bak similarity index 100% rename from infrastructure/backup/DMS README.bak rename to docs/backup/DMS README.bak diff --git a/infrastructure/backup/NestJS01.bak b/docs/backup/NestJS01.bak similarity index 100% rename from infrastructure/backup/NestJS01.bak rename to docs/backup/NestJS01.bak diff --git a/infrastructure/backup/NextJS01.bak b/docs/backup/NextJS01.bak similarity index 100% rename from infrastructure/backup/NextJS01.bak rename to docs/backup/NextJS01.bak diff --git a/infrastructure/backup/backend_setup.bak b/docs/backup/backend_setup.bak similarity index 100% rename from infrastructure/backup/backend_setup.bak rename to docs/backup/backend_setup.bak diff --git a/infrastructure/backup/data-dictionary-v1.5.1.md b/docs/backup/data-dictionary-v1.5.1.md similarity index 100% rename from infrastructure/backup/data-dictionary-v1.5.1.md rename to docs/backup/data-dictionary-v1.5.1.md diff --git a/docs/backup/document-numbering-add.md b/docs/backup/document-numbering-add.md new file mode 100644 index 0000000..b2749ae --- /dev/null +++ b/docs/backup/document-numbering-add.md @@ -0,0 +1,1813 @@ +# Backend Implementation Guide: Document Numbering + +**Version**: 1.0.0 +**Last Updated**: 2025-01-16 +**Status**: APPROVED +**Related**: [Requirements](../01-requirements/21-document-numbering-requirements.md), [ADR-018](../05-decisions/adr-018-document-numbering.md) + +--- + +## 1. Architecture Overview + +### 1.1 Module Structure +``` +backend/src/modules/document-numbering/ +├── document-numbering.module.ts +├── controllers/ +│ ├── numbering.controller.ts +│ ├── numbering-admin.controller.ts +│ └── numbering-metrics.controller.ts +├── services/ +│ ├── sequence.service.ts +│ ├── reservation.service.ts +│ ├── manual-override.service.ts +│ ├── void-replace.service.ts +│ ├── format.service.ts +│ ├── metrics.service.ts +│ └── migration.service.ts +├── entities/ +│ ├── numbering-config.entity.ts +│ ├── numbering-sequence.entity.ts +│ ├── numbering-audit-log.entity.ts +│ └── numbering-reservation.entity.ts +├── dto/ +│ ├── reserve-number.dto.ts +│ ├── confirm-reservation.dto.ts +│ ├── manual-override.dto.ts +│ ├── void-document.dto.ts +│ └── bulk-import.dto.ts +├── guards/ +│ └── manual-override.guard.ts +├── decorators/ +│ └── audit-numbering.decorator.ts +├── interfaces/ +│ └── numbering.interface.ts +└── tests/ + ├── unit/ + ├── integration/ + └── e2e/ +``` + +--- + +## 2. Core Entities + +### 2.1 Numbering Configuration +```typescript +// entities/numbering-config.entity.ts +import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; + +@Entity('document_numbering_configs') +export class NumberingConfig { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 50, unique: true }) + document_type: string; + + @Column({ length: 200 }) + format: string; + + @Column({ + type: 'enum', + enum: ['GLOBAL', 'PROJECT', 'CONTRACT', 'YEARLY', 'MONTHLY'], + default: 'GLOBAL' + }) + scope: string; + + @Column({ default: false }) + allow_manual_override: boolean; + + @Column({ default: 999999 }) + max_value: number; + + @Column({ type: 'json', nullable: true }) + metadata: Record; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + created_at: Date; + + @Column({ + type: 'timestamp', + default: () => 'CURRENT_TIMESTAMP', + onUpdate: 'CURRENT_TIMESTAMP' + }) + updated_at: Date; + + @OneToMany(() => NumberingSequence, sequence => sequence.config) + sequences: NumberingSequence[]; +} +``` + +### 2.2 Sequence Counter +```typescript +// entities/numbering-sequence.entity.ts +import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm'; + +@Entity('document_numbering_sequences') +export class NumberingSequence { + @PrimaryGeneratedColumn() + id: number; + + @Column() + config_id: number; + + @Column({ length: 50, nullable: true }) + scope_value: string; // project_id, contract_id, year, etc. + + @Column({ default: 0 }) + current_value: number; + + @Column({ type: 'timestamp', nullable: true }) + last_used_at: Date; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + created_at: Date; + + @ManyToOne(() => NumberingConfig, config => config.sequences) + @JoinColumn({ name: 'config_id' }) + config: NumberingConfig; + + // Composite unique constraint + @Index(['config_id', 'scope_value'], { unique: true }) +} +``` + +### 2.3 Audit Log +```typescript +// entities/numbering-audit-log.entity.ts +import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm'; + +@Entity('document_numbering_audit_logs') +export class NumberingAuditLog { + @PrimaryGeneratedColumn('increment', { type: 'bigint' }) + id: bigint; + + @Column({ length: 50 }) + @Index() + operation: string; // RESERVE, CONFIRM, CANCEL, MANUAL_OVERRIDE, VOID + + @Column({ length: 50, nullable: true }) + document_type: string; + + @Column({ length: 50, nullable: true }) + @Index() + document_number: string; + + @Column({ type: 'text', nullable: true }) + old_value: string; + + @Column({ type: 'text', nullable: true }) + new_value: string; + + @Column() + @Index() + user_id: number; + + @Column({ length: 45, nullable: true }) + ip_address: string; + + @Column({ length: 500, nullable: true }) + user_agent: string; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + @Index() + timestamp: Date; + + @Column({ type: 'json', nullable: true }) + metadata: Record; +} +``` + +--- + +## 3. Core Services + +### 3.1 Sequence Service +```typescript +// services/sequence.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { Redlock } from 'redlock'; +import { NumberingConfig, NumberingSequence } from '../entities'; +import { FormatService } from './format.service'; + +@Injectable() +export class SequenceService { + private readonly logger = new Logger(SequenceService.name); + + constructor( + @InjectRepository(NumberingConfig) + private configRepo: Repository, + + @InjectRepository(NumberingSequence) + private sequenceRepo: Repository, + + private dataSource: DataSource, + private redlock: Redlock, + private formatService: FormatService, + ) {} + + /** + * Get next sequence number with distributed locking + */ + async getNextSequence( + documentType: string, + scopeValue?: string, + ): Promise { + // 1. Get configuration + const config = await this.getConfig(documentType); + + // 2. Build lock key + const lockKey = this.buildLockKey(documentType, scopeValue); + + // 3. Try with Redlock first + try { + return await this.getSequenceWithRedlock(config, scopeValue, lockKey); + } catch (error) { + if (this.isRedisUnavailable(error)) { + // Fallback to database-only mode + this.logger.warn('Redis unavailable, using DB-only mode'); + return await this.getSequenceWithDbLock(config, scopeValue); + } + throw error; + } + } + + /** + * Get sequence with Redlock + Database pessimistic lock + */ + private async getSequenceWithRedlock( + config: NumberingConfig, + scopeValue: string, + lockKey: string, + ): Promise { + // Acquire distributed lock + const lock = await this.redlock.acquire([lockKey], 5000, { + retryCount: 3, + retryDelay: 200, + retryJitter: 100, + }); + + try { + return await this.dataSource.transaction(async (manager) => { + // Get or create sequence with pessimistic lock + let sequence = await manager.findOne(NumberingSequence, { + where: { + config_id: config.id, + scope_value: scopeValue || null, + }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!sequence) { + sequence = await this.createSequence(manager, config, scopeValue); + } + + // Increment sequence + const nextValue = await this.incrementSequence( + manager, + sequence, + config, + ); + + // Format number + return this.formatService.formatNumber(config.format, nextValue, { + documentType: config.document_type, + scopeValue, + }); + }); + } finally { + // Always release lock + await lock.release(); + } + } + + /** + * Fallback: Database-only locking (no Redis) + */ + private async getSequenceWithDbLock( + config: NumberingConfig, + scopeValue: string, + ): Promise { + return await this.dataSource.transaction(async (manager) => { + let sequence = await manager.findOne(NumberingSequence, { + where: { + config_id: config.id, + scope_value: scopeValue || null, + }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!sequence) { + sequence = await this.createSequence(manager, config, scopeValue); + } + + const nextValue = await this.incrementSequence( + manager, + sequence, + config, + ); + + return this.formatService.formatNumber(config.format, nextValue, { + documentType: config.document_type, + scopeValue, + }); + }); + } + + /** + * Increment sequence, skip cancelled numbers + */ + private async incrementSequence( + manager: EntityManager, + sequence: NumberingSequence, + config: NumberingConfig, + ): Promise { + let nextValue = sequence.current_value + 1; + + // Skip cancelled numbers + while (await this.isCancelledNumber(manager, config, nextValue)) { + this.logger.debug(`Skipping cancelled number: ${nextValue}`); + nextValue++; + } + + // Check max value + if (nextValue > config.max_value) { + throw new SequenceExhaustedError( + `Sequence exhausted for ${config.document_type}. Max: ${config.max_value}`, + ); + } + + // Update sequence + sequence.current_value = nextValue; + sequence.last_used_at = new Date(); + await manager.save(sequence); + + return nextValue; + } + + /** + * Check if number is cancelled + */ + private async isCancelledNumber( + manager: EntityManager, + config: NumberingConfig, + value: number, + ): Promise { + const count = await manager.count(NumberingAuditLog, { + where: { + document_type: config.document_type, + operation: 'CANCEL', + metadata: { sequence_value: value }, + }, + }); + return count > 0; + } + + /** + * Create new sequence + */ + private async createSequence( + manager: EntityManager, + config: NumberingConfig, + scopeValue: string, + ): Promise { + const sequence = manager.create(NumberingSequence, { + config_id: config.id, + scope_value: scopeValue || null, + current_value: 0, + }); + return await manager.save(sequence); + } + + /** + * Build lock key for Redlock + */ + private buildLockKey(documentType: string, scopeValue?: string): string { + const parts = ['numbering', documentType]; + if (scopeValue) parts.push(scopeValue); + return parts.join(':'); + } + + /** + * Check if error is Redis unavailable + */ + private isRedisUnavailable(error: any): boolean { + return error.message?.includes('Redis') || + error.message?.includes('ECONNREFUSED'); + } + + /** + * Get configuration (cached) + */ + @Cacheable({ ttl: 3600, key: 'numbering:config:{documentType}' }) + private async getConfig(documentType: string): Promise { + const config = await this.configRepo.findOne({ + where: { document_type: documentType }, + }); + + if (!config) { + throw new ConfigNotFoundError( + `Numbering config not found for ${documentType}`, + ); + } + + return config; + } +} +``` + +--- + +### 3.2 Reservation Service +```typescript +// services/reservation.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import { v4 as uuidv4 } from 'uuid'; +import { SequenceService } from './sequence.service'; +import { AuditService } from './audit.service'; + +interface Reservation { + token: string; + document_number: string; + document_type: string; + scope_value?: string; + expires_at: Date; + metadata?: Record; +} + +@Injectable() +export class ReservationService { + private readonly logger = new Logger(ReservationService.name); + private readonly TTL = 300; // 5 minutes + + constructor( + private redis: Redis, + private sequenceService: SequenceService, + private auditService: AuditService, + ) {} + + /** + * Reserve a document number + */ + async reserve( + documentType: string, + scopeValue?: string, + metadata?: Record, + ): Promise { + // 1. Generate next number + const documentNumber = await this.sequenceService.getNextSequence( + documentType, + scopeValue, + ); + + // 2. Generate reservation token + const token = uuidv4(); + + // 3. Calculate expiry + const expiresAt = new Date(Date.now() + this.TTL * 1000); + + // 4. Save reservation to Redis + const reservation: Reservation = { + token, + document_number: documentNumber, + document_type: documentType, + scope_value: scopeValue, + expires_at: expiresAt, + metadata, + }; + + await this.redis.setex( + `reservation:${token}`, + this.TTL, + JSON.stringify(reservation), + ); + + // 5. Audit log + await this.auditService.log({ + operation: 'RESERVE', + document_type: documentType, + document_number: documentNumber, + metadata: { token, scope_value: scopeValue }, + }); + + this.logger.log(`Reserved number: ${documentNumber}, token: ${token}`); + + return reservation; + } + + /** + * Confirm reservation + */ + async confirm(token: string, userId: number): Promise { + // 1. Get reservation from Redis + const reservation = await this.getReservation(token); + + if (!reservation) { + throw new ReservationExpiredError( + 'Reservation not found or expired. Please reserve a new number.', + ); + } + + // 2. Save to database (via document creation) + // Note: Actual document creation happens in the calling service + + // 3. Delete reservation from Redis + await this.redis.del(`reservation:${token}`); + + // 4. Audit log + await this.auditService.log({ + operation: 'CONFIRM', + document_type: reservation.document_type, + document_number: reservation.document_number, + user_id: userId, + metadata: { token }, + }); + + this.logger.log(`Confirmed reservation: ${reservation.document_number}`); + + return reservation.document_number; + } + + /** + * Cancel reservation + */ + async cancel(token: string, userId: number): Promise { + const reservation = await this.getReservation(token); + + if (reservation) { + // Delete reservation + await this.redis.del(`reservation:${token}`); + + // Audit log + await this.auditService.log({ + operation: 'CANCEL', + document_type: reservation.document_type, + document_number: reservation.document_number, + user_id: userId, + metadata: { token }, + }); + + this.logger.log(`Cancelled reservation: ${reservation.document_number}`); + } + } + + /** + * Get reservation from Redis + */ + private async getReservation(token: string): Promise { + const data = await this.redis.get(`reservation:${token}`); + return data ? JSON.parse(data) : null; + } + + /** + * Cleanup expired reservations (scheduled job) + */ + @Cron('0 */5 * * * *') // Every 5 minutes + async cleanupExpired(): Promise { + const keys = await this.redis.keys('reservation:*'); + + for (const key of keys) { + const ttl = await this.redis.ttl(key); + if (ttl <= 0) { + await this.redis.del(key); + this.logger.debug(`Cleaned up expired reservation: ${key}`); + } + } + } +} +``` + +--- + +### 3.3 Manual Override Service +```typescript +// services/manual-override.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { NumberingConfig, NumberingSequence } from '../entities'; +import { FormatService } from './format.service'; +import { AuditService } from './audit.service'; + +@Injectable() +export class ManualOverrideService { + private readonly logger = new Logger(ManualOverrideService.name); + + constructor( + @InjectRepository(NumberingConfig) + private configRepo: Repository, + + @InjectRepository(NumberingSequence) + private sequenceRepo: Repository, + + private dataSource: DataSource, + private formatService: FormatService, + private auditService: AuditService, + ) {} + + /** + * Create document with manual number + */ + async createWithManualNumber( + documentType: string, + manualNumber: string, + userId: number, + reason: string, + skipValidation = false, + ): Promise { + // 1. Get configuration + const config = await this.configRepo.findOne({ + where: { document_type: documentType }, + }); + + if (!config) { + throw new ConfigNotFoundError(`Config not found for ${documentType}`); + } + + if (!config.allow_manual_override) { + throw new ManualOverrideNotAllowedError( + `Manual override not allowed for ${documentType}`, + ); + } + + // 2. Validate + if (!skipValidation) { + await this.validate(manualNumber, config); + } + + // 3. Check duplicate + const exists = await this.checkDuplicate(manualNumber); + if (exists) { + throw new DuplicateNumberError( + `Number ${manualNumber} already exists`, + ); + } + + // 4. Update sequence if higher + await this.updateSequenceIfHigher( + documentType, + manualNumber, + config, + ); + + // 5. Audit log + await this.auditService.log({ + operation: 'MANUAL_OVERRIDE', + document_type: documentType, + document_number: manualNumber, + user_id: userId, + metadata: { reason, skip_validation: skipValidation }, + }); + + this.logger.log(`Manual override: ${manualNumber} by user ${userId}`); + } + + /** + * Validate manual number format + */ + private async validate( + manualNumber: string, + config: NumberingConfig, + ): Promise { + const isValid = this.formatService.matchesFormat( + manualNumber, + config.format, + ); + + if (!isValid) { + throw new InvalidFormatError( + `Number ${manualNumber} does not match format ${config.format}`, + ); + } + } + + /** + * Check if number already exists + */ + private async checkDuplicate(number: string): Promise { + // Check in your document tables + // This is a placeholder - implement based on your schema + const count = await this.dataSource.query( + ` + SELECT COUNT(*) as count FROM ( + SELECT document_number FROM correspondences WHERE document_number = ? + UNION ALL + SELECT document_number FROM rfas WHERE document_number = ? + UNION ALL + SELECT document_number FROM drawings WHERE document_number = ? + ) AS all_docs + `, + [number, number, number], + ); + + return count[0].count > 0; + } + + /** + * Update sequence counter if manual number is higher + */ + private async updateSequenceIfHigher( + documentType: string, + manualNumber: string, + config: NumberingConfig, + ): Promise { + // Extract sequence value from manual number + const sequenceValue = this.formatService.extractSequence( + manualNumber, + config.format, + ); + + if (!sequenceValue) { + this.logger.warn(`Could not extract sequence from ${manualNumber}`); + return; + } + + // Update sequence if higher + await this.dataSource.transaction(async (manager) => { + const sequence = await manager.findOne(NumberingSequence, { + where: { config_id: config.id }, + lock: { mode: 'pessimistic_write' }, + }); + + if (sequence && sequenceValue > sequence.current_value) { + sequence.current_value = sequenceValue; + sequence.last_used_at = new Date(); + await manager.save(sequence); + + this.logger.log( + `Updated sequence for ${documentType} to ${sequenceValue}`, + ); + } + }); + } +} +``` + +--- + +## 4. Controllers + +### 4.1 Main Numbering Controller +```typescript +// controllers/numbering.controller.ts +import { + Controller, Post, Body, UseGuards, + HttpCode, HttpStatus +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../auth/guards'; +import { CurrentUser } from '../../common/decorators'; +import { ReservationService } from '../services'; +import { ReserveNumberDto, ConfirmReservationDto } from '../dto'; + +@ApiTags('Document Numbering') +@ApiBearerAuth() +@Controller('document-numbering') +@UseGuards(JwtAuthGuard) +export class NumberingController { + constructor( + private reservationService: ReservationService, + ) {} + + @Post('reserve') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Reserve a document number' }) + async reserve( + @Body() dto: ReserveNumberDto, + @CurrentUser() user: any, + ) { + const reservation = await this.reservationService.reserve( + dto.document_type, + dto.scope_value, + dto.metadata, + ); + + return { + success: true, + data: reservation, + }; + } + + @Post('confirm') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Confirm a reservation' }) + async confirm( + @Body() dto: ConfirmReservationDto, + @CurrentUser() user: any, + ) { + const documentNumber = await this.reservationService.confirm( + dto.token, + user.id, + ); + + return { + success: true, + data: { document_number: documentNumber }, + }; + } + + @Post('cancel') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Cancel a reservation' }) + async cancel( + @Body() dto: ConfirmReservationDto, + @CurrentUser() user: any, + ) { + await this.reservationService.cancel(dto.token, user.id); + + return { + success: true, + message: 'Reservation cancelled', + }; + } +} +``` + +--- + +## 5. Integration with Document Creation + +### 5.1 Correspondence Example +```typescript +// modules/correspondence/services/correspondence.service.ts +@Injectable() +export class CorrespondenceService { + constructor( + private reservationService: ReservationService, + private dataSource: DataSource, + ) {} + + async create(dto: CreateCorrespondenceDto, userId: number) { + // Phase 1: Reserve number + const { token, document_number } = await this.reservationService.reserve( + 'COR', + dto.project_id.toString(), + ); + + try { + // Phase 2: Create document in transaction + const correspondence = await this.dataSource.transaction( + async (manager) => { + // Create correspondence + const corr = manager.create(Correspondence, { + document_number, + ...dto, + created_by: userId, + }); + + await manager.save(corr); + + // Confirm reservation + await this.reservationService.confirm(token, userId); + + return corr; + }, + ); + + return correspondence; + } catch (error) { + // Phase 2 failed: Cancel reservation + await this.reservationService.cancel(token, userId); + throw error; + } + } +} +``` + +--- + +## 6. Testing + +### 6.1 Unit Tests +```typescript +// tests/unit/sequence.service.spec.ts +describe('SequenceService', () => { + let service: SequenceService; + let configRepo: Repository; + let sequenceRepo: Repository; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + SequenceService, + { provide: getRepositoryToken(NumberingConfig), useValue: mockRepo }, + { provide: getRepositoryToken(NumberingSequence), useValue: mockRepo }, + { provide: DataSource, useValue: mockDataSource }, + { provide: Redlock, useValue: mockRedlock }, + { provide: FormatService, useValue: mockFormatService }, + ], + }).compile(); + + service = module.get(SequenceService); + }); + + it('should generate sequential numbers', async () => { + const num1 = await service.getNextSequence('COR'); + const num2 = await service.getNextSequence('COR'); + + expect(extractSeq(num1)).toBe(1); + expect(extractSeq(num2)).toBe(2); + }); + + it('should skip cancelled numbers', async () => { + // Mark sequence 2 as cancelled + await markAsCancelled('COR', 2); + + const num1 = await service.getNextSequence('COR'); + const num2 = await service.getNextSequence('COR'); + + expect(extractSeq(num1)).toBe(1); + expect(extractSeq(num2)).toBe(3); // Skipped 2 + }); + + it('should throw on sequence exhausted', async () => { + await setSequence('COR', 999999); // Max value + + await expect( + service.getNextSequence('COR') + ).rejects.toThrow(SequenceExhaustedError); + }); +}); +``` + +### 6.2 Integration Tests +```typescript +// tests/integration/reservation.spec.ts +describe('Reservation Flow (Integration)', () => { + let app: INestApplication; + let redis: Redis; + + beforeAll(async () => { + app = await createTestingApp(); + redis = app.get(Redis); + }); + + it('should complete two-phase commit successfully', async () => { + // Phase 1: Reserve + const { body: reserve } = await request(app.getHttpServer()) + .post('/document-numbering/reserve') + .send({ document_type: 'COR' }) + .expect(201); + + expect(reserve.data.token).toBeDefined(); + expect(reserve.data.document_number).toMatch(/^COR-\d{4}-\d{5}$/); + + // Verify reservation in Redis + const cached = await redis.get(`reservation:${reserve.data.token}`); + expect(cached).toBeDefined(); + + // Phase 2: Confirm + const { body: confirm } = await request(app.getHttpServer()) + .post('/document-numbering/confirm') + .send({ token: reserve.data.token }) + .expect(200); + + expect(confirm.data.document_number).toBe(reserve.data.document_number); + + // Verify reservation deleted + const deleted = await redis.get(`reservation:${reserve.data.token}`); + expect(deleted).toBeNull(); + }); + + it('should handle cancel gracefully', async () => { + const { body: reserve } = await request(app.getHttpServer()) + .post('/document-numbering/reserve') + .send({ document_type: 'COR' }) + .expect(201); + + await request(app.getHttpServer()) + .post('/document-numbering/cancel') + .send({ token: reserve.data.token }) + .expect(200); + + // Verify reservation deleted + const deleted = await redis.get(`reservation:${reserve.data.token}`); + expect(deleted).toBeNull(); + }); +}); +``` + +### 6.3 Load Tests +```typescript +// tests/load/concurrency.spec.ts +describe('Concurrency Test', () => { + it('should handle 1000 concurrent requests without duplicates', async () => { + const promises = Array.from({ length: 1000 }, (_, i) => + request(app.getHttpServer()) + .post('/document-numbering/reserve') + .send({ document_type: 'COR' }) + ); + + const results = await Promise.all(promises); + + // Extract all document numbers + const numbers = results.map(r => r.body.data.document_number); + + // Check for duplicates + const uniqueNumbers = new Set(numbers); + expect(uniqueNumbers.size).toBe(1000); + + // Verify sequential + const sequences = numbers.map(n => extractSeq(n)).sort((a, b) => a - b); + expect(sequences[0]).toBe(1); + expect(sequences[999]).toBe(1000); + }); +}); +``` + +--- + +## 7. Deployment Checklist + +### 7.1 Pre-Deployment +- [ ] Run all tests (unit, integration, E2E) +- [ ] Load test (1000 req/s for 5 min) +- [ ] Setup Redis cluster (3 nodes) +- [ ] Run database migrations +- [ ] Configure environment variables +- [ ] Setup monitoring (Prometheus + Grafana) +- [ ] Configure alerts (PagerDuty/Slack) +- [ ] Review security settings +- [ ] Backup database +- [ ] Document rollback procedure + +### 7.2 Deployment Steps +1. Deploy Redis cluster to staging +2. Run migrations on staging database +3. Deploy backend service to staging +4. Run smoke tests on staging +5. Load test staging environment +6. Get approval from stakeholders +7. Deploy to production (blue-green deployment) +8. Monitor for 1 hour +9. Gradual rollout (10% → 50% → 100%) + +### 7.3 Post-Deployment +- [ ] Verify all metrics green +- [ ] Check error rates (<0.1%) +- [ ] Validate audit logs working +- [ ] Test critical workflows +- [ ] Monitor performance for 24 hours +- [ ] Collect user feedback +- [ ] Schedule retrospective + +--- + +## 8. Monitoring & Observability + +### 8.1 Prometheus Metrics +```typescript +// metrics/numbering.metrics.ts +import { Injectable } from '@nestjs/common'; +import { Counter, Gauge, Histogram, register } from 'prom-client'; + +@Injectable() +export class NumberingMetrics { + // Counter: Total numbers generated + private readonly numbersGenerated = new Counter({ + name: 'numbering_sequences_total', + help: 'Total document numbers generated', + labelNames: ['document_type'], + }); + + // Gauge: Current sequence value + private readonly sequenceValue = new Gauge({ + name: 'numbering_sequence_current', + help: 'Current sequence value', + labelNames: ['document_type', 'scope'], + }); + + // Gauge: Sequence utilization (%) + private readonly sequenceUtilization = new Gauge({ + name: 'numbering_sequence_utilization', + help: 'Sequence utilization percentage', + labelNames: ['document_type'], + }); + + // Histogram: Lock wait time + private readonly lockWaitTime = new Histogram({ + name: 'numbering_lock_wait_seconds', + help: 'Time spent waiting for lock acquisition', + labelNames: ['document_type'], + buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5], + }); + + // Counter: Lock failures + private readonly lockFailures = new Counter({ + name: 'numbering_lock_failures_total', + help: 'Total lock acquisition failures', + labelNames: ['document_type', 'reason'], + }); + + // Counter: Manual overrides + private readonly manualOverrides = new Counter({ + name: 'numbering_manual_overrides_total', + help: 'Total manual overrides', + labelNames: ['document_type'], + }); + + incrementNumbersGenerated(documentType: string) { + this.numbersGenerated.inc({ document_type: documentType }); + } + + setSequenceValue(documentType: string, scope: string, value: number) { + this.sequenceValue.set({ document_type: documentType, scope }, value); + } + + setSequenceUtilization(documentType: string, percent: number) { + this.sequenceUtilization.set({ document_type: documentType }, percent); + } + + observeLockWaitTime(documentType: string, seconds: number) { + this.lockWaitTime.observe({ document_type: documentType }, seconds); + } + + incrementLockFailures(documentType: string, reason: string) { + this.lockFailures.inc({ document_type: documentType, reason }); + } + + incrementManualOverrides(documentType: string) { + this.manualOverrides.inc({ document_type: documentType }); + } +} +``` + +### 8.2 Grafana Dashboard +```json +{ + "dashboard": { + "title": "Document Numbering", + "panels": [ + { + "title": "Numbers Generated per Minute", + "targets": [ + { + "expr": "rate(numbering_sequences_total[1m])" + } + ] + }, + { + "title": "Sequence Utilization", + "targets": [ + { + "expr": "numbering_sequence_utilization" + } + ], + "thresholds": [90, 95] + }, + { + "title": "Lock Wait Time (p95)", + "targets": [ + { + "expr": "histogram_quantile(0.95, numbering_lock_wait_seconds)" + } + ] + }, + { + "title": "Lock Failures", + "targets": [ + { + "expr": "rate(numbering_lock_failures_total[5m])" + } + ] + } + ] + } +} +``` + +### 8.3 Alert Rules +```yaml +# alerts/numbering.yml +groups: + - name: numbering_alerts + interval: 30s + rules: + # Critical: Sequence >95% used + - alert: SequenceCritical + expr: numbering_sequence_utilization > 95 + for: 5m + labels: + severity: critical + annotations: + summary: "Sequence {{ $labels.document_type }} >95% used" + description: "Current: {{ $value }}%. Extend max_value immediately." + + # Warning: Sequence >90% used + - alert: SequenceWarning + expr: numbering_sequence_utilization > 90 + for: 10m + labels: + severity: warning + annotations: + summary: "Sequence {{ $labels.document_type }} >90% used" + description: "Current: {{ $value }}%. Plan to extend max_value." + + # Critical: High lock wait time + - alert: HighLockWaitTime + expr: histogram_quantile(0.95, numbering_lock_wait_seconds) > 1 + for: 5m + labels: + severity: warning + annotations: + summary: "Lock wait time >1s (p95)" + description: "p95: {{ $value }}s. Check Redis cluster health." + + # Critical: Redis down + - alert: RedisUnavailable + expr: up{job="redis-numbering"} == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "Redis cluster unavailable" + description: "Numbering system using DB-only fallback mode." + + # Warning: High error rate + - alert: HighErrorRate + expr: rate(numbering_errors_total[5m]) > 10 + for: 5m + labels: + severity: warning + annotations: + summary: "High error rate in numbering system" + description: "{{ $value }} errors/sec. Check logs." +``` + +--- + +## 9. Troubleshooting Guide + +### 9.1 Common Issues + +#### Issue 1: Duplicate Numbers Generated +**Symptoms**: Same document number appears twice + +**Diagnosis**: +```sql +-- Find duplicates +SELECT document_number, COUNT(*) as count +FROM ( + SELECT document_number FROM correspondences + UNION ALL + SELECT document_number FROM rfas + UNION ALL + SELECT document_number FROM drawings +) AS all_docs +GROUP BY document_number +HAVING count > 1; +``` + +**Root Causes**: +- Redis cluster failure during generation +- Database deadlock +- Bug in locking logic + +**Resolution**: +1. Identify affected documents +2. Manually reassign one with new number +3. Update audit log +4. Review lock acquisition logs +5. Add additional monitoring + +--- + +#### Issue 2: Sequence Exhausted +**Symptoms**: Error "Sequence exhausted for COR" + +**Diagnosis**: +```sql +-- Check current vs max +SELECT + c.document_type, + s.current_value, + c.max_value, + (s.current_value * 100.0 / c.max_value) as utilization +FROM document_numbering_sequences s +JOIN document_numbering_configs c ON s.config_id = c.id +WHERE s.current_value >= c.max_value * 0.9; +``` + +**Resolution**: +```sql +-- Extend max_value +UPDATE document_numbering_configs +SET max_value = max_value * 10 +WHERE document_type = 'COR'; + +-- Or reset yearly sequence (if applicable) +UPDATE document_numbering_sequences +SET current_value = 0, + scope_value = '2026' -- New year +WHERE config_id = ( + SELECT id FROM document_numbering_configs + WHERE document_type = 'COR' +); +``` + +--- + +#### Issue 3: Lock Timeout +**Symptoms**: "Failed to acquire lock after 3 retries" + +**Diagnosis**: +```bash +# Check Redis cluster health +redis-cli --cluster check localhost:7000 + +# Check lock contention +redis-cli KEYS "numbering:*" +redis-cli GET "numbering:COR:project-1" +``` + +**Root Causes**: +- High concurrent load +- Redis node down +- Network latency +- Deadlock in database + +**Resolution**: +1. Check Redis cluster health +2. Increase lock timeout (5s → 10s) +3. Add more Redis nodes +4. Review database slow queries +5. Implement exponential backoff + +--- + +#### Issue 4: Reservation Expired +**Symptoms**: User gets "Reservation expired" error + +**Diagnosis**: +```bash +# Check Redis TTL +redis-cli TTL "reservation:uuid-here" + +# List all reservations +redis-cli KEYS "reservation:*" +``` + +**Root Causes**: +- User took >5 minutes to complete form +- Network issue during confirmation +- Browser closed/refreshed + +**Resolution**: +1. Reserve new number +2. Consider increasing TTL (5 min → 10 min) +3. Add progress auto-save +4. Show countdown timer in UI + +--- + +### 9.2 Debug Commands + +```bash +# Check sequence status +npm run cli numbering:status COR + +# Manually adjust sequence +npm run cli numbering:set COR 1000 + +# Validate sequence integrity +npm run cli numbering:validate + +# Export audit logs +npm run cli numbering:audit-export \ + --start "2025-01-01" \ + --end "2025-01-31" \ + --format csv \ + --output audit.csv + +# Simulate load +npm run cli numbering:load-test \ + --type COR \ + --requests 1000 \ + --concurrency 100 + +# Check for duplicates +npm run cli numbering:check-duplicates +``` + +--- + +## 10. Performance Optimization + +### 10.1 Database Indexes +```sql +-- Composite index for faster lookups +CREATE INDEX idx_sequence_lookup +ON document_numbering_sequences(config_id, scope_value); + +-- Covering index for metrics +CREATE INDEX idx_audit_metrics +ON document_numbering_audit_logs(document_type, timestamp) +INCLUDE (operation, user_id); + +-- Index for duplicate checking +CREATE INDEX idx_doc_number +ON correspondences(document_number); + +CREATE INDEX idx_doc_number +ON rfas(document_number); + +CREATE INDEX idx_doc_number +ON drawings(document_number); +``` + +### 10.2 Connection Pooling +```typescript +// config/database.config.ts +export default { + type: 'mariadb', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), + database: process.env.DB_NAME, + + // Connection pool settings + extra: { + connectionLimit: 20, // Max connections + queueLimit: 0, // Unlimited queue + waitForConnections: true, + acquireTimeout: 30000, // 30s timeout + idleTimeout: 10000, // 10s idle timeout + maxIdle: 5, // Max idle connections + }, +}; +``` + +### 10.3 Redis Optimization +```typescript +// config/redis.config.ts +export default { + cluster: [ + { host: 'redis-1', port: 6379 }, + { host: 'redis-2', port: 6379 }, + { host: 'redis-3', port: 6379 }, + ], + options: { + maxRetriesPerRequest: 3, + enableReadyCheck: true, + maxLoadingRetryTime: 10000, + lazyConnect: false, + + // Connection pool + maxRedirections: 16, + retryDelayOnFailover: 100, + retryDelayOnClusterDown: 300, + + // Performance + enableOfflineQueue: true, + connectTimeout: 10000, + keepAlive: 30000, + }, +}; +``` + +### 10.4 Caching Strategy +```typescript +// Cache configuration +@Cacheable({ + ttl: 3600, // 1 hour + key: 'numbering:config:{documentType}', + compress: true, +}) +async getConfig(documentType: string) { + return await this.configRepo.findOne({ + where: { document_type: documentType }, + }); +} + +// Cache invalidation +@CacheEvict({ + key: 'numbering:config:{documentType}', +}) +async updateConfig(documentType: string, data: any) { + return await this.configRepo.update( + { document_type: documentType }, + data, + ); +} +``` + +--- + +## 11. Security Considerations + +### 11.1 Access Control +```typescript +// guards/manual-override.guard.ts +@Injectable() +export class ManualOverrideGuard implements CanActivate { + constructor(private caslAbility: CaslAbilityFactory) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const user = request.user; + + // Check permission + const ability = this.caslAbility.createForUser(user); + + return ability.can('manual_override', 'DocumentNumber'); + } +} + +// Usage +@Post('manual') +@UseGuards(JwtAuthGuard, ManualOverrideGuard) +async manualOverride(@Body() dto: ManualOverrideDto) { + // Only admins can access this +} +``` + +### 11.2 Rate Limiting +```typescript +// Apply rate limit to prevent abuse +@Throttle(100, 60) // 100 requests per minute +@Post('reserve') +async reserve(@Body() dto: ReserveNumberDto) { + // ... +} +``` + +### 11.3 Audit Logging +```typescript +// decorators/audit-numbering.decorator.ts +export function AuditNumbering(operation: string) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: any[]) { + const result = await originalMethod.apply(this, args); + + // Log to audit + await this.auditService.log({ + operation, + timestamp: new Date(), + user_id: args[0]?.user?.id, + metadata: { args }, + }); + + return result; + }; + + return descriptor; + }; +} + +// Usage +@AuditNumbering('MANUAL_OVERRIDE') +async manualOverride(dto: ManualOverrideDto, user: User) { + // Automatically logged +} +``` + +--- + +## 12. Migration Scripts + +### 12.1 Import Legacy Documents +```typescript +// scripts/import-legacy-numbers.ts +import { DataSource } from 'typeorm'; +import * as csv from 'csv-parser'; +import * as fs from 'fs'; + +async function importLegacyNumbers() { + const dataSource = await createDataSource(); + const results = []; + + // Read CSV + fs.createReadStream('legacy-documents.csv') + .pipe(csv()) + .on('data', (row) => results.push(row)) + .on('end', async () => { + console.log(`Found ${results.length} legacy documents`); + + let success = 0; + let failed = 0; + + for (const row of results) { + try { + await dataSource.transaction(async (manager) => { + // 1. Create document + await manager.insert('correspondences', { + document_number: row.document_number, + title: row.title, + created_at: row.created_at, + is_imported: true, + }); + + // 2. Log audit + await manager.insert('document_numbering_audit_logs', { + operation: 'MANUAL_OVERRIDE', + document_type: 'COR', + document_number: row.document_number, + metadata: { imported: true, source: 'legacy' }, + }); + + success++; + }); + } catch (error) { + console.error(`Failed to import ${row.document_number}:`, error); + failed++; + } + } + + console.log(`Import complete: ${success} success, ${failed} failed`); + + // 3. Update sequence counters + await updateSequenceCounters(dataSource); + }); +} + +async function updateSequenceCounters(dataSource: DataSource) { + const result = await dataSource.query(` + SELECT MAX(CAST(SUBSTRING_INDEX(document_number, '-', -1) AS UNSIGNED)) as max_seq + FROM correspondences + WHERE document_number LIKE 'COR-2025-%' + `); + + const maxSeq = result[0].max_seq; + + await dataSource.query(` + UPDATE document_numbering_sequences + SET current_value = ? + WHERE config_id = ( + SELECT id FROM document_numbering_configs + WHERE document_type = 'COR' + ) + `, [maxSeq]); + + console.log(`Updated COR sequence to ${maxSeq}`); +} + +importLegacyNumbers().catch(console.error); +``` + +--- + +## 13. CLI Tools + +### 13.1 Status Command +```typescript +// cli/commands/numbering-status.command.ts +import { Command, CommandRunner } from 'nest-commander'; +import { SequenceService } from '../../modules/document-numbering/services'; + +@Command({ + name: 'numbering:status', + arguments: '[documentType]', + options: { isDefault: false }, +}) +export class NumberingStatusCommand extends CommandRunner { + constructor(private sequenceService: SequenceService) { + super(); + } + + async run(inputs: string[], options: any): Promise { + const [documentType] = inputs; + + if (documentType) { + await this.showTypeStatus(documentType); + } else { + await this.showAllStatus(); + } + } + + private async showTypeStatus(documentType: string) { + const config = await this.sequenceService.getConfig(documentType); + const sequence = await this.sequenceService.getSequence(documentType); + + console.log(`\n📊 Status for ${documentType}:\n`); + console.log(`Format: ${config.format}`); + console.log(`Current Value: ${sequence.current_value}`); + console.log(`Max Value: ${config.max_value}`); + console.log(`Utilization: ${(sequence.current_value / config.max_value * 100).toFixed(2)}%`); + console.log(`Last Used: ${sequence.last_used_at}`); + console.log(`Manual Override: ${config.allow_manual_override ? 'Yes' : 'No'}`); + } + + private async showAllStatus() { + const configs = await this.sequenceService.getAllConfigs(); + + console.log('\n📊 Document Numbering Status:\n'); + console.table( + configs.map((c) => ({ + Type: c.document_type, + Current: c.sequence?.current_value || 0, + Max: c.max_value, + 'Utilization (%)': ((c.sequence?.current_value || 0) / c.max_value * 100).toFixed(2), + 'Last Used': c.sequence?.last_used_at || 'Never', + })), + ); + } +} +``` + +--- + +## 14. Best Practices Summary + +### 14.1 DO's ✅ +- ✅ Always use two-phase commit (reserve + confirm) +- ✅ Implement fallback to DB-only if Redis fails +- ✅ Log every operation to audit trail +- ✅ Monitor sequence utilization (alert at 90%) +- ✅ Test under concurrent load (1000+ req/s) +- ✅ Use pessimistic locking in database +- ✅ Set reasonable TTL for reservations (5 min) +- ✅ Validate manual override format +- ✅ Skip cancelled numbers (never reuse) +- ✅ Implement exponential backoff on retry + +### 14.2 DON'Ts ❌ +- ❌ Never skip validation for manual override +- ❌ Never reuse cancelled numbers +- ❌ Never trust client-generated numbers +- ❌ Never increase sequence without transaction +- ❌ Never ignore lock acquisition failures +- ❌ Never deploy without load testing +- ❌ Never extend max_value without planning +- ❌ Never modify sequence table directly +- ❌ Never skip audit logging +- ❌ Never assume Redis is always available + +--- + +## 15. Appendix + +### 15.1 Error Codes +```typescript +export enum NumberingErrorCode { + CONFIG_NOT_FOUND = 'NB001', + SEQUENCE_EXHAUSTED = 'NB002', + LOCK_TIMEOUT = 'NB003', + RESERVATION_EXPIRED = 'NB004', + DUPLICATE_NUMBER = 'NB005', + INVALID_FORMAT = 'NB006', + MANUAL_OVERRIDE_NOT_ALLOWED = 'NB007', + REDIS_UNAVAILABLE = 'NB008', +} +``` + +### 15.2 Environment Variables +```bash +# Redis Configuration +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_CLUSTER_NODES=redis-1:6379,redis-2:6379,redis-3:6379 + +# Numbering Configuration +NUMBERING_LOCK_TIMEOUT=5000 # 5 seconds +NUMBERING_RESERVATION_TTL=300 # 5 minutes +NUMBERING_RETRY_ATTEMPTS=3 +NUMBERING_RETRY_DELAY=200 # milliseconds + +# Monitoring +PROMETHEUS_PORT=9090 +GRAFANA_PORT=3000 +``` + +### 15.3 Useful Queries +```sql +-- Find next available number +SELECT MAX(CAST(SUBSTRING_INDEX(document_number, '-', -1) AS UNSIGNED)) + 1 +FROM correspondences +WHERE document_number LIKE 'COR-2025-%'; + +-- Check for gaps in sequence +SELECT t1.seq + 1 AS gap_start +FROM ( + SELECT CAST(SUBSTRING_INDEX(document_number, '-', -1) AS UNSIGNED) AS seq + FROM correspondences + WHERE document_number LIKE 'COR-2025-%' +) t1 +LEFT JOIN ( + SELECT CAST(SUBSTRING_INDEX(document_number, '-', -1) AS UNSIGNED) AS seq + FROM correspondences + WHERE document_number LIKE 'COR-2025-%' +) t2 ON t1.seq + 1 = t2.seq +WHERE t2.seq IS NULL +ORDER BY gap_start; + +-- Audit trail for specific number +SELECT * +FROM document_numbering_audit_logs +WHERE document_number = 'COR-2025-00042' +ORDER BY timestamp DESC; +``` + +--- + +**Document Prepared By**: Backend Team +**Last Review**: 2025-01-16 +**Next Review**: 2025-04-16 diff --git a/docs/backup/document-numbering.md b/docs/backup/document-numbering.md new file mode 100644 index 0000000..aa348bf --- /dev/null +++ b/docs/backup/document-numbering.md @@ -0,0 +1,813 @@ +# Document Numbering Implementation Guide + +--- +title: 'Implementation Guide: Document Numbering System' +version: 1.6.1 +status: implemented +owner: Development Team +last_updated: 2025-12-16 +related: + +- specs/01-requirements/03.11-document-numbering.md +- specs/04-operations/document-numbering-operations.md + +--- + +## Overview + +เอกสารนี้อธิบาย implementation details สำหรับระบบ Document Numbering ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) + +## Technology Stack + +- **Backend Framework**: NestJS 10.x +- **ORM**: TypeORM 0.3.x +- **Database**: MariaDB 11.8 +- **Cache/Lock**: Redis 7.x + Redlock +- **Message Queue**: BullMQ +- **Monitoring**: Prometheus + Grafana + +## 1. Database Implementation + +### 1.1. Counter Table Schema + +```sql +CREATE TABLE document_number_formats ( + id INT AUTO_INCREMENT PRIMARY KEY, + project_id INT NOT NULL, + correspondence_type_id INT NULL, -- NULL indicates default format for the project + format_template VARCHAR(100) NOT NULL, + reset_sequence_yearly TINYINT(1) DEFAULT 1, + description VARCHAR(255), + created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6), + updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + + UNIQUE KEY idx_unique_project_type (project_id, correspondence_type_id), + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +); + +CREATE TABLE document_number_counters ( + project_id INT NOT NULL, + originator_organization_id INT NOT NULL, + recipient_organization_id INT NULL, + correspondence_type_id INT NOT NULL, + sub_type_id INT DEFAULT 0, + rfa_type_id INT DEFAULT 0, + discipline_id INT DEFAULT 0, + current_year INT NOT NULL, + version INT DEFAULT 0 NOT NULL, + last_number INT DEFAULT 0, + + PRIMARY KEY ( + project_id, + originator_organization_id, + COALESCE(recipient_organization_id, 0), + correspondence_type_id, + sub_type_id, + rfa_type_id, + discipline_id, + current_year + ), + + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE, + + INDEX idx_counter_lookup (project_id, correspondence_type_id, current_year), + INDEX idx_counter_org (originator_organization_id, current_year), + + CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), + CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='ตารางเก็บ Running Number Counters'; +``` + +### 1.2. Audit Table Schema + +```sql +CREATE TABLE document_number_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + document_id INT NULL COMMENT 'FK to documents (NULL initially, updated after doc creation)', + generated_number VARCHAR(100) NOT NULL, + counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)', + template_used VARCHAR(200) NOT NULL, + user_id INT NULL COMMENT 'FK to users (Allow NULL for system generation)', + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_success BOOLEAN DEFAULT TRUE COMMENT 'Track success/failure status', + + -- Performance & Error Tracking + retry_count INT DEFAULT 0, + lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds', + total_duration_ms INT COMMENT 'Total generation time', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE', + + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail'; +``` + +### 1.3. Error Log Table + +```sql +CREATE TABLE document_number_errors ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + error_type ENUM( + 'LOCK_TIMEOUT', + 'VERSION_CONFLICT', + 'DB_ERROR', + 'REDIS_ERROR', + 'VALIDATION_ERROR' + ) NOT NULL, + error_message TEXT, + stack_trace TEXT, + context_data JSON COMMENT 'Request context (user, project, etc.)', + user_id INT, + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB COMMENT='Document Numbering Error Log'; +``` + +## 2. NestJS Implementation + +### 2.1. Module Structure + +``` +src/modules/document-numbering/ +├── document-numbering.module.ts +├── controllers/ +│ └── document-numbering.controller.ts +├── services/ +│ ├── document-numbering.service.ts +│ ├── document-numbering-lock.service.ts +│ ├── counter.service.ts +│ ├── template.service.ts +│ └── audit.service.ts +├── entities/ +│ ├── document-number-counter.entity.ts +│ ├── document-number-audit.entity.ts +│ └── document-number-error.entity.ts +├── dto/ +│ ├── generate-number.dto.ts +│ └── update-template.dto.ts +├── validators/ +│ └── template.validator.ts +├── jobs/ +│ └── counter-reset.job.ts +└── metrics/ + └── metrics.service.ts +``` + +### 2.2. Number Generation Process + +#### 2.2.1. Resolve Format Template: + * Query document_number_formats by project_id + type_id. + * If no result, query by project_id + NULL (Default Project Format). + * If still no result, apply System Default Template: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`. + * Determine resetSequenceYearly flag from the found format (default: true) + +#### 2.2.2. Determine Counter Key: + * If resetSequenceYearly is True: Use Current Year (e.g., 2025). + * If resetSequenceYearly is False: Use 0 (Continuous). + * Use type_id from the resolved format (Specific ID or NULL). + +#### 2.2.3. Generate Number: + * Use format template to generate number. + * Replace tokens with actual values: + * {PROJECT} -> Project Code + * {ORG} -> Originator Organization Code + * {RECIPIENT} -> Recipient Organization Code + * {TYPE} -> Type Code + * {YEAR} -> Current Year + * {SEQ} -> Sequence Number + * {REV} -> Revision Number + +#### 2.2.4. Validate Number: + * Check if generated number is unique. + * If not unique, increment sequence and retry. + +#### 2.2.5. Update Counter: + * Update document_number_counters with new sequence. + +#### 2.2.6. Generate Audit Record: + * Create audit record with: + * Generated number + * Counter key used + * Template used + * User ID + * IP Address + * User Agent + +#### 2.2.7. Return Generated Number: + * Return generated number to caller. + +### 2.3. TypeORM Entity + +```typescript +// File: src/modules/document-numbering/entities/document-number-counter.entity.ts +import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm'; + +@Entity('document_number_counters') +export class DocumentNumberCounter { + @PrimaryColumn({ name: 'project_id' }) + projectId: number; + + @PrimaryColumn({ name: 'originator_organization_id' }) + originatorOrganizationId: number; + + @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) + recipientOrganizationId: number | null; + + @PrimaryColumn({ name: 'correspondence_type_id' }) + correspondenceTypeId: number; + + @PrimaryColumn({ name: 'sub_type_id', default: 0 }) + subTypeId: number; + + @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) + rfaTypeId: number; + + @PrimaryColumn({ name: 'discipline_id', default: 0 }) + disciplineId: number; + + @PrimaryColumn({ name: 'current_year' }) + currentYear: number; + + @VersionColumn({ name: 'version' }) + version: number; + + @Column({ name: 'last_number', default: 0 }) + lastNumber: number; +} +``` + +### 2.4. Redis Lock Service + +```typescript +// File: src/modules/document-numbering/services/document-numbering-lock.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import Redlock from 'redlock'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import { Redis } from 'ioredis'; + +interface CounterKey { + projectId: number; + originatorOrgId: number; + recipientOrgId: number | null; + correspondenceTypeId: number; + subTypeId: number; + rfaTypeId: number; + disciplineId: number; + year: number; +} + +@Injectable() +export class DocumentNumberingLockService { + private readonly logger = new Logger(DocumentNumberingLockService.name); + private redlock: Redlock; + + constructor(@InjectRedis() private readonly redis: Redis) { + this.redlock = new Redlock([redis], { + driftFactor: 0.01, + retryCount: 5, + retryDelay: 100, + retryJitter: 50, + }); + } + + async acquireLock(counterKey: CounterKey): Promise { + const lockKey = this.buildLockKey(counterKey); + const ttl = 5000; // 5 วินาที + + try { + const lock = await this.redlock.acquire([lockKey], ttl); + this.logger.debug(`Acquired lock: ${lockKey}`); + return lock; + } catch (error) { + this.logger.error(`Failed to acquire lock: ${lockKey}`, error); + throw error; + } + } + + async releaseLock(lock: Redlock.Lock): Promise { + try { + await lock.release(); + this.logger.debug('Released lock'); + } catch (error) { + this.logger.warn('Failed to release lock (may have expired)', error); + } + } + + private buildLockKey(key: CounterKey): string { + return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + + `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + + `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; + } +} +``` + +### 2.4. Counter Service + +```typescript +// File: src/modules/document-numbering/services/counter.service.ts +import { Injectable, ConflictException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { DocumentNumberCounter } from '../entities/document-number-counter.entity'; +import { OptimisticLockVersionMismatchError } from 'typeorm'; + +@Injectable() +export class CounterService { + private readonly logger = new Logger(CounterService.name); + + constructor( + @InjectRepository(DocumentNumberCounter) + private counterRepo: Repository, + private dataSource: DataSource, + ) {} + + async incrementCounter(counterKey: CounterKey): Promise { + const MAX_RETRIES = 2; + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + return await this.dataSource.transaction(async (manager) => { + // ใช้ Optimistic Locking + const counter = await manager.findOne(DocumentNumberCounter, { + where: this.buildWhereClause(counterKey), + }); + + if (!counter) { + // สร้าง counter ใหม่ + const newCounter = manager.create(DocumentNumberCounter, { + ...counterKey, + lastNumber: 1, + version: 0, + }); + await manager.save(newCounter); + return 1; + } + + counter.lastNumber += 1; + await manager.save(counter); // Auto-check version + return counter.lastNumber; + }); + } catch (error) { + if (error instanceof OptimisticLockVersionMismatchError) { + this.logger.warn( + `Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`, + ); + if (attempt === MAX_RETRIES - 1) { + throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่'); + } + continue; + } + throw error; + } + } + } + + private buildWhereClause(key: CounterKey) { + return { + projectId: key.projectId, + originatorOrganizationId: key.originatorOrgId, + recipientOrganizationId: key.recipientOrgId, + correspondenceTypeId: key.correspondenceTypeId, + subTypeId: key.subTypeId, + rfaTypeId: key.rfaTypeId, + disciplineId: key.disciplineId, + currentYear: key.year, + }; + } +} +``` + +### 2.5. Main Service with Retry Logic + +```typescript +// File: src/modules/document-numbering/services/document-numbering.service.ts +import { Injectable, ServiceUnavailableException, Logger } from '@nestjs/common'; +import { DocumentNumberingLockService } from './document-numbering-lock.service'; +import { CounterService } from './counter.service'; +import { AuditService } from './audit.service'; +import { RedisConnectionError } from 'ioredis'; + +@Injectable() +export class DocumentNumberingService { + private readonly logger = new Logger(DocumentNumberingService.name); + + constructor( + private lockService: DocumentNumberingLockService, + private counterService: CounterService, + private auditService: AuditService, + ) {} + + async generateDocumentNumber(dto: GenerateNumberDto): Promise { + const startTime = Date.now(); + let lockWaitMs = 0; + let retryCount = 0; + let fallbackUsed = 'NONE'; + + try { + // พยายามใช้ Redis lock ก่อน + return await this.generateWithRedisLock(dto); + } catch (error) { + if (error instanceof RedisConnectionError) { + // Fallback: ใช้ database lock + this.logger.warn('Redis unavailable, falling back to DB lock'); + fallbackUsed = 'DB_LOCK'; + return await this.generateWithDbLock(dto); + } + throw error; + } finally { + // บันทึก audit log + await this.auditService.logGeneration({ + documentId: dto.documentId, + counterKey: dto.counterKey, + lockWaitMs, + totalDurationMs: Date.now() - startTime, + fallbackUsed, + retryCount, + }); + } + } + + private async generateWithRedisLock(dto: GenerateNumberDto): Promise { + const lock = await this.lockService.acquireLock(dto.counterKey); + + try { + const nextNumber = await this.counterService.incrementCounter(dto.counterKey); + return this.formatNumber(dto.template, nextNumber, dto.counterKey); + } finally { + await this.lockService.releaseLock(lock); + } + } + + private async generateWithDbLock(dto: GenerateNumberDto): Promise { + // ใช้ pessimistic lock + // Implementation details... + } + + private formatNumber(template: string, seq: number, key: CounterKey): string { + // Template formatting logic + // Example: `คคง.-สคฉ.3-0001-2568` + return template + .replace('{SEQ:4}', seq.toString().padStart(4, '0')) + .replace('{YEAR:B.E.}', (key.year + 543).toString()); + // ... more replacements + } +} +``` + +## 3. Template Validation + +```typescript +// File: src/modules/document-numbering/validators/template.validator.ts +import { Injectable } from '@nestjs/common'; + +interface ValidationResult { + valid: boolean; + errors: string[]; +} + +@Injectable() +export class TemplateValidator { + private readonly ALLOWED_TOKENS = [ + 'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE', + 'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV', + ]; + + validate(template: string, correspondenceType: string): ValidationResult { + const tokens = this.extractTokens(template); + const errors: string[] = []; + + // ตรวจสอบ Token ที่ไม่รู้จัก + for (const token of tokens) { + if (!this.ALLOWED_TOKENS.includes(token.name)) { + errors.push(`Unknown token: {${token.name}}`); + } + } + + // กฎพิเศษสำหรับแต่ละประเภท + if (correspondenceType === 'RFA') { + if (!tokens.some((t) => t.name === 'PROJECT')) { + errors.push('RFA template ต้องมี {PROJECT}'); + } + if (!tokens.some((t) => t.name === 'DISCIPLINE')) { + errors.push('RFA template ต้องมี {DISCIPLINE}'); + } + } + + if (correspondenceType === 'TRANSMITTAL') { + if (!tokens.some((t) => t.name === 'SUB_TYPE')) { + errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}'); + } + } + + // ทุก template ต้องมี {SEQ} + if (!tokens.some((t) => t.name.startsWith('SEQ'))) { + errors.push('Template ต้องมี {SEQ:n}'); + } + + return { valid: errors.length === 0, errors }; + } + + private extractTokens(template: string) { + const regex = /\{([^}]+)\}/g; + const tokens: Array<{ name: string; full: string }> = []; + let match; + + while ((match = regex.exec(template)) !== null) { + const tokenName = match[1].split(':')[0]; // SEQ:4 → SEQ + tokens.push({ name: tokenName, full: match[1] }); + } + + return tokens; + } +} +``` + +## 4. BullMQ Job for Counter Reset + +```typescript +// File: src/modules/document-numbering/jobs/counter-reset.job.ts +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; + +@Processor('document-numbering') +@Injectable() +export class CounterResetJob extends WorkerHost { + private readonly logger = new Logger(CounterResetJob.name); + + @Cron('0 0 1 1 *') // 1 Jan every year at 00:00 + async handleYearlyReset() { + const newYear = new Date().getFullYear(); + + // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว + // แค่เตรียม counter สำหรับปีใหม่ + this.logger.log(`Year changed to ${newYear}, counters are ready`); + + // สามารถทำ cleanup counter ปีเก่าได้ (optional) + // await this.cleanupOldCounters(newYear - 5); // เก็บ 5 ปี + } + + async process() { + // BullMQ job processing + } +} +``` + +## 5. API Controller + +### 5.1. Main Controller (`/document-numbering`) + +```typescript +// File: src/modules/document-numbering/document-numbering.controller.ts +import { + Controller, Get, Post, Patch, + Body, Param, Query, UseGuards, ParseIntPipe, +} from '@nestjs/common'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; +import { DocumentNumberingService } from './document-numbering.service'; +import { PreviewNumberDto } from './dto/preview-number.dto'; + +@Controller('document-numbering') +@UseGuards(JwtAuthGuard, RbacGuard) +export class DocumentNumberingController { + constructor(private readonly numberingService: DocumentNumberingService) {} + + // --- Logs --- + + @Get('logs/audit') + @RequirePermission('system.view_logs') + getAuditLogs(@Query('limit') limit?: number) { + return this.numberingService.getAuditLogs(limit ? Number(limit) : 100); + } + + @Get('logs/errors') + @RequirePermission('system.view_logs') + getErrorLogs(@Query('limit') limit?: number) { + return this.numberingService.getErrorLogs(limit ? Number(limit) : 100); + } + + // --- Sequences / Counters --- + + @Get('sequences') + @RequirePermission('correspondence.read') + getSequences(@Query('projectId') projectId?: number) { + return this.numberingService.getSequences(projectId ? Number(projectId) : undefined); + } + + @Patch('counters/:id') + @RequirePermission('system.manage_settings') + async updateCounter( + @Param('id', ParseIntPipe) id: number, + @Body('sequence') sequence: number + ) { + return this.numberingService.setCounterValue(id, sequence); + } + + // --- Preview --- + + @Post('preview') + @RequirePermission('correspondence.read') + async previewNumber(@Body() dto: PreviewNumberDto) { + return this.numberingService.previewNumber(dto); + } +} +``` + +### 5.2. Admin Controller (`/admin/document-numbering`) + +```typescript +// File: src/modules/document-numbering/document-numbering-admin.controller.ts +import { + Controller, Get, Post, Delete, Body, Param, Query, + UseGuards, ParseIntPipe, +} from '@nestjs/common'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; +import { DocumentNumberingService } from './document-numbering.service'; + +@Controller('admin/document-numbering') +@UseGuards(JwtAuthGuard, RbacGuard) +export class DocumentNumberingAdminController { + constructor(private readonly service: DocumentNumberingService) {} + + // --- Template Management --- + + @Get('templates') + @RequirePermission('system.manage_settings') + async getTemplates(@Query('projectId') projectId?: number) { + if (projectId) { + return this.service.getTemplatesByProject(projectId); + } + return this.service.getTemplates(); + } + + @Post('templates') + @RequirePermission('system.manage_settings') + async saveTemplate(@Body() dto: any) { + return this.service.saveTemplate(dto); + } + + @Delete('templates/:id') + @RequirePermission('system.manage_settings') + async deleteTemplate(@Param('id', ParseIntPipe) id: number) { + await this.service.deleteTemplate(id); + return { success: true }; + } + + // --- Metrics --- + + @Get('metrics') + @RequirePermission('system.view_logs') + async getMetrics() { + const audit = await this.service.getAuditLogs(50); + const errors = await this.service.getErrorLogs(50); + return { audit, errors }; + } + + // --- Admin Operations --- + + @Post('manual-override') + @RequirePermission('system.manage_settings') + async manualOverride(@Body() dto: { + projectId: number; + correspondenceTypeId: number | null; + year: number; + newValue: number; + }) { + return this.service.manualOverride(dto); + } + + @Post('void-and-replace') + @RequirePermission('system.manage_settings') + async voidAndReplace(@Body() dto: { + documentId: number; + reason: string; + }) { + return this.service.voidAndReplace(dto); + } + + @Post('cancel') + @RequirePermission('system.manage_settings') + async cancelNumber(@Body() dto: { + documentNumber: string; + reason: string; + }) { + return this.service.cancelNumber(dto); + } + + @Post('bulk-import') + @RequirePermission('system.manage_settings') + async bulkImport(@Body() items: any[]) { + return this.service.bulkImport(items); + } +} +``` + +### 5.3. API Endpoints Summary + +| Endpoint | Method | Permission | Description | +| -------------------------------------------- | ------ | ------------------------ | --------------------------------- | +| `/document-numbering/logs/audit` | GET | `system.view_logs` | Get audit logs | +| `/document-numbering/logs/errors` | GET | `system.view_logs` | Get error logs | +| `/document-numbering/sequences` | GET | `correspondence.read` | Get counter sequences | +| `/document-numbering/counters/:id` | PATCH | `system.manage_settings` | Update counter value | +| `/document-numbering/preview` | POST | `correspondence.read` | Preview number without generating | +| `/admin/document-numbering/templates` | GET | `system.manage_settings` | Get all templates | +| `/admin/document-numbering/templates` | POST | `system.manage_settings` | Create/update template | +| `/admin/document-numbering/templates/:id` | DELETE | `system.manage_settings` | Delete template | +| `/admin/document-numbering/metrics` | GET | `system.view_logs` | Get metrics (audit + errors) | +| `/admin/document-numbering/manual-override` | POST | `system.manage_settings` | Override counter value | +| `/admin/document-numbering/void-and-replace` | POST | `system.manage_settings` | Void and replace number | +| `/admin/document-numbering/cancel` | POST | `system.manage_settings` | Cancel a number | +| `/admin/document-numbering/bulk-import` | POST | `system.manage_settings` | Bulk import counters | + +## 6. Module Configuration + +```typescript +// File: src/modules/document-numbering/document-numbering.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bullmq'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { DocumentNumberCounter } from './entities/document-number-counter.entity'; +import { DocumentNumberAudit } from './entities/document-number-audit.entity'; +import { DocumentNumberError } from './entities/document-number-error.entity'; +import { DocumentNumberingService } from './services/document-numbering.service'; +import { DocumentNumberingLockService } from './services/document-numbering-lock.service'; +import { CounterService } from './services/counter.service'; +import { AuditService } from './services/audit.service'; +import { TemplateValidator } from './validators/template.validator'; +import { CounterResetJob } from './jobs/counter-reset.job'; +import { DocumentNumberingController } from './controllers/document-numbering.controller'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + DocumentNumberCounter, + DocumentNumberAudit, + DocumentNumberError, + ]), + BullModule.registerQueue({ + name: 'document-numbering', + }), + ThrottlerModule.forRoot({ + ttl: 60, + limit: 10, + }), + ], + controllers: [DocumentNumberingController], + providers: [ + DocumentNumberingService, + DocumentNumberingLockService, + CounterService, + AuditService, + TemplateValidator, + CounterResetJob, + ], + exports: [DocumentNumberingService], +}) +export class DocumentNumberingModule {} +``` + +## 7. Environment Configuration + +```typescript +// .env.example +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +DB_HOST=localhost +DB_PORT=3306 +DB_USERNAME=lcbp3 +DB_PASSWORD= +DB_DATABASE=lcbp3_db +DB_POOL_SIZE=20 + +# Prometheus +PROMETHEUS_PORT=9090 +``` + +## References + +- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) +- [Backend Guidelines](file:///e:/np-dms/lcbp3/specs/03-implementation/backend-guidelines.md) diff --git a/infrastructure/backup/features.bak b/docs/backup/features.bak similarity index 100% rename from infrastructure/backup/features.bak rename to docs/backup/features.bak diff --git a/infrastructure/backup/lcbp3-v1.5.1-schema.sql b/docs/backup/lcbp3-v1.5.1-schema.sql similarity index 100% rename from infrastructure/backup/lcbp3-v1.5.1-schema.sql rename to docs/backup/lcbp3-v1.5.1-schema.sql diff --git a/infrastructure/backup/lcbp3-v1.5.1-seed-basic.sql b/docs/backup/lcbp3-v1.5.1-seed-basic.sql similarity index 100% rename from infrastructure/backup/lcbp3-v1.5.1-seed-basic.sql rename to docs/backup/lcbp3-v1.5.1-seed-basic.sql diff --git a/infrastructure/backup/lcbp3-v1.5.1-seed-contractdrawing.sql b/docs/backup/lcbp3-v1.5.1-seed-contractdrawing.sql similarity index 100% rename from infrastructure/backup/lcbp3-v1.5.1-seed-contractdrawing.sql rename to docs/backup/lcbp3-v1.5.1-seed-contractdrawing.sql diff --git a/infrastructure/backup/lcbp3-v1.5.1-seed-permissions.sql b/docs/backup/lcbp3-v1.5.1-seed-permissions.sql similarity index 100% rename from infrastructure/backup/lcbp3-v1.5.1-seed-permissions.sql rename to docs/backup/lcbp3-v1.5.1-seed-permissions.sql diff --git a/infrastructure/backup/workflow.bak b/docs/backup/workflow.bak similarity index 100% rename from infrastructure/backup/workflow.bak rename to docs/backup/workflow.bak diff --git a/specs/01-requirements/03.11-document-numbering.md b/specs/01-requirements/03.11-document-numbering.md index ab96423..faf2812 100644 --- a/specs/01-requirements/03.11-document-numbering.md +++ b/specs/01-requirements/03.11-document-numbering.md @@ -2,80 +2,155 @@ --- title: 'Functional Requirements: Document Numbering Management' -version: 1.6.0 +version: 1.6.2 status: draft owner: Nattanin Peancharoen -last_updated: 2025-12-02 +last_updated: 2025-12-17 related: - -- specs/01-requirements/01-objectives.md -- specs/01-requirements/02-architecture.md -- specs/01-requirements/03-functional-requirements.md -- specs/03-implementation/document-numbering.md -- specs/04-operations/document-numbering-operations.md -- specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md - + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03unctional-requirements.md + - specs/03-implementation/document-numbering.md + - specs/04-operations/document-numbering-operations.md + - specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md + - specs/05-decisions/adr-018-document-numbering.md +Clean Version v1.6.2 – Scope of Changes: + - เลือกใช้ Single Numbering System (Option A) + - แก้ Primary Key design ให้ implement ได้จริง + - ปรับ Character Rule เป็น UTF‑8 printable + - Bind Reset Policy ชัดเจน (Yearly reset, RFA no reset) + - เพิ่ม Number State Machine + - เพิ่ม Idempotency Key + - Drawing ใช้ separate counter namespace + - เพิ่ม Formal Token Validation Grammar --- > **📖 เอกสารที่เกี่ยวข้อง** > -> - **Implementation Guide**: [document-numbering.md](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) - รายละเอียดการ implement ด้วย NestJS, TypeORM, Redis -> - **Operations Guide**: [document-numbering-operations.md](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) - Monitoring, Troubleshooting, Maintenance Procedures +> - **Implementation Guide**: [document-numbering.md](file:///d:/nap-dms.lcbp3/specs/03-implementation/document-numbering.md) - รายละเอียดการ implement ด้วย NestJS, TypeORM, Redis +> - **Operations Guide**: [document-numbering-operations.md](file:///d:/nap-dms.lcbp3/specs/04-operations/document-numbering-operations.md) - Monitoring, Troubleshooting, Maintenance Procedures -## 3.11.1. วัตถุประสงค์ +--- -- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติและยืดหยุ่นสูง +## 3.11.1 Overview & วัตถุประสงค์ + +### 3.11.1.1 Purpose + +ระบบ Document Numbering สำหรับสร้างเลขที่เอกสารอัตโนมัติที่มีความเป็นเอกลักษณ์ (unique) และสามารถติดตามได้ (traceable) สำหรับเอกสารทุกประเภทในระบบ LCBP3-DMS + +### 3.11.1.2 Requirements Summary + +- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติ, ที่มีความเป็นเอกลักษณ์ (unique) และยืดหยุ่นสูง - ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร - ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์ - ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย -## 3.11.2. Logic การนับเลข (Counter Logic) +### 3.11.1.3 Scope + +- Auto-generation ของเลขที่เอกสารตามรูปแบบที่กำหนด +- Manual override สำหรับการ import เอกสารเก่า +- Cancelled number handling (ไม่ reuse) +- Void & Replace pattern สำหรับการแทนที่เอกสาร +- Distributed locking เพื่อป้องกัน race condition +- Complete audit trail สำหรับทุก operation + +### 3.11.1.4 Document Types Supported + +- Request for Approvals (RFA) +- Request for Information (RFI) +- Transmittal (TRANSMITTAL) +- Email (EMAIL) +- Instruction (INSTRUCTION) +- Letter (LETTER) +- Memorandum (MEMO) +- Minutes of Meeting (MOM) +- Notice (NOTICE) +- Other (OTHER) +- Contract Drawings (COD) +- Shop Drawings (SHD) +- Circulation Sheets (CIR) + +### 3.11.1.5 Architectural Decision (Updated) +AD-DN-001: Single Source of Truth for Numbering ระบบ เลือกใช้ Option A: + - document_number_counters เป็น Core / Authoritative Counter System + - document_numbering_configs ใช้เฉพาะ: + - Template format + - Permission / policy + - ยกเลิกการใช้ document_numbering_sequences เป็น counter จริง +เหตุผล: ลดความซ้ำซ้อน, ป้องกัน counter mismatch, debug ง่าย, ops ชัดเจน +--- +## 3.11.2 Counter Logic & Reset Policy +### 3.11.2 Counter Logic (Logic การนับเลข) การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร -### Counter Key Components +| Document Type | Reset Policy | +| ------------- | ------------ | +| Correspondence (LETTER, MEMO, RFI, etc.) | Yearly reset | +| Transmittal | Yearly reset | +| RFA | No reset (continuous) | +| Drawing | Separate namespace (see 3.11.8) | + +### 3.11.2.2 Counter Key Fields (Revised) +``` +(project_id, + originator_organization_id, + recipient_organization_id, + correspondence_type_id, + sub_type_id, + rfa_type_id, + discipline_id, + reset_scope) +``` + +* `reset_scope`: + * `YEAR_2025`, `YEAR_2026`, ... + * `NONE` (สำหรับ RFA) + +### 3.11.2.3 Counter Key Components | Component | Required? | Description | Database Source | Default if NULL | | ---------------------------- | ---------------- | ------------------- | --------------------------------------------------------- | --------------- | | `project_id` | ✅ Yes | ID โครงการ | Derived from user context or organization | - | | `originator_organization_id` | ✅ Yes | ID องค์กรผู้ส่ง | `correspondences.originator_id` | - | -| `recipient_organization_id` | Depends on type | ID องค์กรผู้รับหลัก (TO) | `correspondence_recipients` where `recipient_type = 'TO'` | NULL for RFA | -| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | - | +| `recipient_organization_id` | Depends on type | ID องค์กรผู้รับหลัก (TO) | `correspondence_recipients` where `recipient_type = 'TO'` | 0 for RFA | +| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | 0 | | `sub_type_id` | TRANSMITTAL only | ID ประเภทย่อย | `correspondence_sub_types.id` | 0 | | `rfa_type_id` | RFA only | ID ประเภท RFA | `rfa_types.id` | 0 | | `discipline_id` | RFA only | ID สาขางาน | `disciplines.id` | 0 | -| `current_year` | ✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - | +| `reset_scope` | ✅ Yes | ขอบเขต reset | System derived | - | -### Counter Key แยกตามประเภทเอกสาร - -**LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER**: +### 3.11.2.4 Counter Key by Document Type +#### **Global (LETTER / MEMO / RFI / EMAIL / INSTRUCTION / NOTICE / OTHER)**: ``` (project_id, originator_organization_id, recipient_organization_id, - correspondence_type_id, 0, 0, 0, current_year) + correspondence_type_id, 0, 0, 0, 'YEAR_2025') ``` -*หมายเหตุ*: ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id` - -**TRANSMITTAL**: +**หมายเหตุ**: +- ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id` +- ถ้ามีการเพิ่ม correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ +#### **TRANSMITTAL**: ``` (project_id, originator_organization_id, recipient_organization_id, - correspondence_type_id, sub_type_id, 0, 0, current_year) + correspondence_type_id, sub_type_id, 0, 0, 'YEAR_2025') ``` *หมายเหตุ*: ใช้ `sub_type_id` เพิ่มเติม -**RFA**: - +#### **RFA**: ``` -(project_id, originator_organization_id, NULL, - correspondence_type_id, 0, rfa_type_id, discipline_id, current_year) +(project_id, originator_organization_id, 0, + correspondence_type_id, 0, rfa_type_id, discipline_id, 'NONE') ``` -*หมายเหตุ*: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER) +*หมายเหตุ*: +- RFA ไม่ใช้ `recipient_organization_id` (ใช้ 0) เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER) +- ไม่มี yearly reset (`reset_scope = 'NONE'`) -### วิธีการหา project_id +### 3.11.2.5 วิธีการหา project_id เนื่องจาก Template ของ LETTER/TRANSMITTAL ไม่มี `{PROJECT}` token ระบบจะหา `project_id` จาก: @@ -92,26 +167,27 @@ related: - ตรวจสอบว่า organization มีสิทธิ์ใน project นั้น - ตรวจสอบว่า project/contract เป็น active -### Fallback สำหรับค่า NULL +### 3.11.2.6 Fallback สำหรับค่า NULL +- `correspondence_type_id`: ใช้ `0` (ไม่ระบุประเภทเอกสาร) - `discipline_id`: ใช้ `0` (ไม่ระบุสาขางาน) - `sub_type_id`: ใช้ `0` (ไม่มีประเภทย่อย) - `rfa_type_id`: ใช้ `0` (ไม่ระบุประเภท RFA) -- `recipient_organization_id`: ใช้ `NULL` สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL +- `recipient_organization_id`: ใช้ `0` สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL -## 3.11.3. Format Templates by Correspondence Type +--- + +## 3.11.3 Format Templates by Correspondence Type > **📝 หมายเหตุสำคัญ** -> > - Templates ด้านล่างเป็น **ตัวอย่าง** สำหรับประเภทเอกสารหลัก > - ระบบรองรับ **ทุกประเภทเอกสาร** ที่อยู่ใน `correspondence_types` table > - หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ > - Admin สามารถกำหนด Template เฉพาะสำหรับแต่ละประเภทผ่าน Admin Panel -### 3.11.3.1. Letter (TYPE = LETTER) +### 3.11.3.1 Global (correspondence_type_id = defined) **Template**: - ``` {ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.} ``` @@ -131,14 +207,13 @@ related: > - แต่ระบบ**ยังใช้ correspondence_type_id ใน Counter Key** เพื่อแยก counter > - LETTER, MEMO, RFI **มี counter แยกกัน** แม้ template format เหมือนกัน -**Counter Key**: `(project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)` +**Counter Key**: `(project_id, originator_org_id, recipient_org_id, correspondence_type_id, 0, 0, 0, 'YEAR_2025')` --- -### 3.11.3.2. Transmittal (TYPE = TRANSMITTAL) +### 3.11.3.2 Transmittal (TYPE = TRANSMITTAL) **Template**: - ``` {ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.} ``` @@ -153,22 +228,11 @@ related: - `0117` = {SEQ:4} - `2568` = {YEAR:B.E.} -> **⚠️ Template vs Counter Separation** -> -> - {CORR_TYPE} **ไม่แสดง**ใน template (เหมือน LETTER) -> - TRANSMITTAL มี counter แยกจาก LETTER - -**Counter Key**: `(project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)` +**Counter Key**: `(project_id, originator_org_id, recipient_org_id, correspondence_type_id, sub_type_id, 0, 0, 'YEAR_2025')` --- -### 3.11.3.3. RFA (Request for Approval) - -**Template**: - -``` -{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV} -``` +### 3.11.3.3 RFA (Request for Approval) **Example**: `LCBP3-C2-RFA-TER-RPT-0001-A` @@ -187,11 +251,13 @@ related: > - Workflow: **CONTRACTOR → CONSULTANT → OWNER** > - ไม่มี specific `recipient_id` เพราะเป็น workflow ที่กำหนดไว้แล้ว -**Counter Key**: `(project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)` +**Counter Key**: `(project_id, originator_org_id, 0, correspondence_type_id, 0, rfa_type_id, discipline_id, 'NONE')` + --- -### 3.11.3.4. Drawing + +### 3.11.3.4 Drawing **Status**: 🚧 **To Be Determined** @@ -203,30 +269,7 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื --- -### 3.11.3.5. Other Correspondence Types - -**Applicable to**: RFI, MEMO, EMAIL, MOM, INSTRUCTION, NOTICE, OTHER - -**Template**: - -``` -{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.} -``` - -**Example (RFI)**: `คคง.-สคฉ.3-0042-2568` -**Example (MEMO)**: `คคง.-ผรม.1-0001-2568` - -> **🔑 Counter Separation** -> -> - แม้ template format **เหมือนกับ LETTER** -> - แต่แต่ละ type มี **counter แยกกัน** ผ่าน `correspondence_type_id` -> - RFI counter ≠ MEMO counter ≠ LETTER counter - -**Counter Key**: `(project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)` - -**หมายเหตุ**: ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้ ถ้ามีการเพิ่ม correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ - -## 3.11.4. Supported Token Types +## 3.11.4 Supported Token Types | Token | Description | Example | Database Source | | -------------- | ---------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | @@ -238,18 +281,23 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื | `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` | | `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` | | `{SEQ:n}` | Running number (n = จำนวนหลัก) | `0001`, `0029`, `0985` | Based on `document_number_counters.last_number + 1` | -| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `document_number_counters.current_year + 543` | -| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `document_number_counters.current_year` | +| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `reset_scope` + 543 | +| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `reset_scope` | | `{REV}` | Revision Code | `A`, `B`, `AA` | `correspondence_revisions.revision_label` | +| `{PREFIX}` | คำนำหน้าตามประเภทเอกสาร | `COR`, `RFA` | Configurable prefix | +| `{YYYY}` | ปี 4 หลัก | `2025` | Current year | +| `{YY}` | ปี 2 หลัก | `25` | Current year (short) | +| `{MM}` | เดือน 2 หลัก | `01-12` | Current month | +| `{CONTRACT}` | รหัสสัญญา | `C001` | `contracts.contract_code` | ### Token Usage Notes **{SEQ:n}**: -- `n` = จำนวนหลักที่ต้องการ (typically 4) +- `n` = จำนวนหลักที่ต้องการ (typically 4-6) - Counter **เริ่มจาก 0001** และเพิ่มทีละ 1 (0001, 0002, 0003, ...) - Padding ด้วย 0 ทางซ้าย -- Reset ทุกปี (ตาม `current_year` ใน Counter Key) +- Reset ทุกปี (ตาม `reset_scope` ใน Counter Key) **{RECIPIENT}**: @@ -260,7 +308,7 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื **{CORR_TYPE}**: - รองรับทุกค่าจาก `correspondence_types.type_code` -- ถ้าม�การเพิ่มประเภทใหม่ จะใช้งานได้ทันที +- ถ้ามีการเพิ่มประเภทใหม่ จะใช้งานได้ทันที - **แสดงใน template**: RFA only - **ไม่แสดงแต่ใช้ใน counter**: LETTER, TRANSMITTAL, และ Other types @@ -270,830 +318,398 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื - ~~`{TYPE}`~~ → ใช้ `{CORR_TYPE}`, `{SUB_TYPE}`, หรือ `{RFA_TYPE}` แทน (ตามบริบท) - ~~`{CATEGORY}`~~ → ไม่ได้ใช้งานในระบบปัจจุบัน -## 3.11.5. Security & Data Integrity Requirements - -### 3.11.5.1. Concurrency Control - -**Requirements:** - -- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน -- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์ -- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary -- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน - -**Implementation Details:** ดู [Implementation Guide - Section 2.3](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#23-redis-lock-service) - -### 3.11.5.2. Data Integrity - -**Requirements:** - -- ระบบ**ต้อง**ใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates -- ระบบ**ต้อง**มี database constraints เพื่อป้องกันข้อมูลผิดพลาด: - - Unique constraint บน `document_number` - - Foreign key constraints ทุก relationship - - Check constraints สำหรับ business rules - -### 3.11.5.3. Authorization - -**Requirements:** - -- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number -- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ -- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ (requires approval) - -## 3.11.6. Error Handling Requirements - -### 3.11.6.1. Retry Mechanism - -**Requirements:** - -ระบบ**ต้อง**จัดการ error scenarios ต่อไปนี้: - -| Scenario | Strategy | Max Retries | Expected Response | -| ------------------- | ------------------- | ----------- | ------------------------------- | -| Redis Unavailable | Fallback to DB Lock | 0 | Continue (degraded performance) | -| Lock Timeout | Exponential Backoff | 5 | HTTP 503 after final retry | -| Version Conflict | Immediate Retry | 2 | HTTP 409 after final retry | -| DB Connection Error | Exponential Backoff | 3 | HTTP 500 after final retry | - -**Implementation Details:** ดู [Implementation Guide - Section 2.5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#25-main-service-with-retry-logic) - -### 3.11.6.2. User Experience - -**Requirements:** - -- Error messages **ต้อง**เป็นภาษาไทย และเข้าใจง่าย -- HTTP status codes **ต้อง**สื่อความหมายที่ถูกต้อง -- Frontend **ควร**แสดง retry option สำหรับ transient errors - -## 3.11.7. Configuration Management Requirements - -### 3.11.7.1. Template Management - -**Requirements:** - -- Project Admin **ต้อง**สามารถกำหนด/แก้ไข template ผ่าน Admin Panel -- ระบบ**ต้อง**validate template ก่อนบันทึก -- การเปลี่ยนแปลง template **ต้องไม่**ส่งผลต่อเอกสารที่สร้างไว้แล้ว - -### 3.11.7.2. Template Versioning - -**Requirements:** - -- ระบบ**ต้อง**เก็บ history ของ template changes -- ระบบ**ต้อง**บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง -- ระบบ**ควร**สามารถ rollback ไปเวอร์ชันก่อนหน้าได้ - -### 3.11.7.3. Counter Reset Policy - -**Requirements:** - -- Counter **ต้อง**reset ตามปี (อัตโนมัติ) -- Admin **ต้อง**สามารถ manual reset counter ได้ (require approval + audit log) - -**Implementation Details:** ดู [Implementation Guide - Section 4](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#4-bullmq-job-for-counter-reset) - -## 3.11.8. Audit Trail Requirements - -### 3.11.8.1. Audit Logging - -**Requirements:** - -ระบบ**ต้อง**บันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation: - -- `document_id` - เอกสารที่ถูกสร้าง -- `generated_number` - เลขที่ถูกสร้าง -- `counter_key` - key ที่ใช้ในการนับ (JSON format) -- `template_used` - template ที่ใช้ -- `user_id` - ผู้ที่ request -- `ip_address` - IP address ของผู้ request -- `timestamp` - เวลาที่สร้าง -- `retry_count` - จำนวนครั้งที่ retry -- `performance_metrics` - Lock wait time, total duration - -### 3.11.8.2. Error Logging - -**Requirements:** - -- ระบบ**ต้อง**บันทึก error แยกต่างหาก พร้อม error type classification -- ระบบ**ควร**alert ops team สำหรับ critical errors - -### 3.11.8.3. Retention Policy - -**Requirements:** - -- Audit log **ต้อง**เก็บอย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) - -## 3.11.9. Performance Requirements - -### 3.11.9.1. Response Time - -**SLA Targets:** - -| Metric | Target | Notes | -| ---------------- | -------- | ------------------------ | -| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response | -| 99th percentile | ≤ 5 วินาที | รวม retry attempts | -| Normal operation | ≤ 500ms | ไม่มี retry | - -### 3.11.9.2. Throughput - -**Capacity Targets:** - -| Load Level | Target | Notes | -| ----------- | ----------- | --------- | -| Normal load | ≥ 50 req/s | ใช้งานปกติ | -| Peak load | ≥ 100 req/s | ช่วงเร่งงาน | - -### 3.11.9.3. Availability - -**SLA Targets:** - -- **Uptime**: ≥ 99.5% (excluding planned maintenance) -- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน -- **RTO**: ≤ 30 นาที -- **RPO**: ≤ 5 นาที - -**Operations Details:** ดู [Operations Guide - Section 1](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#1-performance-requirements) - -## 3.11.10. Monitoring & Alerting Requirements - -### 3.11.10.1. Metrics - -**Requirements:** - -ระบบ**ต้อง**collect metrics ต่อไปนี้: - -- Lock acquisition time (p50, p95, p99) -- Lock acquisition success/failure rate -- Counter generation latency -- Retry count distribution -- Redis connection status -- Database connection pool usage - -### 3.11.10.2. Alerts - -**Requirements:** - -ระบบ**ต้อง**alert สำหรับ conditions ต่อไปนี้: - -| Severity | Condition | Action | -| ---------- | ---------------------------- | ----------------- | -| 🔴 Critical | Redis unavailable > 1 minute | PagerDuty + Slack | -| 🔴 Critical | Lock failures > 10% in 5 min | PagerDuty + Slack | -| 🟡 Warning | Lock failures > 5% in 5 min | Slack | -| 🟡 Warning | Avg lock wait time > 1 sec | Slack | -| 🟡 Warning | Retry count > 100/hour | Slack | - -### 3.11.10.3. Dashboard - -**Requirements:** - -- Ops team **ต้อง**มี real-time dashboard แสดง: - - Lock acquisition success rate - - Lock wait time percentiles - - Generation rate (per minute) - - Error rate by type - - Connection health status - -**Operations Details:** ดู [Operations Guide - Section 3](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#3-monitoring--metrics) - - -## 3.11.11. API Reference - -เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้: - -### Document Number Generation - -```http -POST /api/v1/documents/{documentId}/generate-number -``` - -สร้างเลขที่เอกสารสำหรับ document ที่ระบุ - -**Request Body:** - -```json -{ - "counterKey": { - "projectId": 2, - "originatorOrgId": 22, - "recipientOrgId": 10, - "correspondenceTypeId": 6, - "subTypeId": 0, - "rfaTypeId": 0, - "disciplineId": 0, - "year": 2025 - } -} -``` - -**Response:** - -```json -{ - "documentNumber": "คคง.-สคฉ.3-0001-2568", - "generatedAt": "2025-12-02T15:30:00Z" -} -``` - -### Template Management - -```http -GET /api/v1/document-numbering/configs -``` - -ดูรายการ template configuration ทั้งหมด - -```http -PUT /api/v1/document-numbering/configs/{configId} -``` - -แก้ไข template (Project Admin only) - -```http -POST /api/v1/document-numbering/configs/{configId}/reset-counter -``` - -Reset counter (Super Admin only, requires approval) - -**รายละเอียดเพิ่มเติม:** ดู [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md) - -## 3.11.12. Database Schema Reference - -เอกสารนี้อ้างอิงถึง database tables ต่อไปนี้: - -### Core Tables - -- `document_number_counters` - เก็บ counter values และ template configuration -- `document_number_audit` - เก็บ audit trail ของการ generate เลขที่ -- `document_number_errors` - เก็บ error logs - -### Related Tables - -- `documents` - เก็บ document number ที่ถูกสร้าง (column: `document_number` UNIQUE) -- `correspondence_types` - ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.) -- `correspondence_sub_types` - ประเภทย่อย (สำหรับ TRANSMITTAL) -- `rfa_types` - ประเภท RFA (SHD, RPT, MAT, etc.) -- `disciplines` - สาขาวิชา (TER, STR, GEO, etc.) -- `projects` - โครงการ -- `organizations` - องค์กร - -**Schema Details:** ดู [Implementation Guide - Section 1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#1-database-implementation) - -## 3.11.13. Database Schema Requirements - -### 3.11.13.1. Counter Table Schema Requirements - -**Primary Table**: `document_number_counters` - -**Required Columns:** -- Composite primary key: `(project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)` -- `version` - สำหรับ optimistic locking -- `last_number` - counter value (เริ่มจาก 0) - -**Important Notes:** -- ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL -- Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน) -- ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน - -**Schema Details:** ดู [Implementation Guide - Section 1.1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#11-counter-table-schema) - -### 3.11.13.2. Audit Table Requirements - -**Primary Table**: `document_number_audit` - -**Required Columns:** -- `document_id`, `generated_number`, `counter_key` (JSON) -- `template_used`, `user_id`, `ip_address` -- Performance metrics: `retry_count`, `lock_wait_ms`, `total_duration_ms` -- `fallback_used` - tracking fallback scenarios - -**Retention:** ≥ 7 ปี - -### 3.11.13.3. Error Log Requirements - -**Primary Table**: `document_number_errors` - -**Required Columns:** -- `error_type` - ENUM classification -- `error_message`, `stack_trace`, `context_data` (JSON) -- `user_id`, `ip_address`, `created_at`, `resolved_at` - -## 3.11.14. Security Considerations - -### 3.11.14.1. Authorization - -- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number -- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ -- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ - -### 3.11.14.2. Rate Limiting - -**Requirements:** -- Limit ต่อ user: **10 requests/minute** (prevent abuse) -- Limit ต่อ IP: **50 requests/minute** - -**Implementation Details:** ดู [Implementation Guide - Section 5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#5-api-controller) - -### 3.11.14.3. Audit & Compliance - -**Requirements:** -- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering -- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) -- Audit log **ต้องไม่**สามารถแก้ไขได้ (immutable) +--- + +## 3.11.5 Character & Format Rules (Updated) + +### BR-DN-002 (Revised) + +* Document number **must be printable UTF‑8** +* Disallowed: + * Control characters + * Newlines / tabs +* Allowed: + * Thai + * English + * Numbers + * `-`, `_`, `.` + +### BR-DN-003: Number Format Rules +- Min length: 10 characters +- Max length: 50 characters +- Must include {SEQ:n} token exactly once --- -## References +## 3.11.6 Number State Machine (New) -- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) -- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) -- [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md) -- [Data Dictionary](file:///e:/np-dms/lcbp3/specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md) +### States - -``` -lock:docnum:{project_id}:{org_id}:{recip_id}:{type_id}:{sub}:{rfa}:{disc}:{year} +```text +RESERVED → CONFIRMED → VOID + ↘ CANCELLED ``` -**Lock Configuration**: +### Rules -- **TTL**: 5 วินาที (auto-release เมื่อ timeout) -- **Acquisition Timeout**: 10 วินาที -- **Retry Delay**: 100ms (exponential backoff) -- **Drift Factor**: 0.01 (Redlock algorithm) +* **RESERVED**: + * TTL 5 minutes + * Auto-expire → CANCELLED +* **CONFIRMED**: + * Linked to document_id +* **VOID**: + * Only CONFIRMED numbers + * Replacement creates new number -**Implementation (NestJS)**: +--- -```typescript -// src/document-numbering/services/document-numbering-lock.service.ts -import Redlock from 'redlock'; -import { Injectable } from '@nestjs/common'; +## 3.11.7 Idempotency (New) -@Injectable() -export class DocumentNumberingLockService { - private redlock: Redlock; +### API Requirement - async acquireLock(counterKey: CounterKey): Promise { - const lockKey = this.buildLockKey(counterKey); - return await this.redlock.acquire([lockKey], 5000); // 5s TTL - } +* All number generation APIs **must** support: - private buildLockKey(key: CounterKey): string { - return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + - `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + - `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; - } -} +```http +Idempotency-Key: UUID ``` -### 3.11.5.2. Optimistic Locking +### Behavior -ใช้ **TypeORM Optimistic Lock** ร่วมกับ `@Version()` decorator: +* Same key + same payload → return same number +* Prevents double submit / retry duplication -**Entity Definition**: +--- -```typescript -// src/document-numbering/entities/document-number-counter.entity.ts -import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm'; +## 3.11.8 Drawing Numbering (Clarified) -@Entity('document_number_counters') -export class DocumentNumberCounter { - @PrimaryColumn({ name: 'project_id' }) - projectId: number; +* Drawing numbering **does not use** this counter table +* Uses **separate counter namespace**: - @PrimaryColumn({ name: 'originator_organization_id' }) - originatorOrganizationId: number; - - @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) - recipientOrganizationId: number | null; - - @PrimaryColumn({ name: 'correspondence_type_id' }) - correspondenceTypeId: number; - - @PrimaryColumn({ name: 'sub_type_id', default: 0 }) - subTypeId: number; - - @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) - rfaTypeId: number; - - @PrimaryColumn({ name: 'discipline_id', default: 0 }) - disciplineId: number; - - @PrimaryColumn({ name: 'current_year' }) - currentYear: number; - - @VersionColumn({ name: 'version' }) - version: number; - - @Column({ name: 'last_number', default: 0 }) - lastNumber: number; -} +```text +DRAWING:::: ``` -**Transaction Handling**: +* Prevents collision with correspondence/RFA -```typescript -// ใช้ TypeORM Transaction + Optimistic Lock -await this.connection.transaction(async (manager) => { - const counter = await manager.findOne(DocumentNumberCounter, { - where: counterKey - }); +--- - counter.lastNumber += 1; - await manager.save(counter); // auto-check version -}); +## 3.11.9 Token Validation Grammar (New) + +### EBNF + +```ebnf +TEMPLATE := TOKEN ("-" TOKEN)* +TOKEN := SIMPLE | PARAM +SIMPLE := "{PROJECT}" | "{ORIGINATOR}" | "{RECIPIENT}" | + "{CORR_TYPE}" | "{DISCIPLINE}" | "{RFA_TYPE}" | + "{REV}" | "{YYYY}" | "{YY}" | "{MM}" +PARAM := "{SEQ:" DIGIT+ "}" +DIGIT := "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ``` -หาก version conflict → TypeORM throw `OptimisticLockVersionMismatchError` → retry +### Validation Rules -### 3.11.5.3. Database Constraints +* Must include `{SEQ:n}` exactly once +* Unknown tokens → validation error +* Max template length: 50 chars -**Unique Constraints**: +--- + +## 3.11.10 Functional Requirements + +### 3.11.10.1 Auto Number Generation + +#### FR-DN-001: Generate Sequential Number +**Priority**: CRITICAL | **Status**: Required + +**Description**: +ระบบต้องสามารถสร้างเลขที่เอกสารอัตโนมัติตามลำดับ (sequential) โดยไม่ซ้ำกัน + +**Acceptance Criteria**: +- เลขที่เอกสารต้องเป็น unique ในscope ที่กำหนด +- ต้องเพิ่มขึ้นทีละ 1 (increment by 1) +- ต้องรองรับ concurrent requests โดยไม่มีเลขที่ซ้ำ +- Response time < 100ms (p95) + +--- + +#### FR-DN-002: Configurable Number Format +**Priority**: HIGH | **Status**: Required + +**Description**: +ระบบต้องรองรับการกำหนดรูปแบบเลขที่เอกสารที่หลากหลาย + +**Acceptance Criteria**: +- รองรับ format tokens ที่ระบุ +- Admin สามารถกำหนด format ผ่าน UI ได้ +- Validate format ก่อน save +- แสดง preview ของเลขที่ที่จะถูกสร้าง + +--- + +#### FR-DN-003: Scope-based Sequences +**Priority**: HIGH | **Status**: Required + +**Description**: +ระบบต้องรองรับการสร้าง sequence ที่แยกตาม scope ที่ต่างกัน + +**Acceptance Criteria**: +- เลขที่ไม่ซ้ำภายใน scope เดียวกัน +- Scope ที่ต่างกันสามารถมีเลขที่เดียวกันได้ +- Support multiple active scopes + +--- + +### 3.11.10.2 Manual Override + +#### FR-DN-004: Manual Number Assignment +**Priority**: HIGH | **Status**: Required + +**Description**: +ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override) + +**Use Cases**: +1. Import เอกสารเก่าจากระบบเดิม +2. External documents จาก client/consultant +3. Correction หลังพบความผิดพลาด + +**Acceptance Criteria**: +- ตรวจสอบ duplicate ก่อน save +- Validate format ตามรูปแบบที่กำหนด +- Auto-update sequence counter ถ้าเลขที่สูงกว่า current +- บันทึก audit log ว่าเป็น manual override +- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น + +--- + +#### FR-DN-005: Bulk Import Support +**Priority**: MEDIUM | **Status**: Required + +**Description**: +ระบบต้องรองรับการ import เอกสารหลายรายการพร้อมกัน + +**Acceptance Criteria**: +- รองรับไฟล์ CSV/Excel +- Validate ทุกรายการก่อน import +- แสดง preview ก่อน confirm +- Rollback ทั้งหมดถ้ามีรายการใดผิดพลาด (transactional) +- Auto-update sequence counters หลัง import +- Generate import report + +--- + +### 3.11.10.3 Cancelled & Void Handling + +#### FR-DN-006: Skip Cancelled Numbers +**Priority**: HIGH | **Status**: Required + +**Description**: +เลขที่เอกสารที่ถูกยกเลิกต้องไม่ถูก reuse + +**Rationale**: +- รักษา audit trail ที่ชัดเจน +- ป้องกันความสับสน +- Legal compliance + +**Acceptance Criteria**: +- Cancelled number ยังคงอยู่ในฐานข้อมูลพร้อม status +- ระบบข้าม (skip) cancelled number เมื่อสร้างเลขที่ใหม่ +- บันทึกเหตุผลการยกเลิก +- แสดง cancelled numbers ใน audit trail + +--- + +#### FR-DN-007: Void and Replace +**Priority**: HIGH | **Status**: Required + +**Description**: +ระบบต้องรองรับการ void เอกสารและสร้างเอกสารใหม่แทน + +**Workflow**: +1. User เลือกเอกสารที่ต้องการ void +2. ระบุเหตุผล (required) +3. ระบบเปลี่ยน status เอกสารเดิมเป็น VOID +4. สร้างเอกสารใหม่ด้วยเลขที่ใหม่ +5. Link เอกสารใหม่กับเดิม (voided_from_id) + +**Acceptance Criteria**: +- เอกสารเดิม status = VOID (ไม่ลบ) +- เอกสารใหม่ได้เลขที่ต่อเนื่องจาก sequence +- มี reference link ระหว่างเอกสาร +- บันทึก void reason +- แสดง void history chain (A→B→C) + +--- + +### 3.11.10.4 Concurrency & Performance + +#### FR-DN-008: Prevent Race Conditions +**Priority**: CRITICAL | **Status**: Required + +**Description**: +ระบบต้องป้องกันการสร้างเลขที่ซ้ำเมื่อมีการ request พร้อมกัน + +**Solution**: +- Distributed locking (Redlock) +- Database pessimistic locking +- Two-phase commit pattern + +**Acceptance Criteria**: +- Zero duplicate numbers ภายใต้ concurrent load (1000 req/s) +- Lock acquisition time < 50ms (avg) +- Automatic retry on lock failure (max 3 times) +- Timeout handling (30 seconds) + +--- + +#### FR-DN-009: Two-Phase Commit +**Priority**: HIGH | **Status**: Required + +**Description**: +ใช้ Two-phase commit pattern เพื่อความสมบูรณ์ของข้อมูล + +**Phase 1: Reserve** +- ล็อกเลขที่และ reserve ไว้ชั่วคราว +- Set TTL 5 นาที +- Return reservation token + +**Phase2: Confirm or Cancel** +- Confirm: บันทึกลงฐานข้อมูลถาวร +- Cancel: คืน lock และ reservation + +**Acceptance Criteria**: +- Reservation ต้อง expire หลัง 5 นาที +- Auto-cleanup expired reservations +- Support explicit cancel +- Idempotent confirmation + +--- +### 3.11.10.5 Monitoring & Audit + +#### FR-DN-010: Complete Audit Trail +**Priority**: HIGH | **Status**: Required + +**Description**: +บันทึกทุก operation ที่เกิดขึ้นกับเลขที่เอกสาร + +**Events to Log**: +- Number reserved +- Number confirmed +- Number cancelled +- Manual override +- Void document +- Sequence adjusted +- Format changed + +**Acceptance Criteria**: +- Log ทุก operation +- Searchable by user, date, type +- Export to CSV +- Retain for 7 years + +--- + +#### FR-DN-011: Metrics & Alerting +**Priority**: MEDIUM | **Status**: Required + +**Description**: +แสดงสถิติและส่ง alert เมื่อเกิดปัญหา + +**Metrics**: +- Sequence utilization (% of max) +- Average lock wait time +- Failed lock attempts +- Numbers generated per day +- Manual overrides per day + +**Alerts**: +- Sequence >90% used (WARNING) +- Sequence >95% used (CRITICAL) +- Lock wait time >1s (WARNING) +- Redis unavailable (CRITICAL) +- High error rate (WARNING) + +--- + +## 3.11.11 Database Schema (Corrected) + +### 3.11.11.1 document_number_counters ```sql --- บน documents table -ALTER TABLE documents -ADD CONSTRAINT uq_document_number UNIQUE (document_number); +CREATE TABLE document_number_counters ( + project_id INT NOT NULL, + originator_organization_id INT NOT NULL, + recipient_organization_id INT NOT NULL DEFAULT 0, -- 0 = no recipient (RFA) + correspondence_type_id INT NOT NULL, + sub_type_id INT DEFAULT 0, + rfa_type_id INT DEFAULT 0, + discipline_id INT DEFAULT 0, + reset_scope VARCHAR(20) NOT NULL, + last_number INT DEFAULT 0 NOT NULL, + version INT DEFAULT 0 NOT NULL, + + PRIMARY KEY ( + project_id, + originator_organization_id, + recipient_organization_id, + correspondence_type_id, + sub_type_id, + rfa_type_id, + discipline_id, + reset_scope + ), + + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci + COMMENT = 'ตารางเก็บ Running Number Counters'; ``` -**Foreign Key Constraints**: +### Rules -- `project_id` → `projects(id)` ON DELETE CASCADE -- `originator_organization_id` → `organizations(id)` ON DELETE CASCADE -- `recipient_organization_id` → `organizations(id)` ON DELETE CASCADE -- `correspondence_type_id` → `correspondence_types(id)` ON DELETE CASCADE +* RFA → `recipient_organization_id = 0` +* Reset: + * Yearly: `reset_scope = 'YEAR_2025'` + * No reset: `reset_scope = 'NONE'` -**Check Constraints**: +### 3.11.11.2 Index Requirements ```sql --- ตรวจสอบว่า last_number ≥ 0 -ALTER TABLE document_number_counters -ADD CONSTRAINT chk_last_number_positive CHECK (last_number >= 0); +-- Index สำหรับ Performance +CREATE INDEX idx_counter_lookup +ON document_number_counters ( + project_id, + correspondence_type_id, + reset_scope +); --- ตรวจสอบว่า current_year เป็นปี ค.ศ. ที่สมเหตุสมผล -ALTER TABLE document_number_counters -ADD CONSTRAINT chk_current_year_valid -CHECK (current_year BETWEEN 2020 AND 2100); +-- Index สำหรับ Originator lookup +CREATE INDEX idx_counter_org +ON document_number_counters ( + originator_organization_id, + reset_scope +); ``` -## 3.11.6. Retry Mechanism & Error Handling - -### 3.11.6.1. Scenario 1: Redis Unavailable - -**Fallback Strategy**: Database-only Pessimistic Locking - -**Implementation**: - -```typescript -// src/document-numbering/services/document-numbering.service.ts -@Injectable() -export class DocumentNumberingService { - async generateDocumentNumber(dto: GenerateNumberDto): Promise { - try { - // พยายามใช้ Redis lock ก่อน - return await this.generateWithRedisLock(dto); - } catch (error) { - if (error instanceof RedisConnectionError) { - // Fallback: ใช้ database lock - this.logger.warn('Redis unavailable, falling back to DB lock'); - await this.alertOpsTeam('redis_unavailable'); - return await this.generateWithDbLock(dto); - } - throw error; - } - } - - private async generateWithDbLock(dto: GenerateNumberDto): Promise { - return await this.connection.transaction(async (manager) => { - // SELECT ... FOR UPDATE = Pessimistic Lock - const counter = await manager - .createQueryBuilder(DocumentNumberCounter, 'c') - .where(counterKeyCondition) - .setLock('pessimistic_write') - .getOne(); - - counter.lastNumber += 1; - await manager.save(counter); - return this.formatNumber(counter); - }); - } -} -``` - -**Monitoring**: - -- Log warning พร้อม context (project_id, user_id, timestamp) -- Alert Ops Team ผ่าน Slack/Email -- ระบบยังใช้งานได้แต่ performance อาจลดลง 30-50% - -### 3.11.6.2. Scenario 2: Lock Acquisition Timeout - -**Retry Strategy**: Exponential Backoff with Jitter - -```typescript -// ใช้ @nestjs/common Retry Decorator หรือ custom retry logic -import { retry } from 'rxjs/operators'; - -const RETRY_CONFIG = { - maxRetries: 5, - delays: [1000, 2000, 4000, 8000, 16000], // exponential backoff - jitter: 0.1 // เพิ่ม randomness ป้องกัน thundering herd -}; - -async acquireLockWithRetry(key: CounterKey): Promise { - for (let i = 0; i < RETRY_CONFIG.maxRetries; i++) { - try { - return await this.lockService.acquireLock(key); - } catch (error) { - if (i === RETRY_CONFIG.maxRetries - 1) { - throw new ServiceUnavailableException( - 'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง' - ); - } - const delay = RETRY_CONFIG.delays[i]; - const jitter = delay * RETRY_CONFIG.jitter * Math.random(); - await this.sleep(delay + jitter); - } - } -} -``` - -**Response**: - -- HTTP Status: `503 Service Temporarily Unavailable` -- Response Body: - - ```json - { - "statusCode": 503, - "message": "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง", - "error": "Service Unavailable", - "retryAfter": 30 - } - ``` - -### 3.11.6.3. Scenario 3: Version Conflict (Optimistic Lock) - -**Retry Strategy**: Immediate Retry (2 attempts) - -```typescript -async incrementCounter(counterKey: CounterKey): Promise { - const MAX_RETRIES = 2; - - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { - try { - return await this.connection.transaction(async (manager) => { - const counter = await manager.findOne( - DocumentNumberCounter, - { where: counterKey } - ); - - counter.lastNumber += 1; - await manager.save(counter); // Version check ที่นี่ - return counter.lastNumber; - }); - } catch (error) { - if (error instanceof OptimisticLockVersionMismatchError) { - this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`); - if (attempt === MAX_RETRIES - 1) { - throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่'); - } - // Retry ทันที (ไม่มี delay) - continue; - } - throw error; - } - } -} -``` - -**Response**: - -- HTTP Status: `409 Conflict` -- Frontend Action: Auto-retry หรือแสดง toast notification - -### 3.11.6.4. Scenario 4: Database Connection Error - -**Retry Strategy**: Exponential Backoff (3 attempts) - -```typescript -const DB_RETRY_CONFIG = { - maxRetries: 3, - delays: [1000, 2000, 4000] -}; - -// TypeORM connection retry (กำหนดใน ormconfig) -{ - type: 'mysql', - extra: { - connectionLimit: 10, - acquireTimeout: 10000, - // Retry connection 3 ครั้ง - retryAttempts: 3, - retryDelay: 1000 - } -} -``` - -**Response**: - -- HTTP Status: `500 Internal Server Error` -- Response Body: - - ```json - { - "statusCode": 500, - "message": "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ", - "error": "Internal Server Error", - "ref": "ERR-20250102-1234-ABCD" - } - ``` - -- Alerting: ส่ง PagerDuty/Slack alert ทันที (severity: CRITICAL) - -## 3.11.7. Configuration Management - -### 3.11.7.1. Admin Panel Configuration - -**Features**: - -- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Web UI -- Preview document number ก่อนบันทึก -- Template validation แบบ real-time - -**Template Validation Logic**: - -```typescript -// src/document-numbering/validators/template.validator.ts -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class TemplateValidator { - private readonly ALLOWED_TOKENS = [ - 'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE', - 'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV' - ]; - - validate(template: string, correspondenceType: string): ValidationResult { - const tokens = this.extractTokens(template); - const errors: string[] = []; - - // ตรวจสอบ Token ที่ไม่รู้จัก - for (const token of tokens) { - if (!this.ALLOWED_TOKENS.includes(token.name)) { - errors.push(`Unknown token: {${token.name}}`); - } - } - - // กฎพิเศษสำหรับแต่ละประเภท - if (correspondenceType === 'RFA') { - if (!tokens.some(t => t.name === 'PROJECT')) { - errors.push('RFA template ต้องมี {PROJECT}'); - } - } - - if (correspondenceType === 'TRANSMITTAL') { - if (!tokens.some(t => t.name === 'SUB_TYPE')) { - errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}'); - } - } - - return { valid: errors.length === 0, errors }; - } -} -``` - -**API Endpoint**: - -```typescript -// PUT /api/v1/document-numbering/configs/:configId -@Put('configs/:configId') -@Roles('PROJECT_ADMIN') -async updateTemplate( - @Param('configId') configId: number, - @Body() dto: UpdateTemplateDto -): Promise { - // Validate template - const validation = await this.templateValidator.validate( - dto.template, - dto.correspondenceType - ); - - if (!validation.valid) { - throw new BadRequestException(validation.errors); - } - - // บันทึก template (ไม่ส่งผลต่อเอกสารที่สร้างแล้ว) - return await this.configService.update(configId, dto); -} -``` - -### 3.11.7.2. Template Versioning - -**Database Table**: `document_number_config_history` +### 3.11.11.3 Numbering Configuration Table ```sql -CREATE TABLE document_number_config_history ( - id INT AUTO_INCREMENT PRIMARY KEY, - config_id INT NOT NULL, - template_before TEXT, - template_after TEXT NOT NULL, - changed_by INT NOT NULL, - changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - change_reason TEXT, - - FOREIGN KEY (config_id) REFERENCES document_number_configs(id), - FOREIGN KEY (changed_by) REFERENCES users(id) -) ENGINE=InnoDB COMMENT='Template Change History'; +CREATE TABLE document_numbering_configs ( + id INT PRIMARY KEY AUTO_INCREMENT, + document_type VARCHAR(50) NOT NULL, + format VARCHAR(200) NOT NULL, + scope ENUM('GLOBAL','PROJECT','CONTRACT','YEARLY','MONTHLY'), + allow_manual_override BOOLEAN DEFAULT FALSE, + max_value INT DEFAULT 999999, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY (document_type, scope) +); ``` -**Audit Trail Implementation**: - -```typescript -@Injectable() -export class ConfigHistoryService { - async recordChange( - configId: number, - oldTemplate: string, - newTemplate: string, - userId: number, - reason: string - ): Promise { - await this.historyRepo.save({ - configId, - templateBefore: oldTemplate, - templateAfter: newTemplate, - changedBy: userId, - changeReason: reason - }); - } - - async rollback(configId: number, historyId: number): Promise { - const history = await this.historyRepo.findOne({ where: { id: historyId }}); - await this.configService.update(configId, { - template: history.templateBefore - }); - } -} -``` - -### 3.11.7.3. Counter Reset Policy - -**Automatic Reset**: - -- **Yearly Reset**: ทุกวันที่ 1 มกราคม (00:00:00 ICT) - - ใช้ **BullMQ Cron Job**: - - ```typescript - // src/document-numbering/jobs/counter-reset.job.ts - @Processor('document-numbering') - export class CounterResetJob { - @Cron('0 0 1 1 *') // 1 Jan every year - async handleYearlyReset() { - const newYear = new Date().getFullYear(); - - // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว - // แค่เตรียม counter สำหรับปีใหม่ - this.logger.log(`Year changed to ${newYear}, counters ready`); - } - } - ``` - -**Manual Reset** (Admin only): - -```typescript -// POST /api/v1/document-numbering/configs/:configId/reset-counter -@Post('configs/:configId/reset-counter') -@Roles('SUPER_ADMIN') -@RequireApproval() // Custom decorator: ต้อง approve จาก 2 admins -async resetCounter( - @Param('configId') configId: number, - @Body() dto: ResetCounterDto -): Promise { - // Validate reason - if (!dto.reason || dto.reason.length < 20) { - throw new BadRequestException('ต้องระบุเหตุผลอย่างน้อย 20 ตัวอักษร'); - } - - // Audit log - await this.auditService.logCounterReset({ - configId, - userId: req.user.id, - reason: dto.reason, - previousValue: counter.lastNumber - }); - - // Reset - await this.counterService.reset(configId); -} - -## 3.11.8. Audit Trail - -### 3.11.8.1. การบันทึก Audit Log - -**Database Table**: `document_number_audit` +### 3.11.11.4 Audit Log Table ```sql CREATE TABLE document_number_audit ( @@ -1121,76 +737,7 @@ CREATE TABLE document_number_audit ( ) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail'; ``` -**Audit Service Implementation**: - -```typescript -// src/document-numbering/services/audit.service.ts -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -@Injectable() -export class DocumentNumberAuditService { - async logGeneration(data: AuditLogData): Promise { - await this.auditRepo.save({ - documentId: data.documentId, - generatedNumber: data.number, - counterKey: JSON.stringify(data.counterKey), - templateUsed: data.template, - userId: data.userId, - ipAddress: data.ipAddress, - userAgent: data.userAgent, - retryCount: data.retryCount ?? 0, - lockWaitMs: data.lockWaitMs, - totalDurationMs: data.totalDurationMs, - fallbackUsed: data.fallbackUsed ?? 'NONE' - }); - } -} -``` - -**Usage in Service**: - -```typescript -@Injectable() -export class DocumentNumberingService { - async generateDocumentNumber(dto: GenerateNumberDto, req: Request) { - const startTime = Date.now(); - let lockWaitMs = 0; - let retryCount = 0; - let fallbackUsed = 'NONE'; - - try { - // ... generate logic ... - const number = await this.doGenerate(dto); - - // Audit log - await this.auditService.logGeneration({ - documentId: dto.documentId, - number, - counterKey: dto.counterKey, - template: config.template, - userId: req.user.id, - ipAddress: req.ip, - userAgent: req.headers['user-agent'], - retryCount, - lockWaitMs, - totalDurationMs: Date.now() - startTime, - fallbackUsed - }); - - return number; - } catch (error) { - // Log error separately - await this.errorLogService.log(error, dto); - throw error; - } - } -} -``` - -### 3.11.8.2. Conflict & Error Logging - -**Separate Error Log Table**: `document_number_errors` +### 3.11.11.5 Error Log Table ```sql CREATE TABLE document_number_errors ( @@ -1216,637 +763,523 @@ CREATE TABLE document_number_errors ( ) ENGINE=InnoDB COMMENT='Document Numbering Error Log'; ``` -**Error Logging Service**: - -```typescript -@Injectable() -export class ErrorLogService { - async log(error: Error, context: any): Promise { - const errorType = this.classifyError(error); - - await this.errorRepo.save({ - errorType, - errorMessage: error.message, - stackTrace: error.stack, - contextData: JSON.stringify(context), - userId: context.userId, - ipAddress: context.ipAddress - }); - - // Alert if critical - if (this.isCritical(errorType)) { - await this.alertService.sendAlert({ - severity: 'CRITICAL', - title: `Document Numbering Error: ${errorType}`, - details: error.message - }); - } - } - - private classifyError(error: Error): string { - if (error instanceof LockTimeoutError) return 'LOCK_TIMEOUT'; - if (error instanceof OptimisticLockVersionMismatchError) return 'VERSION_CONFLICT'; - if (error instanceof QueryFailedError) return 'DB_ERROR'; - if (error instanceof RedisConnectionError) return 'REDIS_ERROR'; - return 'UNKNOWN'; - } -} - -## 3.11.9. Performance Requirements - -### 3.11.9.1. Response Time - -**Target Response Times**: -- **95th percentile**: ≤ 2 วินาที -- **99th percentile**: ≤ 5 วินาที -- **Normal operation** (ไม่มี retry): ≤ 500ms - -**Performance Optimization Strategies**: - -```typescript -// 1. Database Connection Pooling -{ - type: 'mysql', - extra: { - connectionLimit: 20, // Pool size - queueLimit: 0, // Unlimited queue - acquireTimeout: 10000 // 10s timeout - } -} - -// 2. Redis Connection Pooling -import IORedis from 'ioredis'; - -const redis = new IORedis({ - host: process.env.REDIS_HOST, - port: parseInt(process.env.REDIS_PORT), - maxRetriesPerRequest: 3, - enableReadyCheck: true, - lazyConnect: false, - // Connection pool - poolSize: 10 -}); - -// 3. Query Optimization -// ใช้ Index-covered queries -const counter = await this.counterRepo - .createQueryBuilder('c') - .where('c.project_id = :projectId', { projectId }) - .andWhere('c.correspondence_type_id = :typeId', { typeId }) - .andWhere('c.current_year = :year', { year }) - .useIndex('idx_counter_lookup') // Force index usage - .getOne(); -``` - -**Performance Monitoring**: - -```typescript -// Prometheus metrics -import { Counter, Histogram } from 'prom-client'; - -const generationDuration = new Histogram({ - name: 'docnum_generation_duration_seconds', - help: 'Document number generation duration', - labelNames: ['project', 'type', 'status'], - buckets: [0.1, 0.5, 1, 2, 5, 10] -}); - -// Usage -const timer = generationDuration.startTimer(); -try { - const number = await this.generate(dto); - timer({ status: 'success' }); -} catch (error) { - timer({ status: 'error' }); - throw error; -} -``` - -### 3.11.9.2. Throughput - -**Capacity Requirements**: - -- **Normal load**: ≥ 50 requests/second -- **Peak load**: ≥ 100 requests/second (ช่วงเร่งงาน) -- **Burst capacity**: ≥ 200 requests/second (short duration) - -**Load Balancing Strategy**: - -```yaml -# docker-compose.yml -services: - backend: - image: lcbp3-backend:latest - deploy: - replicas: 3 # 3 instances - resources: - limits: - cpus: '1.0' - memory: 1G - reservations: - cpus: '0.5' - memory: 512M - - nginx: - image: nginx:alpine - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf - ports: - - "80:80" -``` - -```nginx -# nginx.conf - Load Balancing Configuration -upstream backend { - least_conn; # Least connections algorithm - server backend:3000 max_fails=3 fail_timeout=30s; - server backend:3001 max_fails=3 fail_timeout=30s; - server backend:3002 max_fails=3 fail_timeout=30s; -} - -server { - location /api/v1/documents/ { - proxy_pass http://backend; - proxy_next_upstream error timeout; - proxy_connect_timeout 10s; - proxy_read_timeout 30s; - } -} -``` - -**Rate Limiting**: - -```typescript -// ใช้ @nestjs/throttler -import { ThrottlerGuard } from '@nestjs/throttler'; - -@Controller('document-numbering') -@UseGuards(ThrottlerGuard) -export class DocumentNumberingController { - @Throttle(10, 60) // 10 requests per 60 seconds per user - @Post('generate') - async generate(@Body() dto: GenerateNumberDto) { - return await this.service.generate(dto); - } -} -``` - -### 3.11.9.3. Availability - -**SLA Targets**: - -- **Uptime**: ≥ 99.5% (excluding planned maintenance) -- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน -- **Recovery Time Objective (RTO)**: ≤ 30 นาที -- **Recovery Point Objective (RPO)**: ≤ 5 นาที - -**High Availability Setup**: - -```yaml -# High Availability Architecture -services: - # MariaDB - Master/Replica - mariadb-master: - image: mariadb:11.8 - environment: - MYSQL_REPLICATION_MODE: master - - mariadb-replica: - image: mariadb:11.8 - environment: - MYSQL_REPLICATION_MODE: slave - MYSQL_MASTER_HOST: mariadb-master - - # Redis - Sentinel Mode - redis-master: - image: redis:7-alpine - command: redis-server --appendonly yes - - redis-replica: - image: redis:7-alpine - command: redis-server --replicaof redis-master 6379 - - redis-sentinel: - image: redis:7-alpine - command: > - redis-sentinel /etc/redis/sentinel.conf - --sentinel monitor mymaster redis-master 6379 2 -``` - -**Health Checks**: - -```typescript -// src/health/health.controller.ts -import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus'; - -@Controller('health') -export class HealthController { - @Get() - @HealthCheck() - check() { - return this.health.check([ - () => this.db.pingCheck('database'), - () => this.redis.pingCheck('redis'), - () => this.customHealthCheck() - ]); - } - - private async customHealthCheck() { - // ทดสอบ generate document number - const canGenerate = await this.testGeneration(); - return { documentNumbering: { status: canGenerate ? 'up' : 'down' }}; - } -} - -## 3.11.10. Monitoring & Alerting - -### 3.11.10.1. Metrics Collection - -**Prometheus Metrics Implementation**: - -```typescript -// src/document-numbering/metrics/metrics.service.ts -import { Injectable } from '@nestjs/common'; -import { Counter, Histogram, Gauge } from 'prom-client'; - -@Injectable() -export class DocumentNumberingMetrics { - // Lock acquisition metrics - private lockAcquisitionDuration = new Histogram({ - name: 'docnum_lock_acquisition_duration_ms', - help: 'Lock acquisition time in milliseconds', - labelNames: ['project', 'type'], - buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000] - }); - - private lockAcquisitionFailures = new Counter({ - name: 'docnum_lock_acquisition_failures_total', - help: 'Total number of lock acquisition failures', - labelNames: ['project', 'type', 'reason'] - }); - - // Generation metrics - private generationDuration = new Histogram({ - name: 'docnum_generation_duration_ms', - help: 'Total document number generation time', - labelNames: ['project', 'type', 'status'], - buckets: [100, 200, 500, 1000, 2000, 5000] - }); - - private retryCount = new Histogram({ - name: 'docnum_retry_count', - help: 'Number of retries per generation', - labelNames: ['project', 'type'], - buckets: [0, 1, 2, 3, 5, 10] - }); - - // Connection health - private redisConnectionStatus = new Gauge({ - name: 'docnum_redis_connection_status', - help: 'Redis connection status (1=up, 0=down)' - }); - - private dbConnectionPoolUsage = new Gauge({ - name: 'docnum_db_connection_pool_usage', - help: 'Database connection pool usage percentage' - }); -} -``` - -### 3.11.10.2. Alert Rules - -**Prometheus Alert Rules** (`prometheus/alerts.yml`): - -```yaml -groups: - - name: document_numbering_alerts - interval: 30s - rules: - # CRITICAL: Redis unavailable - - alert: RedisUnavailable - expr: docnum_redis_connection_status == 0 - for: 1m - labels: - severity: critical - component: document-numbering - annotations: - summary: "Redis is unavailable for document numbering" - description: "System is falling back to DB-only locking" - - # CRITICAL: High lock failure rate - - alert: HighLockFailureRate - expr: | - rate(docnum_lock_acquisition_failures_total[5m]) > 0.1 - for: 5m - labels: - severity: critical - annotations: - summary: "Lock acquisition failure rate > 10%" - description: "Check Redis and database performance" - - # WARNING: Elevated lock failure rate - - alert: ElevatedLockFailureRate - expr: | - rate(docnum_lock_acquisition_failures_total[5m]) > 0.05 - for: 5m - labels: - severity: warning - annotations: - summary: "Lock acquisition failure rate > 5%" - - # WARNING: Slow lock acquisition - - alert: SlowLockAcquisition - expr: | - histogram_quantile(0.95, - rate(docnum_lock_acquisition_duration_ms_bucket[5m]) - ) > 1000 - for: 5m - labels: - severity: warning - annotations: - summary: "P95 lock acquisition time > 1 second" - - # WARNING: High retry count - - alert: HighRetryCount - expr: | - sum by (project) ( - rate(docnum_retry_count_sum[1h]) - ) > 100 - for: 1h - labels: - severity: warning - annotations: - summary: "Retry count > 100 per hour in project {{ $labels.project }}" - - # WARNING: Slow generation - - alert: SlowDocumentNumberGeneration - expr: | - histogram_quantile(0.95, - rate(docnum_generation_duration_ms_bucket[5m]) - ) > 2000 - for: 5m - labels: - severity: warning - annotations: - summary: "P95 generation time > 2 seconds" -``` - -**AlertManager Configuration** (`alertmanager/config.yml`): - -```yaml -route: - group_by: ['alertname', 'severity'] - group_wait: 30s - group_interval: 5m - repeat_interval: 4h - receiver: 'ops-team' - routes: - # CRITICAL alerts → PagerDuty + Slack - - match: - severity: critical - receiver: 'pagerduty-critical' - continue: true - - # WARNING alerts → Slack only - - match: - severity: warning - receiver: 'slack-warnings' - -receivers: - - name: 'pagerduty-critical' - pagerduty_configs: - - service_key: - description: '{{ .CommonAnnotations.summary }}' - - - name: 'slack-warnings' - slack_configs: - - api_url: - channel: '#lcbp3-alerts' - title: '⚠️ {{ .GroupLabels.alertname }}' - text: '{{ .CommonAnnotations.description }}' - - - name: 'ops-team' - email_configs: - - to: 'ops@example.com' -``` - -### 3.11.10.3. Grafana Dashboard - -**Dashboard Configuration** (`grafana/dashboards/document-numbering.json`): - -```json -{ - "title": "Document Numbering Performance", - "panels": [ - { - "title": "Lock Acquisition Success Rate", - "targets": [{ - "expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))" - }], - "type": "graph", - "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 } - }, - { - "title": "Lock Acquisition Time (Percentiles)", - "targets": [ - { - "expr": "histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", - "legendFormat": "P50" - }, - { - "expr": "histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", - "legendFormat": "P95" - }, - { - "expr": "histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", - "legendFormat": "P99" - } - ], - "type": "graph", - "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 } - }, - { - "title": "Generation Rate (per minute)", - "targets": [{ - "expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60" - }], - "type": "stat", - "gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 } - }, - { - "title": "Redis Connection Status", - "targets": [{ - "expr": "docnum_redis_connection_status" - }], - "type": "stat", - "gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 }, - "thresholds": { - "mode": "absolute", - "steps": [ - { "value": 0, "color": "red" }, - { "value": 1, "color": "green" } - ] - } - }, - { - "title": "Error Rate by Type", - "targets": [{ - "expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))" - }], - "type": "graph", - "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 } - } - ] -} -``` - -**Key Dashboard Panels**: - -- **Lock Acquisition Success Rate**: Real-time success % -- **Lock Wait Time Percentiles**: P50, P95, P99 latency -- **Generation Rate**: Documents/minute -- **Error Breakdown**: By error type (LOCK_TIMEOUT, VERSION_CONFLICT, etc.) -- **Redis/DB Health**: Connection status -- **Retry Distribution**: Histogram of retry counts - -## 3.11.12. API Reference - -เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้ (รายละเอียดใน `specs/02-architecture/api-design.md`): - -- `POST /api/v1/documents/{documentId}/generate-number` - สร้างเลขที่เอกสาร -- `GET /api/v1/document-numbering/configs` - ดูการตั้งค่า template -- `PUT /api/v1/document-numbering/configs/{configId}` - แก้ไข template (Admin only) -- `POST /api/v1/document-numbering/configs/{configId}/reset-counter` - Reset counter (Admin only) - -## 3.11.13. Database Schema Reference - -เอกสารนี้อ้างอิงถึง tables ต่อไปนี้ (รายละเอียดใน `specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md`): - -- `document_number_configs` - เก็บ template และ counter configuration -- `document_number_counters` - เก็บ current counter value -- `document_number_audit` - เก็บ audit trail -- `documents` - เก็บ document number ที่ถูกสร้าง - -## 3.11.14. Database Schema Requirements - -### 3.11.14.1. Counter Table Schema - -ตาราง `document_number_counters` ต้องมีโครงสร้างดังนี้: +### 3.11.11.6 Config History Table ```sql -CREATE TABLE document_number_counters ( - project_id INT NOT NULL, - originator_organization_id INT NOT NULL, - recipient_organization_id INT NULL, -- NULL for RFA - correspondence_type_id INT NOT NULL, - sub_type_id INT DEFAULT 0, -- for TRANSMITTAL - rfa_type_id INT DEFAULT 0, -- for RFA - discipline_id INT DEFAULT 0, -- for RFA - current_year INT NOT NULL, - version INT DEFAULT 0 NOT NULL, -- Optimistic Lock - last_number INT DEFAULT 0, +CREATE TABLE document_number_config_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + config_id INT NOT NULL, + template_before TEXT, + template_after TEXT NOT NULL, + changed_by INT NOT NULL, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + change_reason TEXT, - PRIMARY KEY ( - project_id, - originator_organization_id, - COALESCE(recipient_organization_id, 0), - correspondence_type_id, - sub_type_id, - rfa_type_id, - discipline_id, - current_year - ), - - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, - FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, - FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, - FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci - COMMENT = 'ตารางเก็บ Running Number Counters'; + FOREIGN KEY (config_id) REFERENCES document_number_configs(id), + FOREIGN KEY (changed_by) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Template Change History'; ``` -### 3.11.14.2. Index Requirements - -```sql --- Index สำหรับ Performance -CREATE INDEX idx_counter_lookup -ON document_number_counters ( - project_id, - correspondence_type_id, - current_year -); - --- Index สำหรับ Originator lookup -CREATE INDEX idx_counter_org -ON document_number_counters ( - originator_organization_id, - current_year -); -``` - -### 3.11.14.3. Important Notes +### 3.11.11.7 Important Notes > **💡 Counter Key Design** -> -> - ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL +> - `recipient_organization_id` ใช้ `0` สำหรับ RFA (ไม่มี specific recipient) > - `version` column สำหรับ Optimistic Locking (ป้องกัน race condition) > - `last_number` เริ่มจาก 0 และเพิ่มขึ้นทีละ 1 -> - Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน) +> - Counter reset ทุกปี (เมื่อ `reset_scope` เปลี่ยน) > **⚠️ Migration Notes** -> > - ไม่มีข้อมูลเก่า ไม่ต้องทำ backward compatibility > - สามารถสร้าง table ใหม่ได้เลยตาม schema ข้างต้น > - ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน -### 3.11.14.4. Example Counter Records +### 3.11.11.8 Example Counter Records ```sql -- Example: LETTER from คคง. to สคฉ.3 in LCBP3-C2 year 2025 INSERT INTO document_number_counters ( project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, - current_year, version, last_number + reset_scope, version, last_number ) VALUES ( 2, -- LCBP3-C2 22, -- คคง. 10, -- สคฉ.3 6, -- LETTER 0, 0, 0, - 2025, 0, 0 + 'YEAR_2025', 0, 0 ); -- Example: RFA from ผรม.2 in LCBP3-C2, discipline TER, type RPT, year 2025 INSERT INTO document_number_counters ( project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, - current_year, version, last_number + reset_scope, version, last_number ) VALUES ( 2, -- LCBP3-C2 42, -- ผรม.2 - NULL, -- RFA ไม่มี specific recipient + 0, -- RFA ไม่มี specific recipient 1, -- RFA 0, 18, -- RPT (Report) 5, -- TER (Terminal) - 2025, 0, 0 + 'NONE', 0, 0 -- No yearly reset for RFA ); ``` -## 3.11.15. Security Considerations +--- -### 3.11.14.1. Authorization +## 3.11.12 Security & Data Integrity Requirements -- เฉพาะ authenticated users เท่านั้นที่สามารถ request document number -- เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้ -- เฉพาะ Super Admin เท่านั้นที่ reset counter ได้ +### 3.11.12.1 Concurrency Control -### 3.11.14.2. Rate Limiting +**Requirements:** +- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน +- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์ +- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary +- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน + +### 3.11.12.2 Data Integrity + +**Requirements:** + +- ระบบ**ต้อง**ใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates +- ระบบ**ต้อง**มี database constraints เพื่อป้องกันข้อมูลผิดพลาด: + - Unique constraint บน `document_number` + - Foreign key constraints ทุก relationship + - Check constraints สำหรับ business rules + +### 3.11.12.3 Authorization + +**Requirements:** + +- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number +- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ +- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ (requires approval) + +### 3.11.12.4 Rate Limiting + +**Requirements:** - Limit ต่อ user: **10 requests/minute** (prevent abuse) - Limit ต่อ IP: **50 requests/minute** -### 3.11.14.3. Audit & Compliance +--- -- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering -- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) +## 3.11.13 Error Handling Requirements + +### 3.11.13.1 Retry Mechanism + +**Requirements:** + +ระบบ**ต้อง**จัดการ error scenarios ต่อไปนี้: + +| Scenario | Strategy | Max Retries | Expected Response | +| ------------------- | ------------------- | ----------- | ------------------------------- | +| Redis Unavailable | Fallback to DB Lock | 0 | Continue (degraded performance) | +| Lock Timeout | Exponential Backoff | 5 | HTTP 503 after final retry | +| Version Conflict | Immediate Retry | 2 | HTTP 409 after final retry | +| DB Connection Error | Exponential Backoff | 3 | HTTP 500 after final retry | + +### 3.11.13.2 User Experience + +**Requirements:** + +- Error messages **ต้อง**เป็นภาษาไทย และเข้าใจง่าย +- HTTP status codes **ต้อง**สื่อความหมายที่ถูกต้อง +- Frontend **ควร**แสดง retry option สำหรับ transient errors + +--- + +## 3.11.14 Configuration Management Requirements + +### 3.11.14.1 Template Management + +**Requirements:** + +- Project Admin **ต้อง**สามารถกำหนด/แก้ไข template ผ่าน Admin Panel +- ระบบ**ต้อง**validate template ก่อนบันทึก +- การเปลี่ยนแปลง template **ต้องไม่**ส่งผลต่อเอกสารที่สร้างไว้แล้ว + +### 3.11.14.2 Template Versioning + +**Requirements:** + +- ระบบ**ต้อง**เก็บ history ของ template changes +- ระบบ**ต้อง**บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง +- ระบบ**ควร**สามารถ rollback ไปเวอร์ชันก่อนหน้าได้ + +### 3.11.14.3 Counter Reset Policy + +**Requirements:** + +- Counter **ต้อง**reset ตามปี (อัตโนมัติ) +- Admin **ต้อง**สามารถ manual reset counter ได้ (require approval + audit log) + +--- + +## 3.11.15 Audit Trail Requirements + +### 3.11.15.1 Audit Logging + +**Requirements:** + +ระบบ**ต้อง**บันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation: + +- `document_id` - เอกสารที่ถูกสร้าง +- `generated_number` - เลขที่ถูกสร้าง +- `counter_key` - key ที่ใช้ในการนับ (JSON format) +- `template_used` - template ที่ใช้ +- `user_id` - ผู้ที่ request +- `ip_address` - IP address ของผู้ request +- `timestamp` - เวลาที่สร้าง +- `retry_count` - จำนวนครั้งที่ retry +- `performance_metrics` - Lock wait time, total duration + +### 3.11.15.2 Error Logging + +**Requirements:** + +- ระบบ**ต้อง**บันทึก error แยกต่างหาก พร้อม error type classification +- ระบบ**ควร**alert ops team สำหรับ critical errors + +### 3.11.15.3 Retention Policy + +**Requirements:** + +- Audit log **ต้อง**เก็บอย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) + +--- + +## 3.11.16 Performance Requirements + +### 3.11.16.1 Response Time + +**SLA Targets:** + +| Metric | Target | Notes | +| ----------------- | -------- | ------------------------ | +| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response | +| 99th percentile | ≤ 5 วินาที | รวม retry attempts | +| Normal operation | ≤ 500ms | ไม่มี retry | +| Number generation | < 100ms | (p95) | +| Lock acquisition | < 50ms | (avg) | +| Bulk import | < 5s | per 100 records | + +### 3.11.16.2 Throughput + +**Capacity Targets:** + +| Load Level | Target | Notes | +| ----------- | ----------- | ------------------ | +| Normal load | ≥ 50 req/s | ใช้งานปกติ | +| Peak load | ≥ 100 req/s | ช่วงเร่งงาน | +| Burst | ≥ 200 req/s | short duration | +| Support | > 500 req/s | Scale horizontally | + +### 3.11.16.3 Availability + +**SLA Targets:** + +- **Uptime**: ≥ 99.5% (excluding planned maintenance) +- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน +- **RTO**: ≤ 30 นาที +- **RPO**: ≤ 5 นาที +- **Graceful degradation**: Fallback to DB-only +- **Auto-recovery**: From Redis failure + +--- + +## 3.11.17 Monitoring & Alerting Requirements + +### 3.11.17.1 Metrics + +**Requirements:** + +ระบบ**ต้อง**collect metrics ต่อไปนี้: + +- Lock acquisition time (p50, p95, p99) +- Lock acquisition success/failure rate +- Counter generation latency +- Retry count distribution +- Redis connection status +- Database connection pool usage +- Sequence utilization (% of max) +- Numbers generated per day +- Manual overrides per day + +### 3.11.17.2 Alerts + +**Requirements:** + +ระบบ**ต้อง**alert สำหรับ conditions ต่อไปนี้: + +| Severity | Condition | Action | +| ---------- | ---------------------------- | ----------------- | +| 🔴 Critical | Redis unavailable > 1 minute | PagerDuty + Slack | +| 🔴 Critical | Lock failures > 10% in 5 min | PagerDuty + Slack | +| 🔴 Critical | Sequence >95% used | PagerDuty + Slack | +| 🟡 Warning | Lock failures > 5% in 5 min | Slack | +| 🟡 Warning | Avg lock wait time > 1 sec | Slack | +| 🟡 Warning | Retry count > 100/hour | Slack | +| 🟡 Warning | Sequence >90% used | Slack | +| 🟡 Warning | High error rate | Slack | + +### 3.11.17.3 Dashboard + +**Requirements:** + +- Ops team **ต้อง**มี real-time dashboard (Grafana) แสดง: + - Lock acquisition success rate + - Lock wait time percentiles + - Generation rate (per minute) + - Error rate by type + - Connection health status + - Retry distribution + +--- + +## 3.11.18 API Reference + +### Document Number Generation + +```http +POST /api/v1/documents/{documentId}/generate-number +``` + +สร้างเลขที่เอกสารสำหรับ document ที่ระบุ + +**Request Body:** + +```json +{ + "counterKey": { + "projectId": 2, + "originatorOrgId": 22, + "recipientOrgId": 10, + "correspondenceTypeId": 6, + "subTypeId": 0, + "rfaTypeId": 0, + "disciplineId": 0, + "resetScope": "YEAR_2025" + } +} +``` + +**Response:** + +```json +{ + "documentNumber": "คคง.-สคฉ.3-0001-2568", + "generatedAt": "2025-12-02T15:30:00Z" +} +``` + +### Reserve Number + +```http +POST /api/document-numbering/reserve +``` + +**Request:** +```json +{ + "document_type": "COR", + "project_id": 1, + "contract_id": null, + "metadata": {} +} +``` + +**Response 201:** +```json +{ + "token": "uuid-v4", + "document_number": "COR-2025-00042", + "expires_at": "2025-01-16T10:30:00Z" +} +``` + +### Confirm Reservation + +```http +POST /api/document-numbering/confirm +``` + +**Request:** +```json +{ + "token": "uuid-v4" +} +``` + +**Response 200:** +```json +{ + "document_number": "COR-2025-00042", + "confirmed_at": "2025-01-16T10:25:00Z" +} +``` + +### Manual Override + +```http +POST /api/document-numbering/manual +Authorization: Bearer +``` + +**Request:** +```json +{ + "document_type": "COR", + "document_number": "COR-2024-99999", + "reason": "Import from legacy system", + "skip_validation": false +} +``` + +**Response 201:** +```json +{ + "document_number": "COR-2024-99999", + "is_manual": true, + "created_at": "2025-01-16T10:25:00Z" +} +``` + +### Template Management + +```http +GET /api/v1/document-numbering/configs +``` + +ดูรายการ template configuration ทั้งหมด + +```http +PUT /api/v1/document-numbering/configs/{configId} +``` + +แก้ไข template (Project Admin only) + +```http +POST /api/v1/document-numbering/configs/{configId}/reset-counter +``` + +Reset counter (Super Admin only, requires approval) + +--- + +## 3.11.19 Testing Requirements + +### 3.11.19.1 Unit Tests +- Format parsing and validation +- Sequence increment logic +- Manual override validation +- Scope resolution + +### 3.11.19.2 Integration Tests +- Redis locking mechanism +- Database transactions +- Two-phase commit flow +- Bulk import + +### 3.11.19.3 Load Tests + +```bash +# Must pass without duplicates +concurrent_users: 100 +requests_per_second: 500 +test_duration: 5 minutes +expected_duplicates: 0 +``` + +- Concurrent number generation (1000 req/s) +- Lock contention under load +- Redis failover scenarios +- Database connection pool exhaustion + +### 3.11.19.4 E2E Tests +- Complete document creation flow +- Void and replace workflow +- Bulk import with validation +- Admin configuration UI + +--- + +## 3.11.20 Versioning Note + +* Existing documents **not affected** +* New rules apply to documents generated after upgrade to v1.6.2 + +--- + +## 3.11.21 Migration Plan + +### 3.11.21.1 Legacy Data Import +1. Export existing document numbers from old system +2. Validate format and detect duplicates +3. Bulk import using manual override API +4. Update sequence counters to max values +5. Verify data integrity + +### 3.11.21.2 Rollout Strategy +- Week 1-2: Deploy to staging, test with dummy data +- Week 3: Deploy to production, enable for test project +- Week 4: Enable for all projects +- Week 5+: Monitor and optimize + +--- + +## 3.11.22 Success Criteria + +### 3.11.22.1 Functional Success +- ✅ All FRs implemented and tested +- ✅ Zero duplicate numbers in production +- ✅ Migration of 50,000+ legacy documents +- ✅ UAT approved by stakeholders + +### 3.11.22.2 Performance Success +- ✅ Response time <100ms (p95) +- ✅ Throughput >500 req/s +- ✅ Lock acquisition <50ms (avg) +- ✅ Zero downtime during deployment + +### 3.11.22.3 Business Success +- ✅ Document creation speed +30% +- ✅ Manual numbering errors -80% +- ✅ User satisfaction >4.5/5 +- ✅ System stability >99.9% + +--- + +## 3.11.23 References + +- [Implementation Guide](file:///d:/nap-dms.lcbp3/specs/03-implementation/document-numbering.md) +- [Operations Guide](file:///d:/nap-dms.lcbp3/specs/04-operations/document-numbering-operations.md) +- [API Design](file:///d:/nap-dms.lcbp3/specs/02-architecture/api-design.md) +- [Data Dictionary](file:///d:/nap-dms.lcbp3/specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md) +- [ADR-018: Document Numbering Strategy](file:///d:/nap-dms.lcbp3/specs/05-decisions/adr-018-document-numbering.md) +- [Two-Phase Commit Pattern](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) +- [Redlock Algorithm](https://redis.io/docs/manual/patterns/distributed-locks/) + +--- + +## 3.11.24 Appendix + +### 3.11.24.1 Glossary +- **Sequence**: ลำดับตัวเลขที่เพิ่มขึ้นอัตโนมัติ +- **Scope**: ขอบเขตที่ sequence แยกตาม (project, contract, etc.) +- **Token**: Format placeholder (e.g., {YYYY}, {SEQ}) +- **Redlock**: Distributed locking algorithm สำหรับ Redis + +--- + +**Approval Sign-off**: + +| Role | Name | Date | Signature | +| ------------- | ----------- | ------- | --------- | +| Product Owner | ___________ | _______ | _________ | +| Tech Lead | ___________ | _______ | _________ | +| QA Lead | ___________ | _______ | _________ | + +**End of Document v1.6.2** diff --git a/specs/01-requirements/file.tmp b/specs/01-requirements/file.tmp new file mode 100644 index 0000000..e69de29 diff --git a/specs/02-architecture/README.md b/specs/02-architecture/README.md index f22e454..bafa9d8 100644 --- a/specs/02-architecture/README.md +++ b/specs/02-architecture/README.md @@ -10,9 +10,9 @@ | Attribute | Value | | ------------------ | -------------------------------- | -| **Version** | 1.6.0 | +| **Version** | 1.6.2 | | **Status** | Active | -| **Last Updated** | 2025-12-13 | +| **Last Updated** | 2025-12-17 | | **Owner** | Nattanin Peancharoen | | **Classification** | Internal Technical Documentation | diff --git a/specs/02-architecture/system-architecture.md b/specs/02-architecture/system-architecture.md index 001c08d..b42b932 100644 --- a/specs/02-architecture/system-architecture.md +++ b/specs/02-architecture/system-architecture.md @@ -3,15 +3,15 @@ --- **title:** 'System Architecture' -**version:** 1.5.0 +**version:** 1.6.2 **status:** first-draft **owner:** Nattanin Peancharoen -**last_updated:** 2025-11-30 +**last_updated:** 2025-12-17 **related:** - specs/01-requirements/02-architecture.md - specs/01-requirements/06-non-functional.md -- specs/03-implementation/fullftack-js-v1.5.0.md +- specs/03-implementation/fullftack-js-v1.6.2.md --- @@ -19,9 +19,50 @@ เอกสารนี้อธิบายสถาปัตยกรรมระบบ LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System) ที่ใช้แนวทาง **Headless/API-First Architecture** พร้อมการ Deploy บน QNAP Server ผ่าน Container Station -## 🎯 Architecture Principles +## 1. 🎯 Architecture Principles -### 1.1 Core Principles +### 1.1 Component Overview +``` +┌──────────────────────────────────────────────────────┐ +│ Load Balancer │ +│ (Nginx Proxy Manager) │ +└────────────┬─────────────────────────────────────────┘ + │ + ┌────────┴────────┬──────────────┬──────────────┐ + │ │ │ │ +┌───▼────┐ ┌──────▼──────┐ ┌──▼───┐ ┌─────▼─────┐ +│Backend │ │Backend │ │Backend│ │ Backend │ +│Node 1 │ │Node 2 │ │Node 3 │ │ Node 4 │ +└───┬────┘ └──────┬──────┘ └──┬────┘ └─────┬─────┘ + │ │ │ │ + └────────────────┴──────────────┴───────────────┘ + │ + ┌───────────┼───────────┬──────────────┐ + │ │ │ │ + ┌────▼────┐ ┌──▼───┐ ┌───▼────┐ ┌────▼─────┐ + │ MariaDB │ │Redis │ │ Redis │ │ Redis │ + │ Primary │ │Node 1│ │ Node 2 │ │ Node 3 │ + └────┬────┘ └──────┘ └────────┘ └──────────┘ + │ + ┌────▼────┐ + │ MariaDB │ + │Replicas │ + └─────────┘ +``` +### 1.2 Component Responsibilities + +| Component | Purpose | Critical? | +| --------------- | --------------------------------- | --------- | +| Backend Nodes | API processing, number generation | YES | +| MariaDB Primary | Persistent sequence storage | YES | +| Redis Cluster | Distributed locking, reservations | YES | +| Load Balancer | Traffic distribution | YES | +| Prometheus | Metrics collection | NO | +| Grafana | Monitoring dashboard | NO | + +--- + +### 1.3 Core Principles 1. **Data Integrity First:** ความถูกต้องของข้อมูลต้องมาก่อนทุกอย่าง 2. **Security by Design:** รักษาความปลอดภัยที่ทุกชั้น @@ -29,13 +70,13 @@ 4. **Resilience:** ทนทานต่อ Failure และ Recovery ได้รวดเร็ว 5. **Observability:** ติดตามและวิเคราะห์สถานะระบบได้ง่าย -### 1.2 Architecture Style +### 1.4 Architecture Style - **Headless CMS Architecture:** แยก Frontend และ Backend เป็นอิสระ - **API-First:** Backend เป็น API Server ที่ Frontend หรือ Third-party สามารถเรียกใช้ได้ - **Microservices-Ready:** ออกแบบเป็น Modular Architecture พร้อมแยกเป็น Microservices ในอนาคต -## 🏢 Infrastructure & Deployment +## 2. 🏢 Infrastructure & Deployment ### 2.1 Server Infrastructure @@ -95,7 +136,7 @@ graph TB - ใช้ Joi/Zod validate Environment Variables ตอน App Start - Throw Error ทันทีหากขาด Variable สำคัญ -## 🔧 Core Services +## 3. 🔧 Core Services ### 3.1 Service Overview @@ -195,7 +236,7 @@ graph TB - Index อัตโนมัติเมื่อ Create/Update เอกสาร - Async Indexing ผ่าน Queue (ไม่ Block Main Request) -## 🧱 Backend Module Architecture +## 4. 🧱 Backend Module Architecture ### 4.1 Modular Design @@ -378,7 +419,7 @@ graph TB - Dynamic Schema Generation - Data Transformation -## 📊 Data Flow Architecture +## 5. 📊 Data Flow Architecture ### 5.1 Main Request Flow @@ -518,7 +559,7 @@ sequenceDiagram end ``` -## 🛡️ Security Architecture +## 6. 🛡️ Security Architecture ### 6.1 Security Layers @@ -643,7 +684,7 @@ graph TB | Insecure Deserialization | Input Validation | | Using Known Vulnerable Components | Regular Dependency Updates | -## 📈 Performance & Scalability +## 7. 📈 Performance & Scalability ### 7.1 Caching Strategy @@ -686,7 +727,7 @@ graph TB | Cache Hit Ratio | > 80% | Master Data | | Application Startup | < 30s | Cold Start | -## 🔄 Resilience & Error Handling +## 8. 🔄 Resilience & Error Handling ### 8.1 Resilience Patterns @@ -725,7 +766,7 @@ graph TB - Fallback UI Components - Retry Mechanisms for Failed Requests -## 📊 Monitoring & Observability +## 9. 📊 Monitoring & Observability ### 9.1 Health Checks @@ -819,7 +860,7 @@ GET /health/live # Liveness probe - `ip_address`, `user_agent` - `timestamp` -## 💾 Backup & Disaster Recovery +## 10. 💾 Backup & Disaster Recovery ### 10.1 Backup Strategy @@ -867,7 +908,7 @@ GET /health/live # Liveness probe - Run consistency checks - Verify critical business data -## 🏗️ Deployment Architecture +## 11. 🏗️ Deployment Architecture ### 11.1 Container Deployment @@ -916,7 +957,7 @@ graph LR ProdDeploy --> Monitor[Monitor & Alert] ``` -## 🎯 Future Enhancements +## 12.🎯 Future Enhancements ### 12.1 Scalability Improvements @@ -932,7 +973,7 @@ graph LR - [ ] Mobile Native Apps - [ ] Blockchain Integration for Document Integrity -### 12.3 Infrastructure +### 12.3 Infrastructure Enhancements - [ ] Multi-Region Deployment - [ ] CDN for Static Assets @@ -943,9 +984,9 @@ graph LR **Document Control:** -- **Version:** 1.6.0 +- **Version:** 1.6.2 - **Status:** Active -- **Last Updated:** 2025-12-13 +- **Last Updated:** 2025-12-17 - **Owner:** Nattanin Peancharoen ``` diff --git a/specs/03-implementation/document-numbering.md b/specs/03-implementation/document-numbering.md index 24bfdec..c0680c9 100644 --- a/specs/03-implementation/document-numbering.md +++ b/specs/03-implementation/document-numbering.md @@ -1,40 +1,97 @@ -# Document Numbering Implementation Guide +# Document Numbering Implementation Guide (Combined) --- title: 'Implementation Guide: Document Numbering System' -version: 1.7.0 -status: implemented +version: 1.6.2 +status: APPROVED owner: Development Team -last_updated: 2025-12-16 +last_updated: 2025-12-17 related: - -- specs/01-requirements/03.11-document-numbering.md -- specs/04-operations/document-numbering-operations.md - + - specs/01-requirements/03.11-document-numbering.md + - specs/04-operations/document-numbering-operations.md + - specs/05-decisions/ADR-002-document-numbering-strategy.md --- ## Overview -เอกสารนี้อธิบาย implementation details สำหรับระบบ Document Numbering ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) +เอกสารนี้รวบรวม implementation details สำหรับระบบ Document Numbering โดยผนวกข้อมูลจาก: +- `document-numbering.md` - Core implementation และ database schema +- `document-numbering-add.md` - Extended features (Reservation, Manual Override, Monitoring) + +--- ## Technology Stack -- **Backend Framework**: NestJS 10.x -- **ORM**: TypeORM 0.3.x -- **Database**: MariaDB 11.8 -- **Cache/Lock**: Redis 7.x + Redlock -- **Message Queue**: BullMQ -- **Monitoring**: Prometheus + Grafana +| Component | Technology | +| ----------------- | -------------------- | +| Backend Framework | NestJS 10.x | +| ORM | TypeORM 0.3.x | +| Database | MariaDB 11.8 | +| Cache/Lock | Redis 7.x + Redlock | +| Message Queue | BullMQ | +| Monitoring | Prometheus + Grafana | -## 1. Database Implementation +--- -### 1.1. Counter Table Schema +## 1. Module Structure + +``` +backend/src/modules/document-numbering/ +├── document-numbering.module.ts +├── controllers/ +│ ├── document-numbering.controller.ts # General endpoints +│ ├── document-numbering-admin.controller.ts # Admin endpoints +│ └── numbering-metrics.controller.ts # Metrics endpoints +├── services/ +│ ├── document-numbering.service.ts # Main orchestration +│ ├── document-numbering-lock.service.ts # Redis Lock +│ ├── counter.service.ts # Sequence counter logic +│ ├── reservation.service.ts # Two-phase commit +│ ├── manual-override.service.ts # Manual number handling +│ ├── format.service.ts # Template formatting +│ ├── template.service.ts # Template CRUD +│ ├── audit.service.ts # Audit logging +│ ├── metrics.service.ts # Prometheus metrics +│ └── migration.service.ts # Legacy import +├── entities/ +│ ├── document-number-counter.entity.ts +│ ├── document-number-format.entity.ts +│ ├── document-number-audit.entity.ts +│ ├── document-number-error.entity.ts +│ └── document-number-reservation.entity.ts +├── dto/ +│ ├── generate-number.dto.ts +│ ├── preview-number.dto.ts +│ ├── reserve-number.dto.ts +│ ├── confirm-reservation.dto.ts +│ ├── manual-override.dto.ts +│ ├── void-document.dto.ts +│ └── bulk-import.dto.ts +├── validators/ +│ └── template.validator.ts +├── guards/ +│ └── manual-override.guard.ts +├── decorators/ +│ └── audit-numbering.decorator.ts +├── jobs/ +│ └── counter-reset.job.ts +└── tests/ + ├── unit/ + ├── integration/ + └── e2e/ +``` + +--- + +## 2. Database Schema + +### 2.1 Format Template Table ```sql CREATE TABLE document_number_formats ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, - correspondence_type_id INT NULL, -- NULL indicates default format for the project + correspondence_type_id INT NULL, -- NULL = default format for project format_template VARCHAR(100) NOT NULL, reset_sequence_yearly TINYINT(1) DEFAULT 1, description VARCHAR(255), @@ -44,8 +101,12 @@ CREATE TABLE document_number_formats ( UNIQUE KEY idx_unique_project_type (project_id, correspondence_type_id), FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE -); +) ENGINE=InnoDB COMMENT='Document Number Format Templates'; +``` +### 2.2 Counter Table + +```sql CREATE TABLE document_number_counters ( project_id INT NOT NULL, originator_organization_id INT NOT NULL, @@ -80,31 +141,36 @@ CREATE TABLE document_number_counters ( CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci - COMMENT='ตารางเก็บ Running Number Counters'; + COMMENT='Running Number Counters'; ``` -### 1.2. Audit Table Schema +### 2.3 Audit Table ```sql CREATE TABLE document_number_audit ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id INT NULL COMMENT 'FK to documents (NULL initially, updated after doc creation)', + operation ENUM('RESERVE', 'CONFIRM', 'CANCEL', 'MANUAL_OVERRIDE', 'VOID', 'GENERATE') NOT NULL, + document_id INT NULL COMMENT 'FK to documents (NULL initially)', + document_type VARCHAR(50), generated_number VARCHAR(100) NOT NULL, counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)', template_used VARCHAR(200) NOT NULL, + old_value TEXT NULL, + new_value TEXT NULL, user_id INT NULL COMMENT 'FK to users (Allow NULL for system generation)', ip_address VARCHAR(45), user_agent TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - is_success BOOLEAN DEFAULT TRUE COMMENT 'Track success/failure status', - - -- Performance & Error Tracking + is_success BOOLEAN DEFAULT TRUE, retry_count INT DEFAULT 0, lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds', total_duration_ms INT COMMENT 'Total generation time', fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE', + metadata JSON NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_operation (operation), INDEX idx_document_id (document_id), + INDEX idx_document_number (generated_number), INDEX idx_user_id (user_id), INDEX idx_created_at (created_at), FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, @@ -112,7 +178,7 @@ CREATE TABLE document_number_audit ( ) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail'; ``` -### 1.3. Error Log Table +### 2.4 Error Log Table ```sql CREATE TABLE document_number_errors ( @@ -122,7 +188,10 @@ CREATE TABLE document_number_errors ( 'VERSION_CONFLICT', 'DB_ERROR', 'REDIS_ERROR', - 'VALIDATION_ERROR' + 'VALIDATION_ERROR', + 'SEQUENCE_EXHAUSTED', + 'RESERVATION_EXPIRED', + 'DUPLICATE_NUMBER' ) NOT NULL, error_message TEXT, stack_trace TEXT, @@ -138,194 +207,67 @@ CREATE TABLE document_number_errors ( ) ENGINE=InnoDB COMMENT='Document Numbering Error Log'; ``` -## 2. NestJS Implementation +--- -### 2.1. Module Structure +## 3. Core Services -``` -src/modules/document-numbering/ -├── document-numbering.module.ts -├── controllers/ -│ └── document-numbering.controller.ts -├── services/ -│ ├── document-numbering.service.ts -│ ├── document-numbering-lock.service.ts -│ ├── counter.service.ts -│ ├── template.service.ts -│ └── audit.service.ts -├── entities/ -│ ├── document-number-counter.entity.ts -│ ├── document-number-audit.entity.ts -│ └── document-number-error.entity.ts -├── dto/ -│ ├── generate-number.dto.ts -│ └── update-template.dto.ts -├── validators/ -│ └── template.validator.ts -├── jobs/ -│ └── counter-reset.job.ts -└── metrics/ - └── metrics.service.ts +### 3.1 Number Generation Process + +```mermaid +sequenceDiagram + participant C as Client + participant S as NumberingService + participant L as LockService + participant CS as CounterService + participant DB as Database + participant R as Redis + + C->>S: generateDocumentNumber(dto) + S->>L: acquireLock(counterKey) + L->>R: REDLOCK acquire + R-->>L: lock acquired + L-->>S: lock handle + S->>CS: incrementCounter(counterKey) + CS->>DB: BEGIN TRANSACTION + CS->>DB: SELECT FOR UPDATE + CS->>DB: UPDATE last_number + CS->>DB: COMMIT + DB-->>CS: newNumber + CS-->>S: sequence + S->>S: formatNumber(template, seq) + S->>L: releaseLock() + L->>R: REDLOCK release + S-->>C: documentNumber ``` -### 2.2. Number Generation Process +### 3.2 Two-Phase Commit (Reserve/Confirm) -#### 2.2.1. Resolve Format Template: - * Query document_number_formats by project_id + type_id. - * If no result, query by project_id + NULL (Default Project Format). - * If still no result, apply System Default Template: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`. - * Determine resetSequenceYearly flag from the found format (default: true) +```mermaid +sequenceDiagram + participant C as Client + participant RS as ReservationService + participant SS as SequenceService + participant R as Redis -#### 2.2.2. Determine Counter Key: - * If resetSequenceYearly is True: Use Current Year (e.g., 2025). - * If resetSequenceYearly is False: Use 0 (Continuous). - * Use type_id from the resolved format (Specific ID or NULL). + Note over C,R: Phase 1: Reserve + C->>RS: reserve(documentType) + RS->>SS: getNextSequence() + SS-->>RS: documentNumber + RS->>R: SETEX reservation:{token} (TTL: 5min) + RS-->>C: {token, documentNumber, expiresAt} -#### 2.2.3. Generate Number: - * Use format template to generate number. - * Replace tokens with actual values: - * {PROJECT} -> Project Code - * {ORG} -> Originator Organization Code - * {RECIPIENT} -> Recipient Organization Code - * {TYPE} -> Type Code - * {YEAR} -> Current Year - * {SEQ} -> Sequence Number - * {REV} -> Revision Number + Note over C,R: Phase 2: Confirm + C->>RS: confirm(token) + RS->>R: GET reservation:{token} + R-->>RS: reservationData + RS->>R: DEL reservation:{token} + RS-->>C: documentNumber (confirmed) +``` -#### 2.2.4. Validate Number: - * Check if generated number is unique. - * If not unique, increment sequence and retry. - -#### 2.2.5. Update Counter: - * Update document_number_counters with new sequence. - -#### 2.2.6. Generate Audit Record: - * Create audit record with: - * Generated number - * Counter key used - * Template used - * User ID - * IP Address - * User Agent - -#### 2.2.7. Return Generated Number: - * Return generated number to caller. - -### 2.3. TypeORM Entity +### 3.3 Counter Service Implementation ```typescript -// File: src/modules/document-numbering/entities/document-number-counter.entity.ts -import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm'; - -@Entity('document_number_counters') -export class DocumentNumberCounter { - @PrimaryColumn({ name: 'project_id' }) - projectId: number; - - @PrimaryColumn({ name: 'originator_organization_id' }) - originatorOrganizationId: number; - - @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) - recipientOrganizationId: number | null; - - @PrimaryColumn({ name: 'correspondence_type_id' }) - correspondenceTypeId: number; - - @PrimaryColumn({ name: 'sub_type_id', default: 0 }) - subTypeId: number; - - @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) - rfaTypeId: number; - - @PrimaryColumn({ name: 'discipline_id', default: 0 }) - disciplineId: number; - - @PrimaryColumn({ name: 'current_year' }) - currentYear: number; - - @VersionColumn({ name: 'version' }) - version: number; - - @Column({ name: 'last_number', default: 0 }) - lastNumber: number; -} -``` - -### 2.4. Redis Lock Service - -```typescript -// File: src/modules/document-numbering/services/document-numbering-lock.service.ts -import { Injectable, Logger } from '@nestjs/common'; -import Redlock from 'redlock'; -import { InjectRedis } from '@nestjs-modules/ioredis'; -import { Redis } from 'ioredis'; - -interface CounterKey { - projectId: number; - originatorOrgId: number; - recipientOrgId: number | null; - correspondenceTypeId: number; - subTypeId: number; - rfaTypeId: number; - disciplineId: number; - year: number; -} - -@Injectable() -export class DocumentNumberingLockService { - private readonly logger = new Logger(DocumentNumberingLockService.name); - private redlock: Redlock; - - constructor(@InjectRedis() private readonly redis: Redis) { - this.redlock = new Redlock([redis], { - driftFactor: 0.01, - retryCount: 5, - retryDelay: 100, - retryJitter: 50, - }); - } - - async acquireLock(counterKey: CounterKey): Promise { - const lockKey = this.buildLockKey(counterKey); - const ttl = 5000; // 5 วินาที - - try { - const lock = await this.redlock.acquire([lockKey], ttl); - this.logger.debug(`Acquired lock: ${lockKey}`); - return lock; - } catch (error) { - this.logger.error(`Failed to acquire lock: ${lockKey}`, error); - throw error; - } - } - - async releaseLock(lock: Redlock.Lock): Promise { - try { - await lock.release(); - this.logger.debug('Released lock'); - } catch (error) { - this.logger.warn('Failed to release lock (may have expired)', error); - } - } - - private buildLockKey(key: CounterKey): string { - return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + - `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + - `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; - } -} -``` - -### 2.4. Counter Service - -```typescript -// File: src/modules/document-numbering/services/counter.service.ts -import { Injectable, ConflictException, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; -import { DocumentNumberCounter } from '../entities/document-number-counter.entity'; -import { OptimisticLockVersionMismatchError } from 'typeorm'; - +// services/counter.service.ts @Injectable() export class CounterService { private readonly logger = new Logger(CounterService.name); @@ -348,7 +290,6 @@ export class CounterService { }); if (!counter) { - // สร้าง counter ใหม่ const newCounter = manager.create(DocumentNumberCounter, { ...counterKey, lastNumber: 1, @@ -364,9 +305,7 @@ export class CounterService { }); } catch (error) { if (error instanceof OptimisticLockVersionMismatchError) { - this.logger.warn( - `Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`, - ); + this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`); if (attempt === MAX_RETRIES - 1) { throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่'); } @@ -376,110 +315,188 @@ export class CounterService { } } } +} +``` - private buildWhereClause(key: CounterKey) { - return { - projectId: key.projectId, - originatorOrganizationId: key.originatorOrgId, - recipientOrganizationId: key.recipientOrgId, - correspondenceTypeId: key.correspondenceTypeId, - subTypeId: key.subTypeId, - rfaTypeId: key.rfaTypeId, - disciplineId: key.disciplineId, - currentYear: key.year, - }; +### 3.4 Redis Lock Service + +```typescript +// services/document-numbering-lock.service.ts +@Injectable() +export class DocumentNumberingLockService { + private readonly logger = new Logger(DocumentNumberingLockService.name); + private redlock: Redlock; + + constructor(@InjectRedis() private readonly redis: Redis) { + this.redlock = new Redlock([redis], { + driftFactor: 0.01, + retryCount: 5, + retryDelay: 100, + retryJitter: 50, + }); + } + + async acquireLock(counterKey: CounterKey): Promise { + const lockKey = this.buildLockKey(counterKey); + const ttl = 5000; // 5 seconds + + try { + const lock = await this.redlock.acquire([lockKey], ttl); + this.logger.debug(`Acquired lock: ${lockKey}`); + return lock; + } catch (error) { + this.logger.error(`Failed to acquire lock: ${lockKey}`, error); + throw error; + } + } + + async releaseLock(lock: Redlock.Lock): Promise { + try { + await lock.release(); + } catch (error) { + this.logger.warn('Failed to release lock (may have expired)', error); + } + } + + private buildLockKey(key: CounterKey): string { + return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + + `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + + `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; } } ``` -### 2.5. Main Service with Retry Logic +### 3.5 Reservation Service ```typescript -// File: src/modules/document-numbering/services/document-numbering.service.ts -import { Injectable, ServiceUnavailableException, Logger } from '@nestjs/common'; -import { DocumentNumberingLockService } from './document-numbering-lock.service'; -import { CounterService } from './counter.service'; -import { AuditService } from './audit.service'; -import { RedisConnectionError } from 'ioredis'; - +// services/reservation.service.ts @Injectable() -export class DocumentNumberingService { - private readonly logger = new Logger(DocumentNumberingService.name); +export class ReservationService { + private readonly TTL = 300; // 5 minutes constructor( - private lockService: DocumentNumberingLockService, - private counterService: CounterService, + private redis: Redis, + private sequenceService: SequenceService, private auditService: AuditService, ) {} - async generateDocumentNumber(dto: GenerateNumberDto): Promise { - const startTime = Date.now(); - let lockWaitMs = 0; - let retryCount = 0; - let fallbackUsed = 'NONE'; + async reserve( + documentType: string, + scopeValue?: string, + metadata?: Record, + ): Promise { + // 1. Generate next number + const documentNumber = await this.sequenceService.getNextSequence( + documentType, + scopeValue, + ); - try { - // พยายามใช้ Redis lock ก่อน - return await this.generateWithRedisLock(dto); - } catch (error) { - if (error instanceof RedisConnectionError) { - // Fallback: ใช้ database lock - this.logger.warn('Redis unavailable, falling back to DB lock'); - fallbackUsed = 'DB_LOCK'; - return await this.generateWithDbLock(dto); - } - throw error; - } finally { - // บันทึก audit log - await this.auditService.logGeneration({ - documentId: dto.documentId, - counterKey: dto.counterKey, - lockWaitMs, - totalDurationMs: Date.now() - startTime, - fallbackUsed, - retryCount, + // 2. Generate reservation token + const token = uuidv4(); + const expiresAt = new Date(Date.now() + this.TTL * 1000); + + // 3. Save to Redis + const reservation: Reservation = { + token, + document_number: documentNumber, + document_type: documentType, + scope_value: scopeValue, + expires_at: expiresAt, + metadata, + }; + + await this.redis.setex( + `reservation:${token}`, + this.TTL, + JSON.stringify(reservation), + ); + + // 4. Audit log + await this.auditService.log({ + operation: 'RESERVE', + document_type: documentType, + document_number: documentNumber, + metadata: { token, scope_value: scopeValue }, + }); + + return reservation; + } + + async confirm(token: string, userId: number): Promise { + const reservation = await this.getReservation(token); + + if (!reservation) { + throw new ReservationExpiredError( + 'Reservation not found or expired. Please reserve a new number.', + ); + } + + await this.redis.del(`reservation:${token}`); + + await this.auditService.log({ + operation: 'CONFIRM', + document_type: reservation.document_type, + document_number: reservation.document_number, + user_id: userId, + metadata: { token }, + }); + + return reservation.document_number; + } + + async cancel(token: string, userId: number): Promise { + const reservation = await this.getReservation(token); + + if (reservation) { + await this.redis.del(`reservation:${token}`); + + await this.auditService.log({ + operation: 'CANCEL', + document_type: reservation.document_type, + document_number: reservation.document_number, + user_id: userId, + metadata: { token }, }); } } - private async generateWithRedisLock(dto: GenerateNumberDto): Promise { - const lock = await this.lockService.acquireLock(dto.counterKey); - - try { - const nextNumber = await this.counterService.incrementCounter(dto.counterKey); - return this.formatNumber(dto.template, nextNumber, dto.counterKey); - } finally { - await this.lockService.releaseLock(lock); + @Cron('0 */5 * * * *') // Every 5 minutes + async cleanupExpired(): Promise { + const keys = await this.redis.keys('reservation:*'); + for (const key of keys) { + const ttl = await this.redis.ttl(key); + if (ttl <= 0) { + await this.redis.del(key); + } } } - - private async generateWithDbLock(dto: GenerateNumberDto): Promise { - // ใช้ pessimistic lock - // Implementation details... - } - - private formatNumber(template: string, seq: number, key: CounterKey): string { - // Template formatting logic - // Example: `คคง.-สคฉ.3-0001-2568` - return template - .replace('{SEQ:4}', seq.toString().padStart(4, '0')) - .replace('{YEAR:B.E.}', (key.year + 543).toString()); - // ... more replacements - } } ``` -## 3. Template Validation +--- + +## 4. Template System + +### 4.1 Supported Tokens + +| Token | Description | Example Output | +| -------------- | ---------------------------- | -------------- | +| `{PROJECT}` | Project Code | `LCBP3` | +| `{ORIGINATOR}` | Originator Organization Code | `คคง.` | +| `{RECIPIENT}` | Recipient Organization Code | `สคฉ.3` | +| `{CORR_TYPE}` | Correspondence Type Code | `L` | +| `{SUB_TYPE}` | Sub Type Code | `TD` | +| `{RFA_TYPE}` | RFA Type Code | `RFA` | +| `{DISCIPLINE}` | Discipline Code | `CV` | +| `{SEQ:n}` | Sequence Number (n digits) | `0001` | +| `{YEAR:CE}` | Year (Common Era) | `2025` | +| `{YEAR:BE}` | Year (Buddhist Era) | `2568` | +| `{REV}` | Revision Number | `A` | + +### 4.2 Template Validation ```typescript -// File: src/modules/document-numbering/validators/template.validator.ts -import { Injectable } from '@nestjs/common'; - -interface ValidationResult { - valid: boolean; - errors: string[]; -} - +// validators/template.validator.ts @Injectable() export class TemplateValidator { private readonly ALLOWED_TOKENS = [ @@ -521,280 +538,194 @@ export class TemplateValidator { return { valid: errors.length === 0, errors }; } - - private extractTokens(template: string) { - const regex = /\{([^}]+)\}/g; - const tokens: Array<{ name: string; full: string }> = []; - let match; - - while ((match = regex.exec(template)) !== null) { - const tokenName = match[1].split(':')[0]; // SEQ:4 → SEQ - tokens.push({ name: tokenName, full: match[1] }); - } - - return tokens; - } } ``` -## 4. BullMQ Job for Counter Reset +--- + +## 5. API Endpoints + +### 5.1 General Endpoints (`/document-numbering`) + +| Endpoint | Method | Permission | Description | +| --------------- | ------ | ------------------------ | --------------------------------- | +| `/logs/audit` | GET | `system.view_logs` | Get audit logs | +| `/logs/errors` | GET | `system.view_logs` | Get error logs | +| `/sequences` | GET | `correspondence.read` | Get counter sequences | +| `/counters/:id` | PATCH | `system.manage_settings` | Update counter value | +| `/preview` | POST | `correspondence.read` | Preview number without generating | +| `/reserve` | POST | `correspondence.create` | Reserve a document number | +| `/confirm` | POST | `correspondence.create` | Confirm a reservation | +| `/cancel` | POST | `correspondence.create` | Cancel a reservation | + +### 5.2 Admin Endpoints (`/admin/document-numbering`) + +| Endpoint | Method | Permission | Description | +| ------------------- | ------ | ------------------------ | ----------------------- | +| `/templates` | GET | `system.manage_settings` | Get all templates | +| `/templates` | POST | `system.manage_settings` | Create/update template | +| `/templates/:id` | DELETE | `system.manage_settings` | Delete template | +| `/metrics` | GET | `system.view_logs` | Get metrics | +| `/manual-override` | POST | `system.manage_settings` | Override counter value | +| `/void-and-replace` | POST | `system.manage_settings` | Void and replace number | +| `/cancel` | POST | `system.manage_settings` | Cancel a number | +| `/bulk-import` | POST | `system.manage_settings` | Bulk import counters | + +--- + +## 6. Monitoring & Observability + +### 6.1 Prometheus Metrics ```typescript -// File: src/modules/document-numbering/jobs/counter-reset.job.ts -import { Processor, WorkerHost } from '@nestjs/bullmq'; -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; - -@Processor('document-numbering') @Injectable() -export class CounterResetJob extends WorkerHost { - private readonly logger = new Logger(CounterResetJob.name); +export class NumberingMetrics { + // Counter: Total numbers generated + private readonly numbersGenerated = new Counter({ + name: 'numbering_sequences_total', + help: 'Total document numbers generated', + labelNames: ['document_type'], + }); - @Cron('0 0 1 1 *') // 1 Jan every year at 00:00 - async handleYearlyReset() { - const newYear = new Date().getFullYear(); + // Gauge: Sequence utilization (%) + private readonly sequenceUtilization = new Gauge({ + name: 'numbering_sequence_utilization', + help: 'Sequence utilization percentage', + labelNames: ['document_type'], + }); - // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว - // แค่เตรียม counter สำหรับปีใหม่ - this.logger.log(`Year changed to ${newYear}, counters are ready`); + // Histogram: Lock wait time + private readonly lockWaitTime = new Histogram({ + name: 'numbering_lock_wait_seconds', + help: 'Time spent waiting for lock acquisition', + labelNames: ['document_type'], + buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5], + }); - // สามารถทำ cleanup counter ปีเก่าได้ (optional) - // await this.cleanupOldCounters(newYear - 5); // เก็บ 5 ปี - } - - async process() { - // BullMQ job processing - } + // Counter: Lock failures + private readonly lockFailures = new Counter({ + name: 'numbering_lock_failures_total', + help: 'Total lock acquisition failures', + labelNames: ['document_type', 'reason'], + }); } ``` -## 5. API Controller +### 6.2 Alert Rules -### 5.1. Main Controller (`/document-numbering`) +| Alert | Condition | Severity | Action | +| ------------------ | ------------------ | -------- | ---------------------- | +| `SequenceCritical` | Utilization > 95% | Critical | Extend max_value | +| `SequenceWarning` | Utilization > 90% | Warning | Plan extension | +| `HighLockWaitTime` | p95 > 1s | Warning | Check Redis health | +| `RedisUnavailable` | Redis cluster down | Critical | Switch to DB-only mode | +| `HighErrorRate` | > 10 errors/sec | Warning | Check logs | -```typescript -// File: src/modules/document-numbering/document-numbering.controller.ts -import { - Controller, Get, Post, Patch, - Body, Param, Query, UseGuards, ParseIntPipe, -} from '@nestjs/common'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; -import { RbacGuard } from '../../common/guards/rbac.guard'; -import { RequirePermission } from '../../common/decorators/require-permission.decorator'; -import { DocumentNumberingService } from './document-numbering.service'; -import { PreviewNumberDto } from './dto/preview-number.dto'; +--- -@Controller('document-numbering') -@UseGuards(JwtAuthGuard, RbacGuard) -export class DocumentNumberingController { - constructor(private readonly numberingService: DocumentNumberingService) {} +## 7. Error Handling - // --- Logs --- +### 7.1 Error Codes - @Get('logs/audit') - @RequirePermission('system.view_logs') - getAuditLogs(@Query('limit') limit?: number) { - return this.numberingService.getAuditLogs(limit ? Number(limit) : 100); - } +| Code | Name | Description | +| ----- | --------------------------- | -------------------------- | +| NB001 | CONFIG_NOT_FOUND | Config not found for type | +| NB002 | SEQUENCE_EXHAUSTED | Sequence reached max value | +| NB003 | LOCK_TIMEOUT | Failed to acquire lock | +| NB004 | RESERVATION_EXPIRED | Reservation token expired | +| NB005 | DUPLICATE_NUMBER | Number already exists | +| NB006 | INVALID_FORMAT | Number format invalid | +| NB007 | MANUAL_OVERRIDE_NOT_ALLOWED | Manual override disabled | +| NB008 | REDIS_UNAVAILABLE | Redis connection failed | - @Get('logs/errors') - @RequirePermission('system.view_logs') - getErrorLogs(@Query('limit') limit?: number) { - return this.numberingService.getErrorLogs(limit ? Number(limit) : 100); - } +### 7.2 Fallback Strategy - // --- Sequences / Counters --- - - @Get('sequences') - @RequirePermission('correspondence.read') - getSequences(@Query('projectId') projectId?: number) { - return this.numberingService.getSequences(projectId ? Number(projectId) : undefined); - } - - @Patch('counters/:id') - @RequirePermission('system.manage_settings') - async updateCounter( - @Param('id', ParseIntPipe) id: number, - @Body('sequence') sequence: number - ) { - return this.numberingService.setCounterValue(id, sequence); - } - - // --- Preview --- - - @Post('preview') - @RequirePermission('correspondence.read') - async previewNumber(@Body() dto: PreviewNumberDto) { - return this.numberingService.previewNumber(dto); - } -} +```mermaid +flowchart TD + A[Generate Number Request] --> B{Redis Available?} + B -->|Yes| C[Acquire Redlock] + B -->|No| D[Use DB-only Lock] + C --> E{Lock Acquired?} + E -->|Yes| F[Increment Counter] + E -->|No| G{Retry < 3?} + G -->|Yes| C + G -->|No| H[Fallback to DB Lock] + D --> F + H --> F + F --> I[Format Number] + I --> J[Return Number] ``` -### 5.2. Admin Controller (`/admin/document-numbering`) +--- -```typescript -// File: src/modules/document-numbering/document-numbering-admin.controller.ts -import { - Controller, Get, Post, Delete, Body, Param, Query, - UseGuards, ParseIntPipe, -} from '@nestjs/common'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; -import { RbacGuard } from '../../common/guards/rbac.guard'; -import { RequirePermission } from '../../common/decorators/require-permission.decorator'; -import { DocumentNumberingService } from './document-numbering.service'; +## 8. Testing -@Controller('admin/document-numbering') -@UseGuards(JwtAuthGuard, RbacGuard) -export class DocumentNumberingAdminController { - constructor(private readonly service: DocumentNumberingService) {} - - // --- Template Management --- - - @Get('templates') - @RequirePermission('system.manage_settings') - async getTemplates(@Query('projectId') projectId?: number) { - if (projectId) { - return this.service.getTemplatesByProject(projectId); - } - return this.service.getTemplates(); - } - - @Post('templates') - @RequirePermission('system.manage_settings') - async saveTemplate(@Body() dto: any) { - return this.service.saveTemplate(dto); - } - - @Delete('templates/:id') - @RequirePermission('system.manage_settings') - async deleteTemplate(@Param('id', ParseIntPipe) id: number) { - await this.service.deleteTemplate(id); - return { success: true }; - } - - // --- Metrics --- - - @Get('metrics') - @RequirePermission('system.view_logs') - async getMetrics() { - const audit = await this.service.getAuditLogs(50); - const errors = await this.service.getErrorLogs(50); - return { audit, errors }; - } - - // --- Admin Operations --- - - @Post('manual-override') - @RequirePermission('system.manage_settings') - async manualOverride(@Body() dto: { - projectId: number; - correspondenceTypeId: number | null; - year: number; - newValue: number; - }) { - return this.service.manualOverride(dto); - } - - @Post('void-and-replace') - @RequirePermission('system.manage_settings') - async voidAndReplace(@Body() dto: { - documentId: number; - reason: string; - }) { - return this.service.voidAndReplace(dto); - } - - @Post('cancel') - @RequirePermission('system.manage_settings') - async cancelNumber(@Body() dto: { - documentNumber: string; - reason: string; - }) { - return this.service.cancelNumber(dto); - } - - @Post('bulk-import') - @RequirePermission('system.manage_settings') - async bulkImport(@Body() items: any[]) { - return this.service.bulkImport(items); - } -} +### 8.1 Unit Tests +```bash +# Run unit tests +pnpm test:watch -- --testPathPattern=document-numbering ``` -### 5.3. API Endpoints Summary - -| Endpoint | Method | Permission | Description | -| -------------------------------------------- | ------ | ------------------------ | --------------------------------- | -| `/document-numbering/logs/audit` | GET | `system.view_logs` | Get audit logs | -| `/document-numbering/logs/errors` | GET | `system.view_logs` | Get error logs | -| `/document-numbering/sequences` | GET | `correspondence.read` | Get counter sequences | -| `/document-numbering/counters/:id` | PATCH | `system.manage_settings` | Update counter value | -| `/document-numbering/preview` | POST | `correspondence.read` | Preview number without generating | -| `/admin/document-numbering/templates` | GET | `system.manage_settings` | Get all templates | -| `/admin/document-numbering/templates` | POST | `system.manage_settings` | Create/update template | -| `/admin/document-numbering/templates/:id` | DELETE | `system.manage_settings` | Delete template | -| `/admin/document-numbering/metrics` | GET | `system.view_logs` | Get metrics (audit + errors) | -| `/admin/document-numbering/manual-override` | POST | `system.manage_settings` | Override counter value | -| `/admin/document-numbering/void-and-replace` | POST | `system.manage_settings` | Void and replace number | -| `/admin/document-numbering/cancel` | POST | `system.manage_settings` | Cancel a number | -| `/admin/document-numbering/bulk-import` | POST | `system.manage_settings` | Bulk import counters | - -## 6. Module Configuration - -```typescript -// File: src/modules/document-numbering/document-numbering.module.ts -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { BullModule } from '@nestjs/bullmq'; -import { ThrottlerModule } from '@nestjs/throttler'; -import { DocumentNumberCounter } from './entities/document-number-counter.entity'; -import { DocumentNumberAudit } from './entities/document-number-audit.entity'; -import { DocumentNumberError } from './entities/document-number-error.entity'; -import { DocumentNumberingService } from './services/document-numbering.service'; -import { DocumentNumberingLockService } from './services/document-numbering-lock.service'; -import { CounterService } from './services/counter.service'; -import { AuditService } from './services/audit.service'; -import { TemplateValidator } from './validators/template.validator'; -import { CounterResetJob } from './jobs/counter-reset.job'; -import { DocumentNumberingController } from './controllers/document-numbering.controller'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - DocumentNumberCounter, - DocumentNumberAudit, - DocumentNumberError, - ]), - BullModule.registerQueue({ - name: 'document-numbering', - }), - ThrottlerModule.forRoot({ - ttl: 60, - limit: 10, - }), - ], - controllers: [DocumentNumberingController], - providers: [ - DocumentNumberingService, - DocumentNumberingLockService, - CounterService, - AuditService, - TemplateValidator, - CounterResetJob, - ], - exports: [DocumentNumberingService], -}) -export class DocumentNumberingModule {} +### 8.2 Integration Tests +```bash +# Run integration tests +pnpm test:e2e -- --testPathPattern=numbering ``` -## 7. Environment Configuration - +### 8.3 Concurrency Test ```typescript -// .env.example +// tests/load/concurrency.spec.ts +it('should handle 1000 concurrent requests without duplicates', async () => { + const promises = Array.from({ length: 1000 }, () => + request(app.getHttpServer()) + .post('/document-numbering/reserve') + .send({ document_type: 'COR' }) + ); + + const results = await Promise.all(promises); + const numbers = results.map(r => r.body.data.document_number); + const uniqueNumbers = new Set(numbers); + + expect(uniqueNumbers.size).toBe(1000); +}); +``` + +--- + +## 9. Best Practices + +### 9.1 DO's ✅ +- ✅ Always use two-phase commit (reserve + confirm) +- ✅ Implement fallback to DB-only if Redis fails +- ✅ Log every operation to audit trail +- ✅ Monitor sequence utilization (alert at 90%) +- ✅ Test under concurrent load (1000+ req/s) +- ✅ Use pessimistic locking in database +- ✅ Set reasonable TTL for reservations (5 min) +- ✅ Validate manual override format +- ✅ Skip cancelled numbers (never reuse) + +### 9.2 DON'Ts ❌ +- ❌ Never skip validation for manual override +- ❌ Never reuse cancelled numbers +- ❌ Never trust client-generated numbers +- ❌ Never increase sequence without transaction +- ❌ Never deploy without load testing +- ❌ Never modify sequence table directly +- ❌ Never skip audit logging + +--- + +## 10. Environment Variables + +```bash +# Redis Configuration REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= +REDIS_CLUSTER_NODES=redis-1:6379,redis-2:6379,redis-3:6379 +# Database DB_HOST=localhost DB_PORT=3306 DB_USERNAME=lcbp3 @@ -802,12 +733,28 @@ DB_PASSWORD= DB_DATABASE=lcbp3_db DB_POOL_SIZE=20 -# Prometheus +# Numbering Configuration +NUMBERING_LOCK_TIMEOUT=5000 # 5 seconds +NUMBERING_RESERVATION_TTL=300 # 5 minutes +NUMBERING_RETRY_ATTEMPTS=3 +NUMBERING_RETRY_DELAY=200 # milliseconds + +# Monitoring PROMETHEUS_PORT=9090 +GRAFANA_PORT=3000 ``` +--- + ## References -- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) -- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) -- [Backend Guidelines](file:///e:/np-dms/lcbp3/specs/03-implementation/backend-guidelines.md) +- [Requirements](file:///d:/nap-dms.lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Operations Guide](file:///d:/nap-dms.lcbp3/specs/04-operations/document-numbering-operations.md) +- [ADR-018 Document Numbering](file:///d:/nap-dms.lcbp3/specs/05-decisions/adr-018-document-numbering.md) +- [Backend Guidelines](file:///d:/nap-dms.lcbp3/specs/03-implementation/backend-guidelines.md) + +--- + +**Document Version**: 2.0.0 +**Created By**: Development Team +**Last Updated**: 2025-12-17 diff --git a/specs/03-implementation/fullftack-js-v1.5.0.md b/specs/03-implementation/fullftack-js-v1.6.2.md similarity index 99% rename from specs/03-implementation/fullftack-js-v1.5.0.md rename to specs/03-implementation/fullftack-js-v1.6.2.md index 09fec5c..f8475ec 100644 --- a/specs/03-implementation/fullftack-js-v1.5.0.md +++ b/specs/03-implementation/fullftack-js-v1.6.2.md @@ -1,8 +1,8 @@ -# 📝 **Documents Management System Version 1.5.0: แนวทางการพัฒนา FullStackJS** +# 📝 **Documents Management System Version 1.6.1: แนวทางการพัฒนา FullStackJS** **สถานะ:** first-draft -**วันที่:** 2025-12-01 -**อ้างอิง:** Requirements Specification v1.5.0 +**วันที่:** 2025-12-17 +**อ้างอิง:** Requirements Specification v1.6.1 **Classification:** Internal Technical Documentation ## 🧠 **1. ปรัชญาทั่วไป (General Philosophy)** @@ -1082,9 +1082,9 @@ Views เหล่านี้ทำหน้าที่เป็นแหล ## **Document Control:** -- **Document:** FullStackJS v1.5.0 -- **Version:** 1.5 -- **Date:** 2025-12-01 +- **Document:** FullStackJS v1.6.1 +- **Version:** 1.6 +- **Date:** 2025-12-17 - **Author:** NAP LCBP3-DMS & Gemini - **Status:** first-draft - **Classification:** Internal Technical Documentation @@ -1092,4 +1092,4 @@ Views เหล่านี้ทำหน้าที่เป็นแหล --- -`End of FullStackJS Guidelines v1.5.0` +`End of FullStackJS Guidelines v1.6.1` diff --git a/specs/04-operations/document-numbering-operations.md b/specs/04-operations/document-numbering-operations.md index 36c0133..4e0a2e6 100644 --- a/specs/04-operations/document-numbering-operations.md +++ b/specs/04-operations/document-numbering-operations.md @@ -2,14 +2,15 @@ --- title: 'Operations Guide: Document Numbering System' -version: 1.6.0 -status: draft +version: 1.6.2 +status: APPROVED owner: Operations Team -last_updated: 2025-12-02 +last_updated: 2025-12-17 related: - specs/01-requirements/03.11-document-numbering.md - specs/03-implementation/document-numbering.md - specs/04-operations/monitoring-alerting.md + - specs/05-decisions/ADR-002-document-numbering-strategy.md --- ## Overview @@ -678,7 +679,8 @@ See: [Backup & Recovery Guide](file:///e:/np-dms/lcbp3/specs/04-operations/backu ## References -- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) -- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) -- [Monitoring & Alerting](file:///e:/np-dms/lcbp3/specs/04-operations/monitoring-alerting.md) -- [Incident Response](file:///e:/np-dms/lcbp3/specs/04-operations/incident-response.md) +- [Requirements](file:///d:/nap-dms.lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Implementation Guide](file:///d:/nap-dms.lcbp3/specs/03-implementation/document-numbering.md) +- [ADR-002 Document Numbering Strategy](file:///d:/nap-dms.lcbp3/specs/05-decisions/ADR-002-document-numbering-strategy.md) +- [Monitoring & Alerting](file:///d:/nap-dms.lcbp3/specs/04-operations/monitoring-alerting.md) +- [Incident Response](file:///d:/nap-dms.lcbp3/specs/04-operations/incident-response.md) diff --git a/specs/05-decisions/ADR-002-document-numbering-strategy.md b/specs/05-decisions/ADR-002-document-numbering-strategy.md index ec94245..413a840 100644 --- a/specs/05-decisions/ADR-002-document-numbering-strategy.md +++ b/specs/05-decisions/ADR-002-document-numbering-strategy.md @@ -214,11 +214,11 @@ The system resolves the numbering format using the following priority: 2. **Default Format:** If not found, search for a record with matching `project_id` where `correspondence_type_id` is `NULL`. 3. **System Fallback:** If neither exists, use the hardcoded system default: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`. -| Priority | Scenario | Template Source | Counter Scope (Key) | Reset Behavior | -| --- | --- | --- | --- | --- | -| 1 | Specific Format Found | Database (project_id, type_id) | Specific Type (type_id) | Based on reset_sequence_yearly flag | -| 2 | Default Format Found | Database (project_id, type_id=NULL) | Shared Counter (type_id=NULL) | Based on reset_sequence_yearly flag | -| 3 | Fallback (No Config) | System Default: {ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE} | Shared Counter (type_id=NULL) | Reset Yearly (Default: True) | +| Priority | Scenario | Template Source | Counter Scope (Key) | Reset Behavior | +| -------- | --------------------- | --------------------------------------------------- | ----------------------------- | ----------------------------------- | +| 1 | Specific Format Found | Database (project_id, type_id) | Specific Type (type_id) | Based on reset_sequence_yearly flag | +| 2 | Default Format Found | Database (project_id, type_id=NULL) | Shared Counter (type_id=NULL) | Based on reset_sequence_yearly flag | +| 3 | Fallback (No Config) | System Default: {ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE} | Shared Counter (type_id=NULL) | Reset Yearly (Default: True) | ### Format Examples by Document Type @@ -934,9 +934,9 @@ ensure: เป็นไปตาม: -- ✅ [Requirements 3.11](../01-requirements/03.11-document-numbering.md) - Document Numbering Management (v1.5.0) -- ✅ [Backend Plan Section 4.2.10](../../docs/2_Backend_Plan_V1_4_5.md) - DocumentNumberingModule -- ✅ [Data Dictionary](../../docs/4_Data_Dictionary_V1_4_4.md) - Counter Tables +- ✅ [Requirements 3.11](../01-requirements/03.11-document-numbering.md) - Document Numbering Management (v1.6.2) +- ✅ [Implementation Guide](../03-implementation/document-numbering.md) - DocumentNumberingModule (v1.6.1) +- ✅ [Operations Guide](../04-operations/document-numbering-operations.md) - Monitoring & Troubleshooting - ✅ [Security Best Practices](../02-architecture/security-architecture.md) - Rate Limiting, Audit Logging --- @@ -961,7 +961,8 @@ ensure: ## Version History -| Version | Date | Changes | -| ------- | ---------- | ------------------------------------------------------------------------------------- | -| 1.0 | 2025-11-30 | Initial decision | -| 2.0 | 2025-12-02 | Updated with comprehensive error scenarios, monitoring, security, and all token types | +| Version | Date | Changes | +| ------- | ---------- | ------------------------------------------------------------------------------------------------- | +| 1.0 | 2025-11-30 | Initial decision | +| 2.0 | 2025-12-02 | Updated with comprehensive error scenarios, monitoring, security, and all token types | +| 3.0 | 2025-12-17 | Aligned with Requirements v1.6.2: updated counter schema, token definitions, Number State Machine | diff --git a/specs/06-tasks/TASK-BE-017-document-numbering-refactor.md b/specs/06-tasks/TASK-BE-017-document-numbering-refactor.md new file mode 100644 index 0000000..69c8404 --- /dev/null +++ b/specs/06-tasks/TASK-BE-017-document-numbering-refactor.md @@ -0,0 +1,166 @@ +# TASK-BE-017: Document Numbering Backend Refactor + +--- +status: TODO +priority: HIGH +estimated_effort: 3-5 days +dependencies: + - specs/01-requirements/03.11-document-numbering.md (v1.6.2) + - specs/03-implementation/document-numbering.md (v1.6.2) +related_task: TASK-FE-017-document-numbering-refactor.md +--- + +## Objective + +Refactor Document Numbering module ตาม specification v1.6.2 โดยเน้น: +- Number State Machine (RESERVED → CONFIRMED → VOID → CANCELLED) +- Idempotency-Key support +- Counter Key alignment ตาม requirements + +--- + +## Implementation Checklist + +### 1. Entity Updates + +#### 1.1 DocumentNumberCounter Entity +- [ ] Rename `current_year` → ใช้ `reset_scope` pattern (YEAR_2025, NONE) +- [ ] Ensure FK columns match: `correspondence_type_id`, `originator_organization_id`, `recipient_organization_id` +- [ ] Add `rfa_type_id`, `sub_type_id`, `discipline_id` columns if missing +- [ ] Update Primary Key ให้ตรงกับ requirements spec + +```typescript +// Expected Counter Key structure +interface CounterKey { + projectId: number; + originatorOrganizationId: number; + recipientOrganizationId: number; // 0 for RFA + correspondenceTypeId: number; + subTypeId: number; // 0 if not applicable + rfaTypeId: number; // 0 if not applicable + disciplineId: number; // 0 if not applicable + resetScope: string; // 'YEAR_2025', 'NONE' +} +``` + +#### 1.2 DocumentNumberAudit Entity +- [ ] Add `operation` enum: `RESERVE`, `CONFIRM`, `CANCEL`, `MANUAL_OVERRIDE`, `VOID`, `GENERATE` +- [ ] Ensure `counter_key` is stored as JSON +- [ ] Add `idempotency_key` column + +#### 1.3 DocumentNumberReservation Entity (NEW if not exists) +- [ ] Create entity for Two-Phase Commit reservations +- [ ] Fields: `token`, `document_number`, `status`, `expires_at`, `metadata` + +--- + +### 2. Service Updates + +#### 2.1 DocumentNumberingService +- [ ] Implement `reserveNumber()` - Phase 1 of Two-Phase Commit +- [ ] Implement `confirmNumber()` - Phase 2 of Two-Phase Commit +- [ ] Implement `cancelNumber()` - Explicit cancel reservation +- [ ] Add Idempotency-Key checking logic +- [ ] Update `generateNextNumber()` to use new CounterKey structure + +#### 2.2 Counter Key Builder +- [ ] Create helper to build counter key based on document type: + - Global (LETTER, MEMO, RFI): `(project, orig, recip, type, 0, 0, 0, YEAR_XXXX)` + - TRANSMITTAL: `(project, orig, recip, type, subType, 0, 0, YEAR_XXXX)` + - RFA: `(project, orig, 0, type, 0, rfaType, discipline, NONE)` + +#### 2.3 ManualOverrideService +- [ ] Implement `manualOverride()` with validation +- [ ] Auto-update counter if manual number > current + +#### 2.4 VoidReplaceService +- [ ] Implement `voidAndReplace()` workflow +- [ ] Link new document to voided document + +--- + +### 3. Controller Updates + +#### 3.1 DocumentNumberingController +- [ ] Add `POST /reserve` endpoint +- [ ] Add `POST /confirm` endpoint +- [ ] Add `POST /cancel` endpoint +- [ ] Add `Idempotency-Key` header validation middleware + +#### 3.2 DocumentNumberingAdminController +- [ ] Add `POST /manual-override` endpoint +- [ ] Add `POST /void-and-replace` endpoint +- [ ] Add `POST /bulk-import` endpoint +- [ ] Add `GET /metrics` endpoint for monitoring dashboard + +--- + +### 4. Number State Machine + +```mermaid +stateDiagram-v2 + [*] --> RESERVED: reserve() + RESERVED --> CONFIRMED: confirm() + RESERVED --> CANCELLED: cancel() or TTL expired + CONFIRMED --> VOID: void() + CANCELLED --> [*] + VOID --> [*] +``` + +#### 4.1 State Transitions +- [ ] Implement state validation before transitions +- [ ] Log all transitions to audit table +- [ ] TTL 5 minutes for RESERVED state + +--- + +### 5. Testing + +#### 5.1 Unit Tests +- [ ] CounterService.incrementCounter() +- [ ] ReservationService.reserve/confirm/cancel() +- [ ] TemplateValidator.validate() +- [ ] CounterKeyBuilder + +#### 5.2 Integration Tests +- [ ] Two-Phase Commit flow +- [ ] Idempotency-Key duplicate prevention +- [ ] Redis lock + DB optimistic lock + +#### 5.3 Load Tests +- [ ] Concurrent number generation (1000 req/s) +- [ ] Zero duplicates verification + +--- + +## Files to Create/Modify + +| Action | Path | +| ------ | ------------------------------------------------------------------------------------------- | +| MODIFY | `backend/src/modules/document-numbering/entities/document-number-counter.entity.ts` | +| MODIFY | `backend/src/modules/document-numbering/entities/document-number-audit.entity.ts` | +| CREATE | `backend/src/modules/document-numbering/entities/document-number-reservation.entity.ts` | +| MODIFY | `backend/src/modules/document-numbering/services/document-numbering.service.ts` | +| CREATE | `backend/src/modules/document-numbering/services/reservation.service.ts` | +| CREATE | `backend/src/modules/document-numbering/services/manual-override.service.ts` | +| MODIFY | `backend/src/modules/document-numbering/controllers/document-numbering.controller.ts` | +| MODIFY | `backend/src/modules/document-numbering/controllers/document-numbering-admin.controller.ts` | +| CREATE | `backend/src/modules/document-numbering/guards/idempotency.guard.ts` | + +--- + +## Acceptance Criteria + +- [ ] All Counter Key ตรงกับ requirements v1.6.2 +- [ ] Number State Machine ทำงานถูกต้อง +- [ ] Idempotency-Key ป้องกัน duplicate requests +- [ ] Zero duplicate numbers ใน concurrent load test +- [ ] Audit logs บันทึกทุก operation + +--- + +## References + +- [Requirements v1.6.2](file:///d:/nap-dms.lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Implementation Guide v1.6.2](file:///d:/nap-dms.lcbp3/specs/03-implementation/document-numbering.md) +- [ADR-002](file:///d:/nap-dms.lcbp3/specs/05-decisions/ADR-002-document-numbering-strategy.md) diff --git a/specs/06-tasks/TASK-FE-017-document-numbering-refactor.md b/specs/06-tasks/TASK-FE-017-document-numbering-refactor.md new file mode 100644 index 0000000..821adb6 --- /dev/null +++ b/specs/06-tasks/TASK-FE-017-document-numbering-refactor.md @@ -0,0 +1,192 @@ +# TASK-FE-017: Document Numbering Frontend Refactor + +--- +status: TODO +priority: HIGH +estimated_effort: 2-3 days +dependencies: + - TASK-BE-017-document-numbering-refactor.md + - specs/01-requirements/03.11-document-numbering.md (v1.6.2) + - specs/03-implementation/document-numbering.md (v1.6.2) +--- + +## Objective + +Refactor Frontend Document Numbering ตาม specification v1.6.2: +- ป้องกัน User แก้ไขเลขที่เอกสาร +- สร้าง Admin Dashboard ด้วย Metrics +- Implement Admin Tools (Manual Override, Void/Replace) + +--- + +## Implementation Checklist + +### 1. User Mode Forms (Create/Edit) + +#### 1.1 Correspondence Form +- [ ] **Create Mode**: แสดง "Auto Generated" หรือ Preview เลขที่เอกสาร +- [ ] **Edit Mode**: ช่อง Document No เป็น **Read-Only** เสมอ +- [ ] **API Integration**: ตัดการส่ง field `documentNumber` ไป Backend ใน Edit mode + +#### 1.2 RFA Form +- [ ] Same as above - Read-Only document number + +#### 1.3 Transmittal Form +- [ ] Same as above - Read-Only document number + +**Files:** +- `frontend/components/correspondences/form.tsx` +- `frontend/components/rfas/form.tsx` +- `frontend/components/transmittals/form.tsx` + +--- + +### 2. Admin Dashboard (`/admin/numbering`) + +#### 2.1 Tab Structure +``` +/admin/numbering +├── Templates (existing - keep as is) +├── Metrics & Audit (NEW) +└── Admin Tools (NEW) +``` + +#### 2.2 Templates Tab (Existing) +- [ ] Keep current functionality +- [ ] เป็น Tab แรก (default) + +#### 2.3 Metrics & Audit Tab (NEW) +- [ ] Fetch metrics from `GET /admin/document-numbering/metrics` +- [ ] Display: + - Sequence utilization gauge + - Lock wait time chart + - Generation rate chart + - Recent errors table + - Audit logs table with filters + +#### 2.4 Admin Tools Tab (NEW) +- [ ] **Manual Override Form**: + - Input: document_type, document_number, reason + - Calls `POST /admin/document-numbering/manual-override` + +- [ ] **Void & Replace Form**: + - Input: document_id, reason + - Calls `POST /admin/document-numbering/void-and-replace` + +- [ ] **Bulk Import Form**: + - Upload CSV/Excel file + - Preview before import + - Calls `POST /admin/document-numbering/bulk-import` + +--- + +### 3. API Integration + +#### 3.1 New API Endpoints +```typescript +// services/document-numbering.service.ts (frontend) + +interface NumberingMetrics { + sequenceUtilization: number; + lockWaitTimeP95: number; + generationRate: number; + recentErrors: ErrorEntry[]; +} + +// GET /admin/document-numbering/metrics +getMetrics(): Promise + +// POST /admin/document-numbering/manual-override +manualOverride(dto: ManualOverrideDto): Promise + +// POST /admin/document-numbering/void-and-replace +voidAndReplace(dto: VoidReplaceDto): Promise<{ newDocumentNumber: string }> + +// POST /admin/document-numbering/bulk-import +bulkImport(file: File): Promise + +// GET /document-numbering/logs/audit +getAuditLogs(params: AuditQueryParams): Promise +``` + +#### 3.2 DTOs +```typescript +interface ManualOverrideDto { + documentType: string; + documentNumber: string; + reason: string; +} + +interface VoidReplaceDto { + documentId: number; + reason: string; +} + +interface AuditQueryParams { + operation?: 'RESERVE' | 'CONFIRM' | 'CANCEL' | 'MANUAL_OVERRIDE' | 'VOID' | 'GENERATE'; + dateFrom?: string; + dateTo?: string; + userId?: number; + page?: number; + limit?: number; +} +``` + +--- + +### 4. Components to Create + +| Component | Path | Description | +| ------------------ | ----------------------------------------------- | --------------------------- | +| MetricsDashboard | `components/numbering/metrics-dashboard.tsx` | Metrics charts and gauges | +| AuditLogsTable | `components/numbering/audit-logs-table.tsx` | Filterable audit log viewer | +| ManualOverrideForm | `components/numbering/manual-override-form.tsx` | Admin tool form | +| VoidReplaceForm | `components/numbering/void-replace-form.tsx` | Admin tool form | +| BulkImportForm | `components/numbering/bulk-import-form.tsx` | CSV/Excel uploader | + +--- + +### 5. UI/UX Requirements + +#### 5.1 Document Number Display +- ใช้ Badge หรือ Chip style สำหรับ Document Number +- สี: Info (blue) สำหรับ Auto-generated +- สี: Warning (amber) สำหรับ Manual Override +- สี: Destructive (red) สำหรับ Voided + +#### 5.2 Admin Tools Access Control +- ซ่อน Admin Tools tab สำหรับ users ที่ไม่มี permission `system.manage_settings` +- แสดง confirmation dialog ก่อน Manual Override / Void + +--- + +## Files to Create/Modify + +| Action | Path | +| ------ | -------------------------------------------------------- | +| MODIFY | `frontend/app/(admin)/admin/numbering/page.tsx` | +| CREATE | `frontend/components/numbering/metrics-dashboard.tsx` | +| CREATE | `frontend/components/numbering/audit-logs-table.tsx` | +| CREATE | `frontend/components/numbering/manual-override-form.tsx` | +| CREATE | `frontend/components/numbering/void-replace-form.tsx` | +| CREATE | `frontend/components/numbering/bulk-import-form.tsx` | +| MODIFY | `frontend/services/document-numbering.service.ts` | +| MODIFY | `frontend/components/correspondences/form.tsx` | + +--- + +## Acceptance Criteria + +- [ ] Document Number เป็น Read-Only ใน Edit mode ทุก form +- [ ] Admin Dashboard แสดง Metrics ได้ถูกต้อง +- [ ] Manual Override ทำงานได้และบันทึก Audit +- [ ] Void/Replace สร้างเลขใหม่และ link กับเอกสารเดิม +- [ ] Permission check ถูกต้องสำหรับ Admin Tools + +--- + +## References + +- [Requirements v1.6.2](file:///d:/nap-dms.lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Frontend Guidelines](file:///d:/nap-dms.lcbp3/specs/03-implementation/frontend-guidelines.md) +- [REQ-009 Original Task](file:///d:/nap-dms.lcbp3/specs/06-tasks/REQ-009-DocumentNumbering.md) diff --git a/infrastructure/Git_command.md b/specs/08-infrastructure/Git_command.md similarity index 100% rename from infrastructure/Git_command.md rename to specs/08-infrastructure/Git_command.md diff --git a/infrastructure/Gitea_setting.md b/specs/08-infrastructure/Gitea_setting.md similarity index 100% rename from infrastructure/Gitea_setting.md rename to specs/08-infrastructure/Gitea_setting.md diff --git a/specs/08-infrastructure/Infrastructure Setup.md b/specs/08-infrastructure/Infrastructure Setup.md new file mode 100644 index 0000000..ecbec13 --- /dev/null +++ b/specs/08-infrastructure/Infrastructure Setup.md @@ -0,0 +1,957 @@ +# Infrastructure Setup + +## 1. Redis Cluster Configuration + +### 1.1 Docker Compose Setup +```yaml +# docker-compose-redis.yml +version: '3.8' + +services: + redis-1: + image: redis:7-alpine + container_name: lcbp3-redis-1 + command: redis-server --port 6379 --cluster-enabled yes --cluster-config-file nodes.conf + ports: + - "6379:6379" + - "16379:16379" + volumes: + - redis-1-data:/data + networks: + - lcbp3-network + restart: unless-stopped + + redis-2: + image: redis:7-alpine + container_name: lcbp3-redis-2 + command: redis-server --port 6379 --cluster-enabled yes --cluster-config-file nodes.conf + ports: + - "6380:6379" + - "16380:16379" + volumes: + - redis-2-data:/data + networks: + - lcbp3-network + restart: unless-stopped + + redis-3: + image: redis:7-alpine + container_name: lcbp3-redis-3 + command: redis-server --port 6379 --cluster-enabled yes --cluster-config-file nodes.conf + ports: + - "6381:6379" + - "16381:16379" + volumes: + - redis-3-data:/data + networks: + - lcbp3-network + restart: unless-stopped + +volumes: + redis-1-data: + redis-2-data: + redis-3-data: + +networks: + lcbp3-network: + external: true +``` + +#### Initialize Cluster +```bash +# Start Redis nodes +docker-compose -f docker-compose-redis.yml up -d + +# Wait for nodes to start +sleep 10 + +# Create cluster +docker exec -it lcbp3-redis-1 redis-cli --cluster create \ + 172.20.0.2:6379 \ + 172.20.0.3:6379 \ + 172.20.0.4:6379 \ + --cluster-replicas 0 + +# Verify cluster +docker exec -it lcbp3-redis-1 redis-cli cluster info +docker exec -it lcbp3-redis-1 redis-cli cluster nodes +``` + +#### Health Check Script +```bash +#!/bin/bash +# scripts/check-redis-cluster.sh + +echo "🔍 Checking Redis Cluster Health..." + +for port in 6379 6380 6381; do + echo "\n📍 Node on port $port:" + + # Check if node is up + docker exec lcbp3-redis-$(($port - 6378)) redis-cli -p 6379 ping + + # Check cluster status + docker exec lcbp3-redis-$(($port - 6378)) redis-cli -p 6379 cluster info | grep cluster_state + + # Check memory usage + docker exec lcbp3-redis-$(($port - 6378)) redis-cli -p 6379 info memory | grep used_memory_human +done + +echo "\n✅ Cluster check complete" +``` + +--- + +## 2. Database Configuration + +### 2.1 MariaDB Optimization for Numbering +```sql +-- /etc/mysql/mariadb.conf.d/50-numbering.cnf + +[mysqld] +# Connection pool +max_connections = 200 +thread_cache_size = 50 + +# Query cache (disabled for InnoDB) +query_cache_type = 0 +query_cache_size = 0 + +# InnoDB settings +innodb_buffer_pool_size = 4G +innodb_log_file_size = 512M +innodb_flush_log_at_trx_commit = 1 +innodb_lock_wait_timeout = 50 + +# Performance Schema +performance_schema = ON +performance_schema_instrument = 'wait/lock/%=ON' + +# Binary logging +log_bin = /var/log/mysql/mysql-bin.log +expire_logs_days = 7 +max_binlog_size = 100M + +# Slow query log +slow_query_log = 1 +slow_query_log_file = /var/log/mysql/slow-query.log +long_query_time = 1 +``` + +### 2.2 Monitoring Locks +```sql +-- Check for lock contention +SELECT + r.trx_id waiting_trx_id, + r.trx_mysql_thread_id waiting_thread, + r.trx_query waiting_query, + b.trx_id blocking_trx_id, + b.trx_mysql_thread_id blocking_thread, + b.trx_query blocking_query +FROM information_schema.innodb_lock_waits w +INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id +INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id; + +-- Check active transactions +SELECT * FROM information_schema.innodb_trx; + +-- Kill long-running transaction (if needed) +KILL ; +``` + +--- + +## 3. Backend Service Configuration + +### 3.1 Backend Service Deployment + +#### Docker Compose +```yaml +# docker-compose-backend.yml +version: '3.8' + +services: + backend-1: + image: lcbp3-backend:latest + container_name: lcbp3-backend-1 + environment: + - NODE_ENV=production + - DB_HOST=mariadb-primary + - REDIS_CLUSTER_NODES=redis-1:6379,redis-2:6379,redis-3:6379 + - NUMBERING_LOCK_TIMEOUT=5000 + - NUMBERING_RESERVATION_TTL=300 + ports: + - "3001:3000" + depends_on: + - mariadb-primary + - redis-1 + - redis-2 + - redis-3 + networks: + - lcbp3-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + + backend-2: + image: lcbp3-backend:latest + container_name: lcbp3-backend-2 + environment: + - NODE_ENV=production + - DB_HOST=mariadb-primary + - REDIS_CLUSTER_NODES=redis-1:6379,redis-2:6379,redis-3:6379 + ports: + - "3002:3000" + depends_on: + - mariadb-primary + - redis-1 + networks: + - lcbp3-network + restart: unless-stopped + +networks: + lcbp3-network: + external: true +``` + +#### Health Check Endpoint +```typescript +// health/numbering.health.ts +import { Injectable } from '@nestjs/common'; +import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus'; +import { Redis } from 'ioredis'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class NumberingHealthIndicator extends HealthIndicator { + constructor( + private redis: Redis, + private dataSource: DataSource, + ) { + super(); + } + + async isHealthy(key: string): Promise { + const checks = await Promise.all([ + this.checkRedis(), + this.checkDatabase(), + this.checkSequenceIntegrity(), + ]); + + const isHealthy = checks.every((check) => check.status === 'up'); + + return this.getStatus(key, isHealthy, { checks }); + } + + private async checkRedis(): Promise { + try { + await this.redis.ping(); + return { name: 'redis', status: 'up' }; + } catch (error) { + return { name: 'redis', status: 'down', error: error.message }; + } + } + + private async checkDatabase(): Promise { + try { + await this.dataSource.query('SELECT 1'); + return { name: 'database', status: 'up' }; + } catch (error) { + return { name: 'database', status: 'down', error: error.message }; + } + } + + private async checkSequenceIntegrity(): Promise { + try { + const result = await this.dataSource.query(` + SELECT COUNT(*) as count + FROM document_numbering_sequences + WHERE current_value > ( + SELECT max_value FROM document_numbering_configs + WHERE id = config_id + ) + `); + + const hasIssue = result[0].count > 0; + + return { + name: 'sequence_integrity', + status: hasIssue ? 'degraded' : 'up', + exceeded_sequences: result[0].count, + }; + } catch (error) { + return { name: 'sequence_integrity', status: 'down', error: error.message }; + } + } +} +``` + +--- + +## 4. Monitoring & Alerting + +### 4.1 Prometheus Configuration + +```yaml +# prometheus.yml +global: + scrape_interval: 15s + evaluation_interval: 15s + +alerting: + alertmanagers: + - static_configs: + - targets: + - alertmanager:9093 + +rule_files: + - "/etc/prometheus/alerts/numbering.yml" + +scrape_configs: + - job_name: 'backend' + static_configs: + - targets: + - 'backend-1:3000' + - 'backend-2:3000' + metrics_path: '/metrics' + + - job_name: 'redis-numbering' + static_configs: + - targets: + - 'redis-1:6379' + - 'redis-2:6379' + - 'redis-3:6379' + metrics_path: '/metrics' + + - job_name: 'mariadb' + static_configs: + - targets: + - 'mariadb-exporter:9104' +``` + +### 4.2 Alert Manager Configuration + +```yaml +# alertmanager.yml +global: + resolve_timeout: 5m + +route: + receiver: 'default' + group_by: ['alertname', 'severity'] + group_wait: 10s + group_interval: 10s + repeat_interval: 12h + + routes: + - match: + severity: critical + receiver: 'critical' + continue: true + + - match: + severity: warning + receiver: 'warning' + +receivers: + - name: 'default' + slack_configs: + - api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK' + channel: '#lcbp3-alerts' + title: '{{ .GroupLabels.alertname }}' + text: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}' + + - name: 'critical' + email_configs: + - to: 'devops@lcbp3.com' + from: 'alerts@lcbp3.com' + smarthost: 'smtp.gmail.com:587' + auth_username: 'alerts@lcbp3.com' + auth_password: 'your-password' + headers: + Subject: '🚨 CRITICAL: {{ .GroupLabels.alertname }}' + + pagerduty_configs: + - service_key: 'YOUR_PAGERDUTY_KEY' + + - name: 'warning' + slack_configs: + - api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK' + channel: '#lcbp3-warnings' +``` + +### 4.3 Grafana Dashboards + +#### Import Dashboard JSON +```bash +# Download dashboard template +curl -o numbering-dashboard.json \ + https://raw.githubusercontent.com/lcbp3/grafana-dashboards/main/numbering.json + +# Import to Grafana +curl -X POST http://admin:admin@localhost:3000/api/dashboards/db \ + -H "Content-Type: application/json" \ + -d @numbering-dashboard.json +``` + +#### Key Panels to Monitor +1. **Numbers Generated per Minute** - Rate of number creation +2. **Sequence Utilization** - Current usage vs max (alert >90%) +3. **Lock Wait Time (p95)** - Performance indicator +4. **Lock Failures** - System health indicator +5. **Redis Cluster Health** - Node status +6. **Database Connection Pool** - Resource usage + +--- + +## 5. Backup & Recovery + +### 5.1 Database Backup Strategy + +#### Automated Backup Script +```bash +#!/bin/bash +# scripts/backup-numbering-db.sh + +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/backups/numbering" +DB_NAME="lcbp3_production" + +echo "🔄 Starting backup at $DATE" + +# Create backup directory +mkdir -p $BACKUP_DIR + +# Backup numbering tables only +docker exec lcbp3-mariadb mysqldump \ + --single-transaction \ + --routines \ + --triggers \ + $DB_NAME \ + document_numbering_configs \ + document_numbering_sequences \ + document_numbering_audit_logs \ + > $BACKUP_DIR/numbering_$DATE.sql + +# Compress backup +gzip $BACKUP_DIR/numbering_$DATE.sql + +# Keep only last 30 days +find $BACKUP_DIR -name "numbering_*.sql.gz" -mtime +30 -delete + +echo "✅ Backup complete: numbering_$DATE.sql.gz" +``` + +#### Cron Schedule +```cron +# Run backup daily at 2 AM +0 2 * * * /opt/lcbp3/scripts/backup-numbering-db.sh >> /var/log/numbering-backup.log 2>&1 + +# Run integrity check weekly on Sunday at 3 AM +0 3 * * 0 /opt/lcbp3/scripts/check-sequence-integrity.sh >> /var/log/numbering-integrity.log 2>&1 +``` + +### 5.2 Redis Backup + +#### Enable RDB Persistence +```conf +# redis.conf +save 900 1 # Save if 1 key changed after 900 seconds +save 300 10 # Save if 10 keys changed after 300 seconds +save 60 10000 # Save if 10000 keys changed after 60 seconds + +dbfilename dump.rdb +dir /data + +# Enable AOF for durability +appendonly yes +appendfilename "appendonly.aof" +appendfsync everysec +``` + +#### Backup Script +```bash +#!/bin/bash +# scripts/backup-redis.sh + +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/backups/redis" + +mkdir -p $BACKUP_DIR + +for i in 1 2 3; do + echo "Backing up redis-$i..." + + # Trigger BGSAVE + docker exec lcbp3-redis-$i redis-cli -p 6379 BGSAVE + + # Wait for save to complete + sleep 10 + + # Copy RDB file + docker cp lcbp3-redis-$i:/data/dump.rdb \ + $BACKUP_DIR/redis-${i}_${DATE}.rdb + + # Copy AOF file + docker cp lcbp3-redis-$i:/data/appendonly.aof \ + $BACKUP_DIR/redis-${i}_${DATE}.aof +done + +# Compress +tar -czf $BACKUP_DIR/redis_cluster_${DATE}.tar.gz $BACKUP_DIR/*_${DATE}.* + +# Cleanup +rm $BACKUP_DIR/*_${DATE}.rdb $BACKUP_DIR/*_${DATE}.aof + +echo "✅ Redis backup complete" +``` + +### 5.3 Recovery Procedures + +#### Scenario 1: Restore from Database Backup +```bash +#!/bin/bash +# scripts/restore-numbering-db.sh + +BACKUP_FILE=$1 + +if [ -z "$BACKUP_FILE" ]; then + echo "Usage: ./restore-numbering-db.sh " + exit 1 +fi + +echo "⚠️ WARNING: This will overwrite current numbering data!" +read -p "Continue? (yes/no): " confirm + +if [ "$confirm" != "yes" ]; then + echo "Aborted" + exit 0 +fi + +# Decompress if needed +if [[ $BACKUP_FILE == *.gz ]]; then + gunzip -c $BACKUP_FILE > /tmp/restore.sql + RESTORE_FILE="/tmp/restore.sql" +else + RESTORE_FILE=$BACKUP_FILE +fi + +# Restore +docker exec -i lcbp3-mariadb mysql lcbp3_production < $RESTORE_FILE + +echo "✅ Restore complete" +echo "🔄 Please verify sequence integrity" +``` + +#### Scenario 2: Redis Node Failure +```bash +# Automatically handled by cluster +# Node will rejoin cluster when restarted + +# Check cluster status +docker exec lcbp3-redis-1 redis-cli cluster info + +# If node is failed, remove and add back +docker exec lcbp3-redis-1 redis-cli --cluster del-node +docker exec lcbp3-redis-1 redis-cli --cluster add-node :6379 :6379 +``` + +--- + +## 6. Maintenance Procedures + +### 6.1 Sequence Adjustment + +#### Increase Max Value +```sql +-- Check current utilization +SELECT + dc.document_type, + ds.current_value, + dc.max_value, + ROUND((ds.current_value * 100.0 / dc.max_value), 2) as utilization +FROM document_numbering_sequences ds +JOIN document_numbering_configs dc ON ds.config_id = dc.id +WHERE ds.current_value > dc.max_value * 0.8; + +-- Increase max_value for type approaching limit +UPDATE document_numbering_configs +SET max_value = max_value * 10, + updated_at = CURRENT_TIMESTAMP +WHERE document_type = 'COR' + AND max_value < 9999999; + +-- Audit log +INSERT INTO document_numbering_audit_logs ( + operation, document_type, old_value, new_value, + user_id, metadata +) VALUES ( + 'ADJUST_MAX_VALUE', 'COR', '999999', '9999999', + 1, '{"reason": "Approaching limit", "automated": false}' +); +``` + +#### Reset Yearly Sequence +```sql +-- For document types with yearly reset +-- Run on January 1st + +START TRANSACTION; + +-- Create new sequence for new year +INSERT INTO document_numbering_sequences ( + config_id, + scope_value, + current_value, + last_used_at +) +SELECT + id as config_id, + YEAR(CURDATE()) as scope_value, + 0 as current_value, + NULL as last_used_at +FROM document_numbering_configs +WHERE scope = 'YEARLY'; + +-- Verify +SELECT * FROM document_numbering_sequences +WHERE scope_value = YEAR(CURDATE()); + +COMMIT; +``` + +### 6.2 Cleanup Old Audit Logs + +```sql +-- Archive logs older than 2 years +-- Run monthly + +START TRANSACTION; + +-- Create archive table (if not exists) +CREATE TABLE IF NOT EXISTS document_numbering_audit_logs_archive +LIKE document_numbering_audit_logs; + +-- Move old logs to archive +INSERT INTO document_numbering_audit_logs_archive +SELECT * FROM document_numbering_audit_logs +WHERE timestamp < DATE_SUB(CURDATE(), INTERVAL 2 YEAR); + +-- Delete from main table +DELETE FROM document_numbering_audit_logs +WHERE timestamp < DATE_SUB(CURDATE(), INTERVAL 2 YEAR); + +-- Optimize table +OPTIMIZE TABLE document_numbering_audit_logs; + +COMMIT; + +-- Export archive to file (optional) +SELECT * FROM document_numbering_audit_logs_archive +INTO OUTFILE '/tmp/audit_archive_2023.csv' +FIELDS TERMINATED BY ',' +ENCLOSED BY '"' +LINES TERMINATED BY '\n'; +``` + +### 6.3 Redis Maintenance + +#### Flush Expired Reservations +```bash +#!/bin/bash +# scripts/cleanup-expired-reservations.sh + +echo "🧹 Cleaning up expired reservations..." + +# Get all reservation keys +KEYS=$(docker exec lcbp3-redis-1 redis-cli --cluster call 172.20.0.2:6379 KEYS "reservation:*" | grep -v "(error)") + +COUNT=0 +for KEY in $KEYS; do + # Check TTL + TTL=$(docker exec lcbp3-redis-1 redis-cli TTL "$KEY") + + if [ "$TTL" -lt 0 ]; then + # Delete expired key + docker exec lcbp3-redis-1 redis-cli DEL "$KEY" + ((COUNT++)) + fi +done + +echo "✅ Cleaned up $COUNT expired reservations" +``` + +--- + +## 7. Disaster Recovery + +### 7.1 Total System Failure + +#### Recovery Steps +```bash +#!/bin/bash +# scripts/disaster-recovery.sh + +echo "🚨 Starting disaster recovery..." + +# 1. Start Redis cluster +echo "1️⃣ Starting Redis cluster..." +docker-compose -f docker-compose-redis.yml up -d +sleep 30 + +# 2. Restore Redis backups +echo "2️⃣ Restoring Redis backups..." +./scripts/restore-redis.sh /backups/redis/latest.tar.gz + +# 3. Start database +echo "3️⃣ Starting MariaDB..." +docker-compose -f docker-compose-db.yml up -d +sleep 30 + +# 4. Restore database +echo "4️⃣ Restoring database..." +./scripts/restore-numbering-db.sh /backups/db/latest.sql.gz + +# 5. Verify sequence integrity +echo "5️⃣ Verifying sequence integrity..." +./scripts/check-sequence-integrity.sh + +# 6. Start backend services +echo "6️⃣ Starting backend services..." +docker-compose -f docker-compose-backend.yml up -d + +# 7. Run health checks +echo "7️⃣ Running health checks..." +sleep 60 +for i in {1..5}; do + curl -f http://localhost:3001/health || echo "Backend $i not healthy" +done + +echo "✅ Disaster recovery complete" +echo "⚠️ Please verify system functionality manually" +``` + +### 7.2 RTO/RPO Targets + +| Scenario | RTO | RPO | Priority | +| ---------------------------- | ------- | ------ | -------- | +| Single backend node failure | 0 min | 0 | P0 | +| Single Redis node failure | 0 min | 0 | P0 | +| Database primary failure | 5 min | 0 | P0 | +| Complete data center failure | 1 hour | 15 min | P1 | +| Data corruption | 4 hours | 1 day | P2 | + +--- + +## 8. Runbooks + +### 8.1 High Sequence Utilization (>90%) + +**Alert**: `SequenceWarning` or `SequenceCritical` + +**Steps**: +1. Check current utilization + ```sql + SELECT document_type, current_value, max_value, + ROUND((current_value * 100.0 / max_value), 2) as pct + FROM document_numbering_sequences s + JOIN document_numbering_configs c ON s.config_id = c.id + WHERE current_value > max_value * 0.9; + ``` + +2. Assess impact + - How many numbers left? + - Daily usage rate? + - Days until exhaustion? + +3. Take action + ```sql + -- Option A: Increase max_value + UPDATE document_numbering_configs + SET max_value = max_value * 10 + WHERE document_type = 'COR'; + + -- Option B: Reset sequence (yearly types only) + -- Schedule for next year/month + ``` + +4. Notify stakeholders +5. Update monitoring thresholds if needed + +--- + +### 8.2 High Lock Wait Time + +**Alert**: `HighLockWaitTime` + +**Steps**: +1. Check Redis cluster health + ```bash + docker exec lcbp3-redis-1 redis-cli cluster info + docker exec lcbp3-redis-1 redis-cli cluster nodes + ``` + +2. Check database locks + ```sql + SELECT * FROM information_schema.innodb_lock_waits; + SELECT * FROM information_schema.innodb_trx + WHERE trx_started < NOW() - INTERVAL 30 SECOND; + ``` + +3. Identify bottleneck + - Redis slow? + - Database slow? + - High concurrent load? + +4. Take action based on cause: + - **Redis**: Add more nodes, check network latency + - **Database**: Optimize queries, increase connection pool + - **High load**: Scale horizontally (add backend nodes) + +5. Monitor improvements + +--- + +### 8.3 Redis Cluster Down + +**Alert**: `RedisUnavailable` + +**Steps**: +1. Verify all nodes down + ```bash + for i in {1..3}; do + docker exec lcbp3-redis-$i redis-cli ping || echo "Node $i DOWN" + done + ``` + +2. Check system falls back to DB-only mode + ```bash + curl http://localhost:3001/health/numbering + # Should show: fallback_mode: true + ``` + +3. Restart Redis cluster + ```bash + docker-compose -f docker-compose-redis.yml restart + sleep 30 + ./scripts/check-redis-cluster.sh + ``` + +4. If restart fails, restore from backup + ```bash + ./scripts/restore-redis.sh /backups/redis/latest.tar.gz + ``` + +5. Verify numbering system back to normal + ```bash + curl http://localhost:3001/health/numbering + # Should show: fallback_mode: false + ``` + +6. Review logs for root cause + +--- + +## 9. Performance Tuning + +### 9.1 Slow Number Generation + +**Diagnosis**: +```sql +-- Check slow queries +SELECT * FROM mysql.slow_log +WHERE sql_text LIKE '%document_numbering%' +ORDER BY query_time DESC +LIMIT 10; + +-- Check index usage +EXPLAIN SELECT * FROM document_numbering_sequences +WHERE config_id = 1 AND scope_value = '2025' +FOR UPDATE; +``` + +**Optimizations**: +```sql +-- Add missing indexes +CREATE INDEX idx_sequence_lookup +ON document_numbering_sequences(config_id, scope_value); + +-- Optimize table +OPTIMIZE TABLE document_numbering_sequences; + +-- Update statistics +ANALYZE TABLE document_numbering_sequences; +``` + +### 8.2 Redis Memory Optimization + +```bash +# Check memory usage +docker exec lcbp3-redis-1 redis-cli INFO memory + +# If memory high, check keys +docker exec lcbp3-redis-1 redis-cli --bigkeys + +# Set maxmemory policy +docker exec lcbp3-redis-1 redis-cli CONFIG SET maxmemory 2gb +docker exec lcbp3-redis-1 redis-cli CONFIG SET maxmemory-policy allkeys-lru +``` + +--- + +## 10. Security Hardening + +### 10.1 Redis Security + +```conf +# redis.conf +requirepass your-strong-redis-password +bind 0.0.0.0 +protected-mode yes +rename-command FLUSHDB "" +rename-command FLUSHALL "" +rename-command CONFIG "CONFIG_abc123" +``` + +### 10.2 Database Security + +```sql +-- Create dedicated numbering user +CREATE USER 'numbering'@'%' IDENTIFIED BY 'strong-password'; + +-- Grant minimal permissions +GRANT SELECT, INSERT, UPDATE ON lcbp3_production.document_numbering_* TO 'numbering'@'%'; +GRANT SELECT ON lcbp3_production.users TO 'numbering'@'%'; + +FLUSH PRIVILEGES; +``` + +### 10.3 Network Security + +```yaml +# docker-compose-network.yml +networks: + lcbp3-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 + driver_opts: + com.docker.network.bridge.name: lcbp3-br + com.docker.network.bridge.enable_icc: "true" + com.docker.network.bridge.enable_ip_masquerade: "true" +``` + +--- + +## 11. Compliance & Audit + +### 11.1 Audit Log Retention + +```sql +-- Export audit logs for compliance +SELECT * +FROM document_numbering diff --git a/infrastructure/MariaDB_setting.md b/specs/08-infrastructure/MariaDB_setting.md similarity index 100% rename from infrastructure/MariaDB_setting.md rename to specs/08-infrastructure/MariaDB_setting.md diff --git a/infrastructure/NPM_setting.md b/specs/08-infrastructure/NPM_setting.md similarity index 100% rename from infrastructure/NPM_setting.md rename to specs/08-infrastructure/NPM_setting.md diff --git a/infrastructure/Securities.md b/specs/08-infrastructure/Securities.md similarity index 100% rename from infrastructure/Securities.md rename to specs/08-infrastructure/Securities.md diff --git a/infrastructure/Service_setting.md b/specs/08-infrastructure/Service_setting.md similarity index 100% rename from infrastructure/Service_setting.md rename to specs/08-infrastructure/Service_setting.md diff --git a/infrastructure/n8n_setting.md b/specs/08-infrastructure/n8n_setting.md similarity index 100% rename from infrastructure/n8n_setting.md rename to specs/08-infrastructure/n8n_setting.md diff --git a/infrastructure/แผนผัง Network.md b/specs/08-infrastructure/แผนผัง Network.md similarity index 100% rename from infrastructure/แผนผัง Network.md rename to specs/08-infrastructure/แผนผัง Network.md diff --git a/specs/09-history/2025-12-17-document-numbering-v162-alignment.md b/specs/09-history/2025-12-17-document-numbering-v162-alignment.md new file mode 100644 index 0000000..07feb10 --- /dev/null +++ b/specs/09-history/2025-12-17-document-numbering-v162-alignment.md @@ -0,0 +1,46 @@ +# 2025-12-17: Document Numbering Specs v1.6.2 Alignment + +**Date:** 2025-12-17 +**Type:** Specification Refactoring +**Related:** REQ-009-DocumentNumbering + +--- + +## Summary + +ปรับปรุง specification files ของ Document Numbering ให้สอดคล้องกับ Requirements v1.6.2 + +--- + +## Changes Made + +### Updated Specifications + +| File | From | To | Key Changes | +| ----------------------------------------------------- | ------ | ------ | --------------------------------------- | +| `05-decisions/ADR-002-document-numbering-strategy.md` | v2.0 | v3.0 | Version refs, compliance links, history | +| `04-operations/document-numbering-operations.md` | v1.6.0 | v1.6.2 | Status→APPROVED, file paths fixed | +| `03-implementation/document-numbering.md` | v1.6.1 | v1.6.2 | ADR reference fixed | + +### New Task Files + +| File | Purpose | +| ----------------------------------------------------- | ----------------------------- | +| `06-tasks/TASK-BE-017-document-numbering-refactor.md` | Backend implementation tasks | +| `06-tasks/TASK-FE-017-document-numbering-refactor.md` | Frontend implementation tasks | + +--- + +## Key Decisions + +1. **Single Source of Truth**: `document_number_counters` เป็น authoritative counter system +2. **Counter Key Structure**: Unified to 8 fields (project, orig, recip, type, sub_type, rfa_type, discipline, reset_scope) +3. **Number State Machine**: RESERVED → CONFIRMED → VOID/CANCELLED +4. **Deprecated Tokens**: `{ORG}`, `{TYPE}` replaced with explicit `{ORIGINATOR}`, `{RECIPIENT}`, `{CORR_TYPE}` + +--- + +## Next Actions + +- [ ] Execute TASK-BE-017 (Backend team) +- [ ] Execute TASK-FE-017 (Frontend team, after BE ready)