251202:1300

This commit is contained in:
2025-12-02 13:26:05 +07:00
parent d62beaa1bd
commit 5acc631994
5 changed files with 3024 additions and 1158 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -73,17 +73,24 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[sql]": {
"editor.defaultFormatter": "mtxr.sqltools",
"editor.tabSize": 2,
"editor.insertSpaces": true
},
"sqltools.codelensLanguages": ["sql", ""],
"sqltools.codelensLanguages": ["sql"],
"sqltools.format": {
"language": "sql",
"params": {
"keywordCase": "upper", // ทำให้ INSERT, VALUES เป็นตัวใหญ่
"tabWidth": 2, // เยื้อง 3 ช่องว่าง (ปรับจาก 4 เป็น 3 เพื่อ match ตัวอย่าง)
"expressionWidth": 80 // ความยาวสูงสุดต่อบรรทัดก่อนตีบรรทัดใหม่ (ป้องกันคอลัมน์แยกบรรทัด)
"expressionWidth": 80, // ความยาวสูงสุดต่อบรรทัดก่อนตีบรรทัดใหม่ (ป้องกันคอลัมน์แยกบรรทัด)
"reservedWordCase": "upper", // ทำให้คำสงวนเป็นตัวใหญ่
"linesBetweenQueries": 1,
"logicalOperatorNewline": "before",
"aliasAs": "before",
"commaPosition": "after",
"linesAroundComments": 0,
"tabulateAlias": false,
"newlineBeforeSemicolon": false
}
},
"sqltools.formatOnSave": true,

View File

