# 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) ได้โดยอัตโนมัติและยืดหยุ่นสูง - ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร - ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์ - ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย ## 3.11.2. Logic การนับเลข (Counter Logic) การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร ### 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` | - | | `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 แยกตามประเภทเอกสาร **LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER**: ``` (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, 0, 0, 0, current_year) ``` *หมายเหตุ*: ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id` **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 - `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. Letter (TYPE = LETTER) **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, corr_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, corr_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, corr_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.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 | 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.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) --- ## 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.5.2. 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.5.3. 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.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` ```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.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` ```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.8.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.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:10.11 environment: MYSQL_REPLICATION_MODE: master mariadb-replica: image: mariadb:10.11 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` ต้องมีโครงสร้างดังนี้: ```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.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 > **💡 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.14.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.15. 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 - 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 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)