@@ -1,69 +1,296 @@
# 3.11 Document Numbering Management (การจัดการเลขที่เอกสาร)
---
title: 'Functional Requirements: Document Numbering Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
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/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md
---
## 3.11.1. วัตถุประสงค์
- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติและยืดหยุ่นสูง
- ระบต้องสามารถกำหนด รูปแบบ(template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร
- ระบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร
- ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์
- ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย
## 3.11.2. Logic การนับเลข (Counter Logic)
- การนับเลขจะต้องรองรับการแยกตาม Key ที่ซับซ้อนขึ้น ตามแต่ละ รูปแบบ(template) ได้
การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วย:
## 3.11.3. Format Template
- `project_id` - รหัสโครงการ
- `doc_type_id` - ชนิดเอกสาร (Correspondence, RFA, Transmittal, Drawing)
- `sub_type_id` - ประเภทย่อยของเอกสาร (nullable)
- `discipline_id` - สาขาวิชา/งาน (nullable)
- `year` - ปี พ.ศ. หรือ ค.ศ. ตามที่กำหนดใน template
- รองรับการกำหนดรูปแบบด้วย Token Replacement
- transmittal to owner:
- {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR B.D.} -> คคง.-สคฉ.3-03-21-0117-2568
- other transmittal:
- {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR B.D.} -> ผรม.2-คคง.-0117-2568
- RFA:
- {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV} -> LCBP3-C2-RFI-ROW-0029-A
- Correspondence type LETTER:
- {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR B.D.} -> คคง.-สคฉ.3-0985-2568
- Correspondence รองรับ Token: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR B.D.} -> คคง.-สคฉ.3-STR-0001-2568
- RFA รองรับ Token: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV} -> TEAM-RFA-STR-0001-A
- Transmittal รองรับ Token: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV} -> TEAM-TR-STR-0001-A
### ตัวอย่าง Counter Key
## 3.11.4. Transmittal Logic
```text
Correspondence: project_id + doc_type_id + sub_type_id + year
RFA: project_id + doc_type_id + discipline_id + year
Transmittal: project_id + doc_type_id + recipient_type + year
Drawing: project_id + doc_type_id + discipline_id + year
```
- รองรับเงื่อนไขพิเศษสำหรับ Transmittal ที่เลขอาจเปลี่ยนตามผู้รับ (To Owner vs To Contractor)
### Fallback สำหรับค่า NULL
## 3.11.5. กลไกความปลอดภัย
- กรณีที่ `discipline_id` หรือ `sub_type_id` เป็น NULL ให้ใช้ค่า Default `0` ในการจัดกลุ่ม Counter
- ป้องกัน Error และรับประกันความถูกต้องของ Running Number (Uniqueness Guarantee)
- ยังคงใช้ Redis Distributed Lock และ Optimistic Locking เพื่อป้องกันเลขซ้ำหรือข้าม
## 3.11.3. Format Templates by Document Type
## 3.11.6. ต้องมี retry mechanism และ fallback strategy เมื่อการ generate เลขที่เอกสารล้มเหลว
ระบบรองรับการกำหนดรูปแบบด้วย **Token Replacement**
## 3.11.7. Fallback Logic (เพิ่ม)
### 3.11.3.1. Correspondence (หนังสือราชการ)
- กรณีที่เอกสารประเภทนั้นไม่มี discipline_id หรือ sub_type_id (เป็นค่า NULL หรือไม่ระบุ) ให้ระบบใช้ค่า Default (เช่น 0) ในการจัดกลุ่ม Counter เพื่อป้องกัน Error และรับประกันความถูกต้องของ Running Number (Uniqueness Guarantee)
- Scenario 1: Redis Unavailable
- Fallback เป็น database-only locking (pessimistic lock)
- Log warning และแจ้ง ops team
#### Letter Type (TYPE = 03)
- **Template**: `{ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}`
- **Example**: `คคง.-สคฉ.3-0985-2568`
- **Counter Key**: `project_id + doc_type_id + sub_type_id + year`
#### Other Correspondence Types
- **Template**: `{ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}`
- **Example**: `คคง.-สคฉ.3-STR-0001-2568`
- **Counter Key**: `project_id + doc_type_id + sub_type_id + year`
### 3.11.3.2. Transmittal
#### Transmittal to Owner
- **Template**: `{ORG}-{ORG}-{TYPE}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}`
- **Example**: `คคง.-สคฉ.3-03-21-0117-2568`
- **Counter Key**: `project_id + doc_type_id + recipient_type + year`
- **Note**: `recipient_type = 'OWNER'`
#### Transmittal to Contractor/Others
- **Template**: `{ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}`
- **Example**: `ผรม.2-คคง.-0117-2568`
- **Counter Key**: `project_id + doc_type_id + recipient_type + year`
- **Note**: `recipient_type = 'CONTRACTOR' | 'CONSULTANT' | 'OTHER'`
#### Alternative Project-based Format
- **Template**: `{PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}`
- **Example**: `LCBP3-TR-STR-0001-A`
- **Counter Key**: `project_id + doc_type_id + discipline_id + year`
### 3.11.3.3. RFA (Request for Approval)
- **Template**: `{PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}`
- **Example**: `LCBP3-C2-RFI-ROW-0029-A`
- **Counter Key**: `project_id + doc_type_id + discipline_id + year`
- **Note**: `{REV}` คือ revision code (A, B, C, ..., AA, AB, ...)
### 3.11.3.4. Drawing
- **Template**: `{PROJECT}-{DISCIPLINE}-{CATEGORY}-{SEQ:4}-{REV}`
- **Example**: `LCBP3-STR-DRW-0001-A`
- **Counter Key**: `project_id + doc_type_id + discipline_id + category + year`
## 3.11.4. Supported Token Types
| Token | Description | Example |
|-------|-------------|---------|
| `{PROJECT}` | รหัสโครงการ | `LCBP3` |
| `{ORG}` | รหัสหน่วยงาน | `คคง.`, `สคฉ.3` |
| `{TYPE}` | รหัสชนิดเอกสาร | `RFI`, `03` |
| `{SUB_TYPE}` | รหัสประเภทย่อย | `21` |
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `ROW` |
| `{CATEGORY}` | หมวดหมู่ | `DRW` |
| `{SEQ:n}` | Running number (n = จำนวนหลัก) | `0001`, `0029` |
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` |
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` |
| `{REV}` | Revision Code | `A`, `B`, `AA` |
## 3.11.5. Transmittal Special Logic
- Transmittal มีเงื่อนไขพิเศษที่เลขอาจเปลี่ยนตามผู้รับ:
- **To Owner**: ใช้ format พิเศษที่มี sub_type รหัสโครงการ
- **To Contractor/Others**: ใช้ format ทั่วไป
- Counter Key จะแยกตาม `recipient_type` เพื่อให้แต่ละประเภทมี running number อิสระ
## 3.11.6. กลไกความปลอดภัย (Concurrency Control)
### 3.11.6.1. Redis Distributed Lock
- ใช้ Redis Distributed Lock เพื่อป้องกัน race condition
- Lock key format: `lock:docnum:{project_id}:{doc_type_id}:{...counter_key_parts}`
- Lock TTL: 5 วินาที (auto-release เมื่อ timeout)
- Lock acquisition timeout: 10 วินาที
### 3.11.6.2. Optimistic Locking
- ใช้ `version` column ในตาราง `document_number_configs`
- ตรวจสอบ version ก่อน update counter
- หาก version conflict เกิดขึ้น → retry transaction
### 3.11.6.3. Database Constraints
- Unique constraint บน `document_number` column
- Foreign key constraints เพื่อความสัมพันธ์ข้อมูล
- Check constraints สำหรับ business rules
## 3.11.7. Retry Mechanism & Error Handling
### 3.11.7.1. Scenario 1: Redis Unavailable
- **Fallback**: ใช้ database-only locking (pessimistic lock)
- **Action**:
- ใช้ `SELECT ... FOR UPDATE` แทน Redis lock
- Log warning พร้อม alert ops team
- ระบบยังใช้งานได้แต่ performance ลดลง
- Scenario 2: Lock Acquisition Timeout
- Retry 5 ครั้งด้วย exponential backoff (1s, 2s, 4s, 8s, 16s)
- หลัง 5 ครั้ง: Return error 503 "Service Temporarily Unavailable"
- Frontend แสดง user-friendly message: "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง"
- Scenario 3: Version Conflict After Lock
- Retry transaction อีก 2 ครั้ง
- หากยังล้มเหลว: Log error พร้อม context และ return 409 Conflict
- Frontend แสดง user-friendly message: "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่"
- Monitoring:
- Alert ถ้า lock acquisition failures > 5% ใน 5 นาที
- Dashboard แสดง lock wait time percentiles
### 3.11.7.2. Scenario 2: Lock Acquisition Timeout
- **Retry**: 5 ครั้งด้วย exponential backoff
- Attempt 1: wait 1s
- Attempt 2: wait 2s
- Attempt 3: wait 4s
- Attempt 4: wait 8s
- Attempt 5: wait 16s (รวม ~31 วินาที)
- **Failure**: Return HTTP 503 "Service Temporarily Unavailable"
- **Frontend**: แสดงข้อความ "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง"
### 3.11.7.3. Scenario 3: Version Conflict After Lock
- **Retry**: 2 ครั้ง (reload counter + retry transaction)
- **Failure**: Log error พร้อม context และ return HTTP 409 Conflict
- **Frontend**: แสดงข้อความ "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่"
### 3.11.7.4. Scenario 4: Database Connection Error
- **Retry**: 3 ครั้งด้วย exponential backoff (1s, 2s, 4s)
- **Failure**: Return HTTP 500 "Internal Server Error"
- **Frontend**: แสดงข้อความ "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ"
## 3.11.8. Configuration Management
### 3.11.8.1. Admin Panel Configuration
- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Admin Panel
- การเปลี่ยนแปลง template จะไม่ส่งผลต่อเอกสารที่สร้างไว้แล้ว
- ต้องมีการ validate template ก่อนบันทึก (ตรวจสอบ token ที่ใช้ถูกต้อง)
### 3.11.8.2. Template Versioning
- เก็บ history ของ template changes
- บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง
- สามารถ rollback ไปเวอร์ชันก่อนหน้าได้
### 3.11.8.3. Counter Reset Policy
- Counter reset ตามปี (yearly reset)
- Counter reset ตาม project phase (optional)
- Admin สามารถ manual reset counter ได้ (require approval + audit log)
## 3.11.9. Audit Trail
### 3.11.9.1. การบันทึก Audit Log
บันทึกทุกการ generate เลขที่เอกสารใน `document_number_audit` table:
- `document_id` - เอกสารที่ถูกสร้าง
- `generated_number` - เลขที่ถูกสร้าง
- `counter_key` - key ที่ใช้ในการนับ
- `template_used` - template ที่ใช้
- `user_id` - ผู้ที่ request
- `ip_address` - IP address ของผู้ request
- `timestamp` - เวลาที่สร้าง
- `retry_count` - จำนวนครั้งที่ retry (ถ้ามี)
### 3.11.9.2. Conflict & Error Logging
- บันทึก version conflicts และ กลไก retry ที่ใช้
- บันทึก lock timeouts และ failure reasons
- บันทึก fallback scenarios (เช่น Redis unavailable)
## 3.11.10. Performance Requirements
### 3.11.10.1. Response Time
- Document number generation ต้องเสร็จภายใน **2 วินาที** (95th percentile)
- Document number generation ต้องเสร็จภายใน **5 วินาที** (99th percentile)
- ในกรณี normal operation (ไม่มี retry) ควรเสร็จภายใน **500ms**
### 3.11.10.2. Throughput
- ระบบรองรับ concurrent requests อย่างน้อย **50 requests/second**
- Peak load รองรับได้ถึง **100 requests/second** (ช่วงเวลาเร่งงาน)
### 3.11.10.3. Availability
- Uptime ≥ 99.5% (exclude planned maintenance)
- Maximum downtime ต่อเดือน ≤ 3.6 ชั่วโมง
## 3.11.11. Monitoring & Alerting
### 3.11.11.1. Metrics to Monitor
- Lock acquisition time (p50, p95, p99)
- Lock acquisition failure rate
- Counter generation latency
- Retry count distribution
- Redis connection status
- Database connection pool usage
### 3.11.11.2. Alert Conditions
- 🔴 **Critical**: Redis unavailable > 1 minute
- 🔴 **Critical**: Lock acquisition failures > 10% in 5 minutes
- 🟡 **Warning**: Lock acquisition failures > 5% in 5 minutes
- 🟡 **Warning**: Average lock wait time > 1 second
- 🟡 **Warning**: Retry count > 100 per hour
### 3.11.11.3. Dashboard
- Real-time lock acquisition success rate
- Lock wait time percentiles (p50, p95, p99)
- Counter generation rate (per minute)
- Error rate breakdown (by error type)
- Redis/Database health status
## 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. 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 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)

View File

@@ -1,7 +1,7 @@
# ADR-002: Document Numbering Strategy
**Status:** Accepted
**Date:** 2025-11-30
**Date:** 2025-12-02
**Decision Makers:** Development Team, System Architect
**Related Documents:**
@@ -12,32 +12,34 @@
## Context and Problem Statement
LCBP3-DMS ต้องสร้างเลขที่เอกสารอัตโนมัติสำหรับ Correspondences และ RF
As โดยเลขที่เอกสารต้อง:
LCBP3-DMS ต้องสร้างเลขที่เอกสารอัตโนมัติสำหรับ Correspondence, RFA, Transmittal และ Drawing โดยเลขที่เอกสารต้อง:
1. **Unique:** ไม่ซ้ำกันในระบบ
2. **Sequential:** เรียงตามลำดับเวลา
3. **Meaningful:** มีโครงสร้างที่อ่านเข้าใจได้ (เช่น `TEAM-RFA-STR-2025-0001`)
4. **Configurable:** สามารถปรับรูปแบบได้ตาม Project/Organization
3. **Meaningful:** มีโครงสร้างที่อ่านเข้าใจได้ (เช่น `LCBP3-C2-RFI-ROW-0029-A`)
4. **Configurable:** สามารถปรับรูปแบบได้ตาม Project/Organization/Document Type
5. **Concurrent-safe:** ป้องกัน Race Condition เมื่อมีหลาย Request พร้อมกัน
### Key Challenges
1. **Race Condition:** เมื่อมี 2+ requests พร้อมกัน อาจได้เลขเดียวกัน
2. **Performance:** ต้องรวดเร็วแม้มี concurrent requests
3. **Flexibility:** รองรับรูปแบบเลขที่หลากหลาย
4. **Discipline Support:** เลขที่ต้องรวม Discipline Code (GEN, STR, ARC)
2. **Performance:** ต้องรวดเร็วแม้มี concurrent requests (50-100 req/sec)
3. **Flexibility:** รองรับรูปแบบเลขที่หลากหลายตามชนิดเอกสาร
4. **Discipline Support:** เลขที่ต้องรวม Discipline Code (GEN, STR, ARC, etc.)
5. **Transmittal Logic:** เลขที่ Transmittal เปลี่ยนตามผู้รับ (To Owner vs To Contractor)
6. **Year Reset:** Counter ต้อง reset ตาม ปี พ.ศ. หรือ ค.ศ.
---
## Decision Drivers
- **Data Integrity:** เลขที่ต้องไม่ซ้ำกันเด็ดขาด
- **Performance:** Generate เลขที่ได้เร็ว (< 100ms)
- **Scalability:** รองรับ concurrent requests สูง
- **Data Integrity:** เลขที่ต้องไม่ซ้ำกันเด็ดขาด (Mission-Critical)
- **Performance:** Generate เลขที่ได้เร็ว (<500ms normal, <2s p95, <5s p99)
- **Scalability:** รองรับ 50-100 concurrent requests/second
- **Maintainability:** ง่ายต่อการ Config และ Debug
- **Flexibility:** รองรับรูปแบบที่หลากหลาย
- **Flexibility:** รองรับ Template-based format สำหรับแต่ละ document type
- **Auditability:** บันทึก history ของทุก generated number
- **Security:** ป้องกัน abuse ด้วย rate limiting
---
@@ -56,8 +58,8 @@ As โดยเลขที่เอกสารต้อง:
**Cons:**
- ❌ ไม่ Configurable (รูปแบบเลขที่ fixed)
- ❌ ยากต่อการ Partition by Project/Type/Year
- ❌ ไม่รองรับ Custom format (เช่น `TEAM-RFA-2025-0001`)
- ❌ ยากต่อการ Partition by Project/Type/Discipline/Year
- ❌ ไม่รองรับ Custom format (เช่น `LCBP3-RFA-2025-0001`)
- ❌ Reset ตาม Year ทำได้ยาก
### Option 2: Application-Level Counter (Single Lock)
@@ -83,11 +85,12 @@ As โดยเลขที่เอกสารต้อง:
**Pros:**
-**Guaranteed Uniqueness:** Double-layer protection
-**Fast Performance:** Redis lock prevents most conflicts
-**Audit Trail:** Counter history in database
-**Fast Performance:** Redis lock prevents most conflicts (<500ms)
-**Audit Trail:** Counter history + audit log in database
-**Configurable Format:** Template-based generation
-**Resilient:** Fallback to DB if Redis issues
-**Partition Support:** Different counters per Project/Type/Discipline/Year
-**Resilient:** Fallback to DB pessimistic lock if Redis unavailable
-**Partition Support:** Different counters per Project/Type/SubType/Discipline/Year
-**Transmittal Logic:** Support recipient-based counting
**Cons:**
@@ -107,8 +110,8 @@ As โดยเลขที่เอกสารต้อง:
1. **Mission-Critical:** เลขที่เอกสารต้องถูกต้อง 100% (ไม่ยอมรับการซ้ำ)
2. **Performance + Safety:** Balance ระหว่างความเร็วและความปลอดภัย
3. **Auditability:** มี Counter history ใน Database
4. **Flexibility:** รองรับ Template-based format
3. **Auditability:** มี Counter history + Audit log ใน Database
4. **Flexibility:** รองรับ Template-based format สำหรับทุก document type
5. **Resilience:** ถ้า Redis มีปัญหา ยัง Fallback ไปใช้ DB Lock ได้
---
@@ -119,42 +122,139 @@ As โดยเลขที่เอกสารต้อง:
```sql
-- Format Templates
CREATE TABLE document_number_formats (
CREATE TABLE document_number_configs (
id INT PRIMARY KEY AUTO_INCREMENT,
project_id INT NOT NULL,
correspondence_type_id INT NOT NULL,
format_template VARCHAR(255) NOT NULL,
-- Example: '{ORG_CODE}-{TYPE_CODE}-{DISCIPLINE_CODE}-{YEAR}-{SEQ:4}'
doc_type_id INT NOT NULL COMMENT 'Correspondence, RFA, Transmittal, Drawing',
sub_type_id INT DEFAULT 0 COMMENT 'ประเภทย่อย (nullable, use 0 for fallback)',
discipline_id INT DEFAULT 0 COMMENT 'สาขาวิชา (nullable, use 0 for fallback)',
template VARCHAR(255) NOT NULL COMMENT 'e.g. {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
version INT DEFAULT 0 NOT NULL COMMENT 'For template versioning',
FOREIGN KEY (project_id) REFERENCES projects(id),
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id),
UNIQUE KEY (project_id, correspondence_type_id)
);
FOREIGN KEY (doc_type_id) REFERENCES document_types(id),
UNIQUE KEY unique_config (project_id, doc_type_id, sub_type_id, discipline_id)
) ENGINE=InnoDB COMMENT='Template configurations for document numbering';
-- Counter Table with Optimistic Locking
CREATE TABLE document_number_counters (
project_id INT NOT NULL,
originator_organization_id INT NOT NULL,
correspondence_type_id INT NOT NULL,
discipline_id INT DEFAULT 0, -- 0 = no discipline
current_year INT NOT NULL,
doc_type_id INT NOT NULL,
sub_type_id INT DEFAULT 0 COMMENT 'For Correspondence types, 0 = fallback',
discipline_id INT DEFAULT 0 COMMENT 'For RFA/Drawing, 0 = fallback',
recipient_type VARCHAR(20) DEFAULT NULL COMMENT 'For Transmittal: OWNER, CONTRACTOR, CONSULTANT, OTHER',
year INT NOT NULL COMMENT 'ปี พ.ศ. หรือ ค.ศ. ตาม template',
last_number INT DEFAULT 0,
version INT DEFAULT 0 NOT NULL, -- Version for Optimistic Lock
version INT DEFAULT 0 NOT NULL COMMENT 'Version for Optimistic Lock',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (project_id, originator_organization_id, correspondence_type_id, discipline_id, current_year),
PRIMARY KEY (project_id, doc_type_id, sub_type_id, discipline_id, COALESCE(recipient_type, ''), year),
FOREIGN KEY (project_id) REFERENCES projects(id),
FOREIGN KEY (originator_organization_id) REFERENCES organizations(id),
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id),
FOREIGN KEY (discipline_id) REFERENCES disciplines(id)
);
FOREIGN KEY (doc_type_id) REFERENCES document_types(id),
INDEX idx_counter_lookup (project_id, doc_type_id, year)
) ENGINE=InnoDB COMMENT='Running number counters with optimistic locking';
-- Audit Trail
CREATE TABLE document_number_audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
document_id INT DEFAULT NULL COMMENT 'FK to documents (set after doc creation)',
generated_number VARCHAR(255) NOT NULL,
counter_key VARCHAR(500) NOT NULL COMMENT 'Redis lock key used',
template_used VARCHAR(255) NOT NULL,
sequence_number INT NOT NULL,
user_id INT NOT NULL,
ip_address VARCHAR(45),
retry_count INT DEFAULT 0,
lock_wait_ms INT DEFAULT 0 COMMENT 'Time spent waiting for lock',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_audit_number (generated_number),
INDEX idx_audit_user (user_id, created_at),
INDEX idx_audit_created (created_at)
) ENGINE=InnoDB COMMENT='Audit trail for all generated document numbers';
```
### NestJS Service Implementation
### Token Types Reference
รองรับ Token ทั้งหมด 9 ประเภท:
| Token | Description | Example Value |
|-------|-------------|---------------|
| `{PROJECT}` | รหัสโครงการ | `LCBP3` |
| `{ORG}` | รหัสหน่วยงาน | `คคง.`, `C2` |
| `{TYPE}` | รหัสชนิดเอกสาร | `RFI`, `03` |
| `{SUB_TYPE}` | รหัสประเภทย่อย | `21` |
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `ROW` |
| `{CATEGORY}` | หมวดหมู่ | `DRW` |
| `{SEQ:n}` | Running number (n digits) | `0001`, `00029` |
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` |
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` |
| `{REV}` | Revision Code | `A`, `B`, `AA` |
### Format Examples by Document Type
#### 1. Correspondence (หนังสือราชการ)
**Letter Type (TYPE = 03):**
```
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-0985-2568
Counter Key: project_id + doc_type_id + sub_type_id + year
```
**Other Correspondence:**
```
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-STR-0001-2568
Counter Key: project_id + doc_type_id + sub_type_id + year
```
#### 2. Transmittal
**To Owner (Special Format):**
```
Template: {ORG}-{ORG}-{TYPE}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-03-21-0117-2568
Counter Key: project_id + doc_type_id + recipient_type('OWNER') + year
Note: recipient_type แยก counter จาก To Contractor
```
**To Contractor/Others:**
```
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
Example: ผรม.2-คคง.-0117-2568
Counter Key: project_id + doc_type_id + recipient_type('CONTRACTOR') + year
```
**Alternative Project-based:**
```
Template: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}
Example: LCBP3-TR-STR-0001-A
Counter Key: project_id + doc_type_id + discipline_id + year
```
#### 3. RFA (Request for Approval)
```
Template: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}
Example: LCBP3-C2-RFI-ROW-0029-A
Counter Key: project_id + doc_type_id + discipline_id + year
```
#### 4. Drawing
```
Template: {PROJECT}-{DISCIPLINE}-{CATEGORY}-{SEQ:4}-{REV}
Example: LCBP3-STR-DRW-0001-A
Counter Key: project_id + doc_type_id + discipline_id + category + year
```
### NestJS Service Implementation (Simplified)
```typescript
// document-numbering.service.ts
import { Injectable } from '@nestjs/common';
// File: backend/src/modules/document-numbering/document-numbering.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import Redlock from 'redlock';
@@ -162,40 +262,91 @@ import Redis from 'ioredis';
interface NumberingContext {
projectId: number;
organizationId: number;
typeId: number;
docTypeId: number;
subTypeId?: number;
disciplineId?: number;
recipientType?: 'OWNER' | 'CONTRACTOR' | 'CONSULTANT' | 'OTHER';
year?: number;
userId: number;
ipAddress: string;
}
@Injectable()
export class DocumentNumberingService {
private readonly logger = new Logger(DocumentNumberingService.name);
constructor(
@InjectRepository(DocumentNumberCounter)
private counterRepo: Repository<DocumentNumberCounter>,
@InjectRepository(DocumentNumberFormat)
private formatRepo: Repository<DocumentNumberFormat>,
@InjectRepository(DocumentNumberConfig)
private configRepo: Repository<DocumentNumberConfig>,
@InjectRepository(DocumentNumberAudit)
private auditRepo: Repository<DocumentNumberAudit>,
private redis: Redis,
private redlock: Redlock
) {}
async generateNextNumber(context: NumberingContext): Promise<string> {
const year = context.year || new Date().getFullYear();
const disciplineId = context.disciplineId || 0;
const year = context.year || new Date().getFullYear() + 543; // พ.ศ.
const subTypeId = context.subTypeId || 0; // Fallback for NULL
const disciplineId = context.disciplineId || 0; // Fallback for NULL
// Step 1: Acquire Redis Distributed Lock
const lockKey = `doc_num:${context.projectId}:${context.organizationId}:${context.typeId}:${disciplineId}:${year}`;
const lock = await this.redlock.acquire([lockKey], 3000); // 3 second TTL
// Build Redis lock key
const lockKey = this.buildLockKey(
context.projectId,
context.docTypeId,
subTypeId,
disciplineId,
context.recipientType,
year
);
// Retry with exponential backoff (Scenario 2 & 3)
return this.retryWithBackoff(
async () => await this.generateNumberWithLock(
lockKey,
context,
year,
subTypeId,
disciplineId
),
5, // Max 5 retries
1000 // Initial delay 1s
);
}
private async generateNumberWithLock(
lockKey: string,
context: NumberingContext,
year: number,
subTypeId: number,
disciplineId: number
): Promise<string> {
let lock: any;
const lockStartTime = Date.now();
try {
// Scenario 1: Redis Unavailable - Fallback to DB lock
try {
// Step 1: Acquire Redis Distributed Lock (TTL: 5 seconds)
lock = await this.redlock.acquire([lockKey], 5000);
} catch (redisError) {
this.logger.warn(`Redis lock failed, falling back to DB lock: ${redisError.message}`);
// Fallback: Use SELECT ... FOR UPDATE (Pessimistic Lock)
return await this.generateWithDatabaseLock(context, year, subTypeId, disciplineId);
}
const lockWaitMs = Date.now() - lockStartTime;
// Step 2: Query current counter with version
let counter = await this.counterRepo.findOne({
where: {
project_id: context.projectId,
originator_organization_id: context.organizationId,
correspondence_type_id: context.typeId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
current_year: year,
recipient_type: context.recipientType || null,
year: year,
},
});
@@ -203,19 +354,21 @@ export class DocumentNumberingService {
if (!counter) {
counter = this.counterRepo.create({
project_id: context.projectId,
originator_organization_id: context.organizationId,
correspondence_type_id: context.typeId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
current_year: year,
recipient_type: context.recipientType || null,
year: year,
last_number: 0,
version: 0,
});
await this.counterRepo.save(counter);
}
const currentVersion = counter.version;
const nextNumber = counter.last_number + 1;
// Step 3: Update counter with Optimistic Lock check
// Step 3: Update counter with Optimistic Lock check (Scenario 3)
const result = await this.counterRepo
.createQueryBuilder()
.update(DocumentNumberCounter)
@@ -225,54 +378,154 @@ export class DocumentNumberingService {
})
.where({
project_id: context.projectId,
originator_organization_id: context.organizationId,
correspondence_type_id: context.typeId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
current_year: year,
recipient_type: context.recipientType || null,
year: year,
version: currentVersion, // Optimistic lock check
})
.execute();
if (result.affected === 0) {
throw new Error('Optimistic lock conflict - counter version changed');
throw new ConflictException('Counter version conflict - retrying...');
}
// Step 4: Generate formatted number
const format = await this.getFormat(context.projectId, context.typeId);
const formattedNumber = await this.formatNumber(format, {
const config = await this.getConfig(
context.projectId,
context.docTypeId,
subTypeId,
disciplineId
);
const formattedNumber = await this.formatNumber(config.template, {
...context,
year,
sequenceNumber: nextNumber,
});
// Step 5: Audit logging
await this.auditRepo.save({
generated_number: formattedNumber,
counter_key: lockKey,
template_used: config.template,
sequence_number: nextNumber,
user_id: context.userId,
ip_address: context.ipAddress,
retry_count: 0,
lock_wait_ms: lockWaitMs,
});
this.logger.log(`Generated: ${formattedNumber} (wait: ${lockWaitMs}ms)`);
return formattedNumber;
} finally {
// Step 5: Release Redis lock
await lock.release();
// Step 6: Release Redis lock
if (lock) {
await lock.release();
}
}
}
private async formatNumber(
format: DocumentNumberFormat,
data: any
): Promise<string> {
let result = format.format_template;
// Replace tokens
private async formatNumber(template: string, data: any): Promise<string> {
// Token replacement logic
const tokens = {
'{ORG_CODE}': await this.getOrgCode(data.organizationId),
'{TYPE_CODE}': await this.getTypeCode(data.typeId),
'{DISCIPLINE_CODE}': await this.getDisciplineCode(data.disciplineId),
'{YEAR}': data.year.toString(),
'{PROJECT}': await this.getProjectCode(data.projectId),
'{ORG}': await this.getOrgCode(data.organizationId),
'{TYPE}': await this.getTypeCode(data.docTypeId),
'{SUB_TYPE}': await this.getSubTypeCode(data.subTypeId),
'{DISCIPLINE}': await this.getDisciplineCode(data.disciplineId),
'{CATEGORY}': await this.getCategoryCode(data.categoryId),
'{SEQ:4}': data.sequenceNumber.toString().padStart(4, '0'),
'{SEQ:5}': data.sequenceNumber.toString().padStart(5, '0'),
'{YEAR:B.E.}': data.year.toString(),
'{YEAR:A.D.}': (data.year - 543).toString(),
'{REV}': data.revisionCode || 'A',
};
let result = template;
for (const [token, value] of Object.entries(tokens)) {
result = result.replace(token, value);
result = result.replace(new RegExp(token, 'g'), value);
}
return result;
}
private buildLockKey(...parts: Array<number | string | null | undefined>): string {
return `doc_num:${parts.filter(p => p !== null && p !== undefined).join(':')}`;
}
// Scenario 2: Lock Acquisition Timeout - Exponential Backoff
private async retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number,
initialDelay: number
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isRetryable =
error instanceof ConflictException ||
error.code === 'ECONNREFUSED' || // Scenario 4
error.code === 'ETIMEDOUT'; // Scenario 4
if (!isRetryable || attempt === maxRetries) {
if (attempt === maxRetries) {
throw new ServiceUnavailableException(
'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง'
);
}
throw error;
}
const delay = initialDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
this.logger.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
}
}
}
// Scenario 1: Fallback to Database Lock
private async generateWithDatabaseLock(
context: NumberingContext,
year: number,
subTypeId: number,
disciplineId: number
): Promise<string> {
return await this.counterRepo.manager.transaction(async (manager) => {
// Pessimistic lock: SELECT ... FOR UPDATE
const counter = await manager
.createQueryBuilder(DocumentNumberCounter, 'counter')
.setLock('pessimistic_write')
.where({
project_id: context.projectId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
recipient_type: context.recipientType || null,
year: year,
})
.getOne();
const nextNumber = (counter?.last_number || 0) + 1;
// Update counter
await manager.save(DocumentNumberCounter, {
...counter,
last_number: nextNumber,
});
// Format and return
const config = await this.getConfig(context.projectId, context.docTypeId, subTypeId, disciplineId);
return await this.formatNumber(config.template, {
...context,
year,
sequenceNumber: nextNumber,
});
});
}
}
```
@@ -280,62 +533,209 @@ export class DocumentNumberingService {
```mermaid
sequenceDiagram
participant Service as Correspondence Service
participant Numbering as Numbering Service
participant Client
participant Service as Numbering Service
participant Redis
participant DB as MariaDB
participant Audit
Service->>Numbering: generateNextNumber(context)
Numbering->>Redis: ACQUIRE Lock (key)
Client->>Service: generateNextNumber(context)
Service->>Redis: ACQUIRE Lock (key, TTL=5s)
alt Lock Acquired
Redis-->>Numbering: Lock Success
Numbering->>DB: SELECT counter (with version)
DB-->>Numbering: current_number, version
Numbering->>DB: UPDATE counter SET last_number = X, version = version + 1<br/>WHERE version = old_version
alt Redis Available
Redis-->>Service: Lock Success
Service->>DB: SELECT counter (with version)
DB-->>Service: current_number, version
Service->>DB: UPDATE counter SET last_number=X, version=version+1<br/>WHERE version=old_version
alt Update Success
DB-->>Numbering: Success (1 row affected)
Numbering->>Numbering: Format Number
Numbering->>Redis: RELEASE Lock
Numbering-->>Service: Document Number
else Version Conflict
DB-->>Numbering: Failed (0 rows affected)
Numbering->>Redis: RELEASE Lock
Numbering->>Numbering: Retry with Exponential Backoff
alt Update Success (No Conflict)
DB-->>Service: Success (1 row affected)
Service->>Service: Format Number with Template
Service->>Audit: Log generated number + metadata
Service->>Redis: RELEASE Lock
Service-->>Client: Document Number
else Version Conflict (Scenario 3)
DB-->>Service: Failed (0 rows affected)
Service->>Redis: RELEASE Lock
Service->>Service: Retry with Exponential Backoff (2x)
Note over Service: If still fail after 2 retries:<br/>Return 409 Conflict
end
else Lock Failed
Redis-->>Numbering: Lock Timeout
Numbering-->>Service: Error: Unable to acquire lock
else Redis Unavailable (Scenario 1)
Redis-->>Service: Connection Error
Service->>DB: BEGIN TRANSACTION
Service->>DB: SELECT ... FOR UPDATE (Pessimistic Lock)
DB-->>Service: Counter (locked)
Service->>DB: UPDATE counter
Service->>DB: COMMIT
Service-->>Client: Document Number (slower but works)
end
alt Lock Timeout (Scenario 2)
Redis-->>Service: Lock Acquisition Timeout
Service->>Service: Retry 5 times with backoff<br/>(1s, 2s, 4s, 8s, 16s)
Note over Service: If all retries fail:<br/>Return 503 Service Unavailable
end
```
---
## Error Handling Scenarios
### Scenario 1: Redis Unavailable
**Trigger:** Redis connection error, Redis down
**Fallback:**
- ใช้ Database-only locking (`SELECT ... FOR UPDATE`)
- Log warning และแจ้ง ops team
- ระบบยังใช้งานได้แต่ performance ลดลง (slower)
### Scenario 2: Lock Acquisition Timeout
**Trigger:** หลาย requests แย่งชิง lock พร้อมกัน
**Retry Logic:**
- Retry 5 ครั้งด้วย exponential backoff: 1s, 2s, 4s, 8s, 16s (รวม ~31 วินาที)
- หลัง 5 ครั้ง: Return HTTP 503 "Service Temporarily Unavailable"
- Frontend: แสดง "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง"
### Scenario 3: Version Conflict After Lock
**Trigger:** Optimistic lock version mismatch
**Retry Logic:**
- Retry 2 ครั้ง (reload counter + retry transaction)
- หลัง 2 ครั้ง: Return HTTP 409 Conflict
- Frontend: แสดง "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่"
### Scenario 4: Database Connection Error
**Trigger:** Database connection timeout, connection pool exhausted
**Retry Logic:**
- Retry 3 ครั้งด้วย exponential backoff: 1s, 2s, 4s
- หลัง 3 ครั้ง: Return HTTP 500 "Internal Server Error"
- Frontend: แสดง "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ"
---
## Performance Requirements
### Response Time Targets
| Metric | Target | Description |
|--------|--------|-------------|
| Normal Operation | <500ms | Under normal load, no conflicts |
| 95th Percentile | <2 seconds | Including retry scenarios |
| 99th Percentile | <5 seconds | Extreme cases with multiple retries |
### Throughput Targets
| Load Level | Target | Notes |
|------------|--------|-------|
| Normal Load | 50 req/sec | Typical office hours |
| Peak Load | 100 req/sec | Construction deadline periods |
### Availability
- **Uptime:** ≥99.5% (exclude planned maintenance)
- **Maximum Downtime:** ≤3.6 hours/month
---
## Monitoring & Alerting
### Metrics to Track
1. **Lock Acquisition Metrics:**
- Lock wait time (p50, p95, p99)
- Lock acquisition success rate
- Lock timeout count
2. **Counter Generation:**
- Generation latency (p50, p95, p99)
- Generation success rate
- Retry count distribution
3. **System Health:**
- Redis connection status
- Database connection pool usage
- Error rate by scenario (1-4)
### Alert Conditions
| Severity | Condition | Action |
|----------|-----------|--------|
| 🔴 Critical | Redis unavailable >1 minute | Page ops team |
| 🔴 Critical | Lock failures >10% in 5 min | Page ops team |
| 🟡 Warning | Lock failures >5% in 5 min | Alert ops team |
| 🟡 Warning | Avg lock wait time >1 second | Alert ops team |
| 🟡 Warning | Retry count >100/hour | Review system load |
### Dashboard Panels
- Real-time lock acquisition success rate (%)
- Lock wait time percentiles chart
- Counter generation rate (per minute)
- Error rate breakdown by type
- Redis/Database health indicators
---
## Security Considerations
### Authorization
- เฉพาะ **authenticated users** สามารถ request document number
- เฉพาะ **Project Admin** สามารถแก้ไข template
- เฉพาะ **Super Admin** สามารถ reset counter
### Rate Limiting
Prevent abuse และ resource exhaustion:
| Scope | Limit | Window |
|-------|-------|--------|
| Per User | 10 requests | 1 minute |
| Per IP Address | 50 requests | 1 minute |
| Global | 5000 requests | 1 minute |
**Implementation:** ใช้ Redis-based rate limiter middleware
### Audit & Compliance
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
- บันทึก: user, IP, timestamp, generated number, retry count
---
## Consequences
### Positive
1.**Zero Duplicate Risk:** Double-lock guarantees uniqueness
2.**High Performance:** Redis lock prevents most DB conflicts (< 100ms)
3.**Audit Trail:** All counters stored in database
4.**Template-Based:** Easy to configure different formats
5.**Partition Support:** Separate counters per Project/Type/Discipline/Year
6.**Discipline Integration:** รองรับ Discipline Code ตาม Requirement 6B
1.**Zero Duplicate Risk:** Double-lock + DB constraint guarantees uniqueness
2.**High Performance:** Redis lock + optimistic locking (<500ms normal)
3.**Complete Audit Trail:** All counters + generated numbers in database
4.**Highly Configurable:** Template-based for all document types
5.**Partition Support:** Separate counters per Project/Type/SubType/Discipline/Recipient/Year
6.**Resilient:** Multiple fallback strategies for all failure scenarios
7.**Transmittal Logic:** Supports recipient-based numbering
8.**Security:** Rate limiting + authorization + audit logging
### Negative
1.**Complexity:** Requires Redis + Database coordination
2.**Dependencies:** Requires both Redis and DB healthy
3.**Retry Logic:** May retry on optimistic lock conflicts
4.**Monitoring:** Need to monitor lock acquisition times
1.**Complexity:** Requires coordination between Redis and Database
2.**Dependencies:** Requires both Redis and DB healthy for optimal performance
3.**Retry Logic:** May retry causing delays under high contention
4.**Monitoring Overhead:** Need comprehensive monitoring for all scenarios
### Mitigation Strategies
- **Redis Dependency:** Use Redis Persistence (AOF) และ Replication
- **Complexity:** Encapsulate logic in `DocumentNumberingService`
- **Retry:** Exponential backoff with max 3 retries
- **Monitoring:** Track lock wait times และ conflict rates
- **Redis Dependency:** Use Redis Persistence (AOF) + Replication + Fallback to DB
- **Complexity:** Encapsulate all logic in `DocumentNumberingService`
- **Retry Delays:** Exponential backoff limits max delay time
- **Monitoring:** Automated dashboards + alerting for all critical metrics
---
@@ -346,12 +746,13 @@ sequenceDiagram
```typescript
describe('DocumentNumberingService - Concurrent Generation', () => {
it('should generate unique numbers for 100 concurrent requests', async () => {
const context = {
const context: NumberingContext = {
projectId: 1,
organizationId: 1,
typeId: 1,
disciplineId: 2, // STR
year: 2025,
docTypeId: 2, // RFA
disciplineId: 3, // STR
year: 2568,
userId: 1,
ipAddress: '192.168.1.1',
};
const promises = Array(100)
@@ -364,22 +765,61 @@ describe('DocumentNumberingService - Concurrent Generation', () => {
const unique = new Set(results);
expect(unique.size).toBe(100);
// Check sequential
const numbers = results.map((r) => parseInt(r.split('-').pop()));
const sorted = [...numbers].sort((a, b) => a - b);
expect(numbers.every((n, i) => sorted.includes(n))).toBe(true);
// Check format
results.forEach(num => {
expect(num).toMatch(/^LCBP3-C2-RFI-STR-\d{4}-A$/);
});
});
it('should use correct format template', async () => {
it('should use correct format for Transmittal To Owner', async () => {
const number = await service.generateNextNumber({
projectId: 1,
organizationId: 3, // TEAM
typeId: 1, // RFA
disciplineId: 2, // STR
year: 2025,
docTypeId: 3, // Transmittal
recipientType: 'OWNER',
year: 2568,
userId: 1,
ipAddress: '192.168.1.1',
});
expect(number).toMatch(/^TEAM-RFA-STR-2025-\d{4}$/);
expect(number).toMatch(/^คคง\.-สคฉ\.3-03-21-\d{4}-2568$/);
});
it('should fallback to DB lock when Redis unavailable', async () => {
jest.spyOn(redlock, 'acquire').mockRejectedValue(new Error('Redis down'));
const number = await service.generateNextNumber(context);
expect(number).toBeDefined();
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('falling back to DB lock'));
});
it('should retry on version conflict and succeed', async () => {
let attempt = 0;
jest.spyOn(counterRepo, 'createQueryBuilder').mockImplementation(() => {
attempt++;
return {
update: () => ({
set: () => ({
where: () => ({
execute: async () => ({
affected: attempt === 1 ? 0 : 1, // Fail first, succeed second
}),
}),
}),
}),
} as any;
});
const result = await service.generateNextNumber(context);
expect(result).toBeDefined();
expect(attempt).toBe(2);
});
it('should throw 503 after max lock acquisition retries', async () => {
jest.spyOn(redlock, 'acquire').mockRejectedValue(new Error('Lock timeout'));
await expect(service.generateNextNumber(context))
.rejects
.toThrow(ServiceUnavailableException);
});
});
```
@@ -387,23 +827,59 @@ describe('DocumentNumberingService - Concurrent Generation', () => {
### Load Testing
```yaml
# Artillery configuration
# artillery.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 50 # 50 requests/second
name: 'Normal Load'
- duration: 30
arrivalRate: 100 # 100 requests/second
name: 'Peak Load'
scenarios:
- name: 'Generate Document Numbers'
- name: 'Generate Document Numbers - RFA'
weight: 40
flow:
- post:
url: '/correspondences'
url: '/api/v1/rfa'
json:
title: 'Load Test {{ $randomString() }}'
project_id: 1
type_id: 1
discipline_id: 2
doc_type_id: 2
discipline_id: 3
- name: 'Generate Document Numbers - Transmittal'
weight: 30
flow:
- post:
url: '/api/v1/transmittals'
json:
title: 'Load Test {{ $randomString() }}'
project_id: 1
doc_type_id: 3
recipient_type: 'OWNER'
- name: 'Generate Document Numbers - Correspondence'
weight: 30
flow:
- post:
url: '/api/v1/correspondences'
json:
title: 'Load Test {{ $randomString() }}'
project_id: 1
doc_type_id: 1
expect:
- statusCode: 200
- statusCode: 201
- contentType: json
ensure:
p95: 2000 # 95th percentile < 2 seconds
p99: 5000 # 99th percentile < 5 seconds
maxErrorRate: 0.1 # < 0.1% errors
```
---
@@ -412,21 +888,34 @@ scenarios:
เป็นไปตาม:
- [Backend Plan Section 4.2.10](../../docs/2_Backend_Plan_V1_4_5.md) - DocumentNumberingModule
- [Requirements 3.11](../01-requirements/03.11-document-numbering.md) - Document Numbering
- [Requirements 6B](../../docs/2_Backend_Plan_V1_4_4.Phase6B.md) - Discipline Support
- ✅ [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
- ✅ [Security Best Practices](../02-architecture/security-architecture.md) - Rate Limiting, Audit Logging
---
## Related ADRs
- [ADR-001: Unified Workflow Engine](./ADR-001-unified-workflow-engine.md) - Workflow triggers number generation
- [ADR-005: Redis Usage Strategy](./ADR-005-redis-usage-strategy.md) - Redis lock implementation
- [ADR-005: Redis Usage Strategy](./ADR-005-redis-usage-strategy.md) - Redis lock implementation details
- [ADR-006: Audit Logging Strategy](./ADR-006-audit-logging-strategy.md) - Comprehensive audit requirements
---
## References
- [Redlock Algorithm](https://redis.io/topics/distlock)
- [TypeORM Optimistic Locking](https://typeorm.io/entities#version-column)
- [Distributed Lock Patterns](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html)
- [Redlock Algorithm](https://redis.io/topics/distlock) - Distributed locking with Redis
- [TypeORM Optimistic Locking](https://typeorm.io/entities#version-column) - Version column usage
- [Distributed Lock Patterns](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) - Martin Kleppmann's analysis
- [Redis Persistence](https://redis.io/topics/persistence) - AOF and RDB strategies
- [Rate Limiting Patterns](https://redis.io/glossary/rate-limiting/) - Redis-based rate limiting
---
## 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 |

File diff suppressed because it is too large Load Diff