1872 lines
64 KiB
Markdown
1872 lines
64 KiB
Markdown
# 3.11 Document Numbering Management (การจัดการเลขที่เอกสาร)
|
|
|
|
---
|
|
title: 'Functional Requirements: Document Numbering Management'
|
|
version: 1.6.0
|
|
status: draft
|
|
owner: Nattanin Peancharoen
|
|
last_updated: 2025-12-02
|
|
related:
|
|
|
|
- specs/01-requirements/01-objectives.md
|
|
- specs/01-requirements/02-architecture.md
|
|
- specs/01-requirements/03-functional-requirements.md
|
|
- specs/03-implementation/document-numbering.md
|
|
- specs/04-operations/document-numbering-operations.md
|
|
- specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md
|
|
|
|
---
|
|
|
|
> **📖 เอกสารที่เกี่ยวข้อง**
|
|
>
|
|
> - **Implementation Guide**: [document-numbering.md](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) - รายละเอียดการ implement ด้วย NestJS, TypeORM, Redis
|
|
> - **Operations Guide**: [document-numbering-operations.md](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) - Monitoring, Troubleshooting, Maintenance Procedures
|
|
|
|
## 3.11.1. วัตถุประสงค์
|
|
|
|
- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติ, ที่มีความเป็นเอกลักษณ์ (unique) และยืดหยุ่นสูง
|
|
- ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร
|
|
- ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์
|
|
- ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย
|
|
|
|
## 3.11.2. Logic การนับเลข (Counter Logic)
|
|
|
|
การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร
|
|
**Scopes**:
|
|
1. **Global**: Sequence ระดับระบบทั้งหมด
|
|
2. **Organization**: Sequence แยกตามองค์กรผู้ส่ง
|
|
3. **Project**: Sequence แยกตามโครงการ
|
|
4. **Contract**: Sequence แยกตามสัญญา
|
|
5. **Yearly**: Sequence reset ทุกปี
|
|
|
|
### Counter Key Components
|
|
|
|
| Component | Required? | Description | Database Source | Default if NULL |
|
|
| ---------------------------- | ---------------- | ------------------- | --------------------------------------------------------- | --------------- |
|
|
| `project_id` | ✅ Yes | ID โครงการ | Derived from user context or organization | - |
|
|
| `originator_organization_id` | ✅ Yes | ID องค์กรผู้ส่ง | `correspondences.originator_id` | - |
|
|
| `recipient_organization_id` | Depends on type | ID องค์กรผู้รับหลัก (TO) | `correspondence_recipients` where `recipient_type = 'TO'` | NULL for RFA |
|
|
| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | 0 |
|
|
| `sub_type_id` | TRANSMITTAL only | ID ประเภทย่อย | `correspondence_sub_types.id` | 0 |
|
|
| `rfa_type_id` | RFA only | ID ประเภท RFA | `rfa_types.id` | 0 |
|
|
| `discipline_id` | RFA only | ID สาขางาน | `disciplines.id` | 0 |
|
|
| `current_year` | ✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - |
|
|
|
|
### Counter Key แยกตามประเภทเอกสาร (correspondence_type_id)
|
|
|
|
#### **RFA / RFI / TRANSMITTAL / EMAIL / INSTRUCTION / LETTER / MEMO / MOM / NOTICE / OTHER**:
|
|
|
|
```
|
|
(project_id, originator_organization_id, recipient_organization_id, 0, 0, 0, 0, current_year)
|
|
```
|
|
|
|
**หมายเหตุ**:
|
|
- ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id`ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้
|
|
- ถ้ามีการเพิ่ม - correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ
|
|
|
|
#### **TRANSMITTAL**:
|
|
|
|
```
|
|
(project_id, originator_organization_id, recipient_organization_id,
|
|
correspondence_type_id, sub_type_id, 0, 0, current_year)
|
|
```
|
|
|
|
*หมายเหตุ*: ใช้ `sub_type_id` เพิ่มเติม
|
|
|
|
#### **RFA**:
|
|
|
|
```
|
|
(project_id, originator_organization_id, NULL,
|
|
correspondence_type_id, 0, rfa_type_id, discipline_id, current_year)
|
|
```
|
|
|
|
*หมายเหตุ*: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER)
|
|
|
|
##### วิธีการหา project_id
|
|
|
|
เนื่องจาก Template ของ LETTER/TRANSMITTAL ไม่มี `{PROJECT}` token ระบบจะหา `project_id` จาก:
|
|
|
|
1. **User Context** (แนะนำ):
|
|
- เมื่อ User สร้างเอกสาร UI จะให้เลือก Project/Contract ก่อน
|
|
- ใช้ `project_id` จาก Context ที่เลือก
|
|
|
|
2. **จาก Organization**:
|
|
- Query `project_organizations` หรือ `contract_organizations`
|
|
- ใช้ `originator_organization_id` หา project ที่เกี่ยวข้อง
|
|
- ถ้ามีหลาย project ให้ User เลือก
|
|
|
|
3. **Validation**:
|
|
- ตรวจสอบว่า organization มีสิทธิ์ใน project นั้น
|
|
- ตรวจสอบว่า project/contract เป็น active
|
|
|
|
##### Fallback สำหรับค่า NULL
|
|
|
|
- `correspondence_type_id`: ใช้ `0` (ไม่ระบุประเภทเอกสาร)
|
|
- `discipline_id`: ใช้ `0` (ไม่ระบุสาขางาน)
|
|
- `sub_type_id`: ใช้ `0` (ไม่มีประเภทย่อย)
|
|
- `rfa_type_id`: ใช้ `0` (ไม่ระบุประเภท RFA)
|
|
- `recipient_organization_id`: ใช้ `NULL` สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL
|
|
|
|
## 3.11.3. Format Templates by Correspondence Type
|
|
|
|
> **📝 หมายเหตุสำคัญ**
|
|
> - Templates ด้านล่างเป็น **ตัวอย่าง** สำหรับประเภทเอกสารหลัก
|
|
> - ระบบรองรับ **ทุกประเภทเอกสาร** ที่อยู่ใน `correspondence_types` table
|
|
> - หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ
|
|
> - Admin สามารถกำหนด Template เฉพาะสำหรับแต่ละประเภทผ่าน Admin Panel
|
|
|
|
### 3.11.3.1. Global (correspondence_type_id = NULL)
|
|
|
|
**Template**:
|
|
|
|
```
|
|
{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}
|
|
```
|
|
|
|
**Example**: `คคง.-สคฉ.3-0001-2568`
|
|
|
|
**Token Breakdown**:
|
|
|
|
- `คคง.` = {ORIGINATOR} = รหัสองค์กรผู้ส่ง
|
|
- `สคฉ.3` = {RECIPIENT} = รหัสองค์กรผู้รับหลัก (TO)
|
|
- `0001` = {SEQ:4} = Running number (เริ่ม 0001, 0002, ...)
|
|
- `2568` = {YEAR:B.E.} = ปี พ.ศ.
|
|
|
|
> **⚠️ Template vs Counter Separation**
|
|
>
|
|
> - {CORR_TYPE} **ไม่แสดง**ใน template เพื่อความกระชับ
|
|
> - แต่ระบบ**ยังใช้ correspondence_type_id ใน Counter Key** เพื่อแยก counter
|
|
> - LETTER, MEMO, RFI **มี counter แยกกัน** แม้ template format เหมือนกัน
|
|
|
|
**Counter Key**: `(project_id, originator_org_id, recipient_org_id, correspondence_type_id, 0, 0, 0, year)`
|
|
|
|
---
|
|
|
|
### 3.11.3.2. Transmittal (TYPE = TRANSMITTAL)
|
|
|
|
**Template**:
|
|
|
|
```
|
|
{ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}
|
|
```
|
|
|
|
**Example**: `คคง.-สคฉ.3-21-0117-2568`
|
|
|
|
**Token Breakdown**:
|
|
|
|
- `คคง.` = {ORIGINATOR}
|
|
- `สคฉ.3` = {RECIPIENT}
|
|
- `21` = {SUB_TYPE} = หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 14=MET, ...)
|
|
- `0117` = {SEQ:4}
|
|
- `2568` = {YEAR:B.E.}
|
|
|
|
> **⚠️ Template vs Counter Separation**
|
|
>
|
|
> - {CORR_TYPE} **ไม่แสดง**ใน template (เหมือน LETTER)
|
|
> - TRANSMITTAL มี counter แยกจาก LETTER
|
|
|
|
**Counter Key**: `(project_id, originator_org_id, recipient_org_id, correspondence_type_id, sub_type_id, 0, 0, year)`
|
|
|
|
---
|
|
|
|
### 3.11.3.3. RFA (Request for Approval)
|
|
|
|
**Template**:
|
|
|
|
```
|
|
{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV}
|
|
```
|
|
|
|
**Example**: `LCBP3-C2-RFA-TER-RPT-0001-A`
|
|
|
|
**Token Breakdown**:
|
|
|
|
- `LCBP3-C2` = {PROJECT} = รหัสโครงการ
|
|
- `RFA` = {CORR_TYPE} = ประเภทเอกสาร (**แสดง**ใน RFA template)
|
|
- `TER` = {DISCIPLINE} = รหัสสาขางาน (TER=Terminal, STR=Structure, ...)
|
|
- `RPT` = {RFA_TYPE} = ประเภท RFA (RPT=Report, SDW=Shop Drawing, ...)
|
|
- `0001` = {SEQ:4}
|
|
- `A` = {REV} = Revision code
|
|
|
|
> **📋 RFA Workflow**
|
|
>
|
|
> - RFA เป็น **เอกสารโครงการ** (Project-level document)
|
|
> - Workflow: **CONTRACTOR → CONSULTANT → OWNER**
|
|
> - ไม่มี specific `recipient_id` เพราะเป็น workflow ที่กำหนดไว้แล้ว
|
|
|
|
**Counter Key**: `(project_id, originator_org_id, NULL, correspondence_type_id, 0, rfa_type_id, discipline_id, year)`
|
|
|
|
---
|
|
|
|
### 3.11.3.4. Drawing
|
|
|
|
**Status**: 🚧 **To Be Determined**
|
|
|
|
Drawing Numbering ยังไม่ได้กำหนด Template เนื่องจาก:
|
|
|
|
- มีความซับซ้อนสูง (Contract Drawing และ Shop Drawing มีกฎต่างกัน)
|
|
- อาจต้องใช้ระบบ Numbering แยกต่างหาก
|
|
- ต้องพิจารณาร่วมกับ RFA ที่เกี่ยวข้อง
|
|
|
|
---
|
|
|
|
## 3.11.4. Supported Token Types
|
|
|
|
| Token | Description | Example | Database Source |
|
|
| -------------- | ---------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` |
|
|
| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` |
|
|
| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` where `recipient_type = 'TO'` |
|
|
| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` |
|
|
| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` |
|
|
| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` |
|
|
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` |
|
|
| `{SEQ:n}` | Running number (n = จำนวนหลัก) | `0001`, `0029`, `0985` | Based on `document_number_counters.last_number + 1` |
|
|
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `document_number_counters.current_year + 543` |
|
|
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `document_number_counters.current_year` |
|
|
| `{REV}` | Revision Code | `A`, `B`, `AA` | `correspondence_revisions.revision_label` |
|
|
|
|
### Token Usage Notes
|
|
|
|
**{SEQ:n}**:
|
|
|
|
- `n` = จำนวนหลักที่ต้องการ (typically 4)
|
|
- Counter **เริ่มจาก 0001** และเพิ่มทีละ 1 (0001, 0002, 0003, ...)
|
|
- Padding ด้วย 0 ทางซ้าย
|
|
- Reset ทุกปี (ตาม `current_year` ใน Counter Key)
|
|
|
|
**{RECIPIENT}**:
|
|
|
|
- ใช้เฉพาะผู้รับที่มี `recipient_type = 'TO'` เท่านั้น
|
|
- ถ้ามีหลาย TO ให้ใช้คนแรก (ตาม sort order)
|
|
- **ไม่ใช้สำหรับ RFA** (RFA ไม่มี {RECIPIENT} ใน template)
|
|
|
|
**{CORR_TYPE}**:
|
|
|
|
- รองรับทุกค่าจาก `correspondence_types.type_code`
|
|
- ถ้าม�การเพิ่มประเภทใหม่ จะใช้งานได้ทันที
|
|
- **แสดงใน template**: RFA only
|
|
- **ไม่แสดงแต่ใช้ใน counter**: LETTER, TRANSMITTAL, และ Other types
|
|
|
|
**Deprecated Tokens** (ไม่ควรใช้):
|
|
|
|
- ~~`{ORG}`~~ → ใช้ `{ORIGINATOR}` หรือ `{RECIPIENT}` แทน
|
|
- ~~`{TYPE}`~~ → ใช้ `{CORR_TYPE}`, `{SUB_TYPE}`, หรือ `{RFA_TYPE}` แทน (ตามบริบท)
|
|
- ~~`{CATEGORY}`~~ → ไม่ได้ใช้งานในระบบปัจจุบัน
|
|
|
|
## 3.11.5. Security & Data Integrity Requirements
|
|
|
|
### 3.11.5.1. Concurrency Control
|
|
|
|
**Requirements:**
|
|
|
|
- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน
|
|
- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์
|
|
- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary
|
|
- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน
|
|
|
|
**Implementation Details:** ดู [Implementation Guide - Section 2.3](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#23-redis-lock-service)
|
|
|
|
### 3.11.5.2. Data Integrity
|
|
|
|
**Requirements:**
|
|
|
|
- ระบบ**ต้อง**ใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates
|
|
- ระบบ**ต้อง**มี database constraints เพื่อป้องกันข้อมูลผิดพลาด:
|
|
- Unique constraint บน `document_number`
|
|
- Foreign key constraints ทุก relationship
|
|
- Check constraints สำหรับ business rules
|
|
|
|
### 3.11.5.3. Authorization
|
|
|
|
**Requirements:**
|
|
|
|
- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number
|
|
- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้
|
|
- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ (requires approval)
|
|
|
|
## 3.11.6. Error Handling Requirements
|
|
|
|
### 3.11.6.1. Retry Mechanism
|
|
|
|
**Requirements:**
|
|
|
|
ระบบ**ต้อง**จัดการ error scenarios ต่อไปนี้:
|
|
|
|
| Scenario | Strategy | Max Retries | Expected Response |
|
|
| ------------------- | ------------------- | ----------- | ------------------------------- |
|
|
| Redis Unavailable | Fallback to DB Lock | 0 | Continue (degraded performance) |
|
|
| Lock Timeout | Exponential Backoff | 5 | HTTP 503 after final retry |
|
|
| Version Conflict | Immediate Retry | 2 | HTTP 409 after final retry |
|
|
| DB Connection Error | Exponential Backoff | 3 | HTTP 500 after final retry |
|
|
|
|
**Implementation Details:** ดู [Implementation Guide - Section 2.5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#25-main-service-with-retry-logic)
|
|
|
|
### 3.11.6.2. User Experience
|
|
|
|
**Requirements:**
|
|
|
|
- Error messages **ต้อง**เป็นภาษาไทย และเข้าใจง่าย
|
|
- HTTP status codes **ต้อง**สื่อความหมายที่ถูกต้อง
|
|
- Frontend **ควร**แสดง retry option สำหรับ transient errors
|
|
|
|
## 3.11.7. Configuration Management Requirements
|
|
|
|
### 3.11.7.1. Template Management
|
|
|
|
**Requirements:**
|
|
|
|
- Project Admin **ต้อง**สามารถกำหนด/แก้ไข template ผ่าน Admin Panel
|
|
- ระบบ**ต้อง**validate template ก่อนบันทึก
|
|
- การเปลี่ยนแปลง template **ต้องไม่**ส่งผลต่อเอกสารที่สร้างไว้แล้ว
|
|
|
|
### 3.11.7.2. Template Versioning
|
|
|
|
**Requirements:**
|
|
|
|
- ระบบ**ต้อง**เก็บ history ของ template changes
|
|
- ระบบ**ต้อง**บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง
|
|
- ระบบ**ควร**สามารถ rollback ไปเวอร์ชันก่อนหน้าได้
|
|
|
|
### 3.11.7.3. Counter Reset Policy
|
|
|
|
**Requirements:**
|
|
|
|
- Counter **ต้อง**reset ตามปี (อัตโนมัติ)
|
|
- Admin **ต้อง**สามารถ manual reset counter ได้ (require approval + audit log)
|
|
|
|
**Implementation Details:** ดู [Implementation Guide - Section 4](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#4-bullmq-job-for-counter-reset)
|
|
|
|
### 3.11.7.4. Manual Override
|
|
|
|
**Requirements:**
|
|
ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override)
|
|
|
|
**Use Cases**:
|
|
- 1. Import เอกสารเก่าจากระบบเดิม
|
|
- 2. External documents จาก client/consultant
|
|
- 3. Correction หลังพบความผิดพลาด
|
|
|
|
**Implementation Details:**
|
|
- ตรวจสอบ duplicate ก่อน save
|
|
- Validate format ตามรูปแบบที่กำหนด
|
|
- Auto-update sequence counter ถ้าเลขที่สูงกว่า current
|
|
- บันทึก audit log ว่าเป็น manual override
|
|
- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น
|
|
|
|
### 3.11.7.5. Auto-Override
|
|
|
|
**Requirements:**
|
|
ระบบต้องรองรับการกำหนดเลขที่เอกสารอัตโนมัติ (auto override)
|
|
|
|
**Use Cases**:
|
|
- 1. Import เอกสารเก่าจากระบบเดิม
|
|
- 2. External documents จาก client/consultant
|
|
- 3. Correction หลังพบความผิดพลาด
|
|
|
|
**Implementation Details:**
|
|
- ตรวจสอบ duplicate ก่อน save
|
|
- Validate format ตามรูปแบบที่กำหนด
|
|
- Auto-update sequence counter ถ้าเลขที่สูงกว่า current
|
|
- บันทึก audit log ว่าเป็น auto override
|
|
- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น
|
|
|
|
###
|
|
## 3.11.9 Audit Trail Requirements
|
|
|
|
### 3.11.9.1. Audit Logging
|
|
|
|
**Requirements:**
|
|
|
|
ระบบ**ต้อง**บันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation:
|
|
|
|
- `document_id` - เอกสารที่ถูกสร้าง
|
|
- `generated_number` - เลขที่ถูกสร้าง
|
|
- `counter_key` - key ที่ใช้ในการนับ (JSON format)
|
|
- `template_used` - template ที่ใช้
|
|
- `user_id` - ผู้ที่ request
|
|
- `ip_address` - IP address ของผู้ request
|
|
- `timestamp` - เวลาที่สร้าง
|
|
- `retry_count` - จำนวนครั้งที่ retry
|
|
- `performance_metrics` - Lock wait time, total duration
|
|
|
|
### 3.11.9.2. Error Logging
|
|
|
|
**Requirements:**
|
|
|
|
- ระบบ**ต้อง**บันทึก error แยกต่างหาก พร้อม error type classification
|
|
- ระบบ**ควร**alert ops team สำหรับ critical errors
|
|
|
|
### 3.11.9.3. Retention Policy
|
|
|
|
**Requirements:**
|
|
|
|
- Audit log **ต้อง**เก็บอย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
|
|
|
|
## 3.11.10 Performance Requirements
|
|
|
|
### 3.11.10.1. Response Time
|
|
|
|
**SLA Targets:**
|
|
|
|
| Metric | Target | Notes |
|
|
| ---------------- | -------- | ------------------------ |
|
|
| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response |
|
|
| 99th percentile | ≤ 5 วินาที | รวม retry attempts |
|
|
| Normal operation | ≤ 500ms | ไม่มี retry |
|
|
|
|
### 3.11.10.2. Throughput
|
|
|
|
**Capacity Targets:**
|
|
|
|
| Load Level | Target | Notes |
|
|
| ----------- | ----------- | --------- |
|
|
| Normal load | ≥ 50 req/s | ใช้งานปกติ |
|
|
| Peak load | ≥ 100 req/s | ช่วงเร่งงาน |
|
|
|
|
### 3.11.10.3. Availability
|
|
|
|
**SLA Targets:**
|
|
|
|
- **Uptime**: ≥ 99.5% (excluding planned maintenance)
|
|
- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน
|
|
- **RTO**: ≤ 30 นาที
|
|
- **RPO**: ≤ 5 นาที
|
|
|
|
**Operations Details:** ดู [Operations Guide - Section 1](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#1-performance-requirements)
|
|
|
|
## 3.11.11 Monitoring & Alerting Requirements
|
|
|
|
### 3.11.11.1. Metrics
|
|
|
|
**Requirements:**
|
|
|
|
ระบบ**ต้อง**collect metrics ต่อไปนี้:
|
|
|
|
- Lock acquisition time (p50, p95, p99)
|
|
- Lock acquisition success/failure rate
|
|
- Counter generation latency
|
|
- Retry count distribution
|
|
- Redis connection status
|
|
- Database connection pool usage
|
|
|
|
### 3.11.11.2. Alerts
|
|
|
|
**Requirements:**
|
|
|
|
ระบบ**ต้อง**alert สำหรับ conditions ต่อไปนี้:
|
|
|
|
| Severity | Condition | Action |
|
|
| ---------- | ---------------------------- | ----------------- |
|
|
| 🔴 Critical | Redis unavailable > 1 minute | PagerDuty + Slack |
|
|
| 🔴 Critical | Lock failures > 10% in 5 min | PagerDuty + Slack |
|
|
| 🟡 Warning | Lock failures > 5% in 5 min | Slack |
|
|
| 🟡 Warning | Avg lock wait time > 1 sec | Slack |
|
|
| 🟡 Warning | Retry count > 100/hour | Slack |
|
|
|
|
### 3.11.11.3. Dashboard
|
|
|
|
**Requirements:**
|
|
|
|
- Ops team **ต้อง**มี real-time dashboard แสดง:
|
|
- Lock acquisition success rate
|
|
- Lock wait time percentiles
|
|
- Generation rate (per minute)
|
|
- Error rate by type
|
|
- Connection health status
|
|
|
|
**Operations Details:** ดู [Operations Guide - Section 3](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#3-monitoring--metrics)
|
|
|
|
|
|
## 3.11.12 API Reference
|
|
|
|
เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้:
|
|
|
|
### Document Number Generation
|
|
|
|
```http
|
|
POST /api/v1/documents/{documentId}/generate-number
|
|
```
|
|
|
|
สร้างเลขที่เอกสารสำหรับ document ที่ระบุ
|
|
|
|
**Request Body:**
|
|
|
|
```json
|
|
{
|
|
"counterKey": {
|
|
"projectId": 2,
|
|
"originatorOrgId": 22,
|
|
"recipientOrgId": 10,
|
|
"correspondenceTypeId": 6,
|
|
"subTypeId": 0,
|
|
"rfaTypeId": 0,
|
|
"disciplineId": 0,
|
|
"year": 2025
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"documentNumber": "คคง.-สคฉ.3-0001-2568",
|
|
"generatedAt": "2025-12-02T15:30:00Z"
|
|
}
|
|
```
|
|
|
|
### Template Management
|
|
|
|
```http
|
|
GET /api/v1/document-numbering/configs
|
|
```
|
|
|
|
ดูรายการ template configuration ทั้งหมด
|
|
|
|
```http
|
|
PUT /api/v1/document-numbering/configs/{configId}
|
|
```
|
|
|
|
แก้ไข template (Project Admin only)
|
|
|
|
```http
|
|
POST /api/v1/document-numbering/configs/{configId}/reset-counter
|
|
```
|
|
|
|
Reset counter (Super Admin only, requires approval)
|
|
|
|
**รายละเอียดเพิ่มเติม:** ดู [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md)
|
|
|
|
## 3.11.13 Database Schema Reference
|
|
|
|
เอกสารนี้อ้างอิงถึง database tables ต่อไปนี้:
|
|
|
|
### Core Tables
|
|
|
|
- `document_number_counters` - เก็บ counter values และ template configuration
|
|
- `document_number_audit` - เก็บ audit trail ของการ generate เลขที่
|
|
- `document_number_errors` - เก็บ error logs
|
|
|
|
### Related Tables
|
|
|
|
- `documents` - เก็บ document number ที่ถูกสร้าง (column: `document_number` UNIQUE)
|
|
- `correspondence_types` - ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.)
|
|
- `correspondence_sub_types` - ประเภทย่อย (สำหรับ TRANSMITTAL)
|
|
- `rfa_types` - ประเภท RFA (SHD, RPT, MAT, etc.)
|
|
- `disciplines` - สาขาวิชา (TER, STR, GEO, etc.)
|
|
- `projects` - โครงการ
|
|
- `organizations` - องค์กร
|
|
|
|
**Schema Details:** ดู [Implementation Guide - Section 1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#1-database-implementation)
|
|
|
|
## 3.11.14 Database Schema Requirements
|
|
|
|
### 3.11.14.1 Counter Table Schema Requirements
|
|
|
|
**Primary Table**: `document_number_counters`
|
|
|
|
**Required Columns:**
|
|
- Composite primary key: `(project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)`
|
|
- `version` - สำหรับ optimistic locking
|
|
- `last_number` - counter value (เริ่มจาก 0)
|
|
|
|
**Important Notes:**
|
|
- ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL
|
|
- Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน)
|
|
- ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน
|
|
|
|
**Schema Details:** ดู [Implementation Guide - Section 1.1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#11-counter-table-schema)
|
|
|
|
### 3.11.14.2 Audit Table Requirements
|
|
|
|
**Primary Table**: `document_number_audit`
|
|
|
|
**Required Columns:**
|
|
- `document_id`, `generated_number`, `counter_key` (JSON)
|
|
- `template_used`, `user_id`, `ip_address`
|
|
- Performance metrics: `retry_count`, `lock_wait_ms`, `total_duration_ms`
|
|
- `fallback_used` - tracking fallback scenarios
|
|
|
|
**Retention:** ≥ 7 ปี
|
|
|
|
### 3.11.14.3 Error Log Requirements
|
|
|
|
**Primary Table**: `document_number_errors`
|
|
|
|
**Required Columns:**
|
|
- `error_type` - ENUM classification
|
|
- `error_message`, `stack_trace`, `context_data` (JSON)
|
|
- `user_id`, `ip_address`, `created_at`, `resolved_at`
|
|
|
|
## 3.11.15 Security Considerations
|
|
|
|
### 3.11.15.1 Authorization
|
|
|
|
- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number
|
|
- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้
|
|
- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้
|
|
|
|
### 3.11.15.2 Rate Limiting
|
|
|
|
**Requirements:**
|
|
- Limit ต่อ user: **10 requests/minute** (prevent abuse)
|
|
- Limit ต่อ IP: **50 requests/minute**
|
|
|
|
**Implementation Details:** ดู [Implementation Guide - Section 5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#5-api-controller)
|
|
|
|
### 3.11.15.3 Audit & Compliance
|
|
|
|
**Requirements:**
|
|
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
|
|
- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
|
|
- Audit log **ต้องไม่**สามารถแก้ไขได้ (immutable)
|
|
|
|
---
|
|
|
|
## 3.11.16 References
|
|
|
|
- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md)
|
|
- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md)
|
|
- [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md)
|
|
- [Data Dictionary](file:///e:/np-dms/lcbp3/specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md)
|
|
|
|
|
|
```
|
|
lock:docnum:{project_id}:{org_id}:{recip_id}:{type_id}:{sub}:{rfa}:{disc}:{year}
|
|
```
|
|
|
|
**Lock Configuration**:
|
|
|
|
- **TTL**: 5 วินาที (auto-release เมื่อ timeout)
|
|
- **Acquisition Timeout**: 10 วินาที
|
|
- **Retry Delay**: 100ms (exponential backoff)
|
|
- **Drift Factor**: 0.01 (Redlock algorithm)
|
|
|
|
**Implementation (NestJS)**:
|
|
|
|
```typescript
|
|
// src/document-numbering/services/document-numbering-lock.service.ts
|
|
import Redlock from 'redlock';
|
|
import { Injectable } from '@nestjs/common';
|
|
|
|
@Injectable()
|
|
export class DocumentNumberingLockService {
|
|
private redlock: Redlock;
|
|
|
|
async acquireLock(counterKey: CounterKey): Promise<Lock> {
|
|
const lockKey = this.buildLockKey(counterKey);
|
|
return await this.redlock.acquire([lockKey], 5000); // 5s TTL
|
|
}
|
|
|
|
private buildLockKey(key: CounterKey): string {
|
|
return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` +
|
|
`${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` +
|
|
`${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`;
|
|
}
|
|
}
|
|
```
|
|
|
|
## 3.11.17 Optimistic Locking
|
|
|
|
ใช้ **TypeORM Optimistic Lock** ร่วมกับ `@Version()` decorator:
|
|
|
|
**Entity Definition**:
|
|
|
|
```typescript
|
|
// src/document-numbering/entities/document-number-counter.entity.ts
|
|
import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm';
|
|
|
|
@Entity('document_number_counters')
|
|
export class DocumentNumberCounter {
|
|
@PrimaryColumn({ name: 'project_id' })
|
|
projectId: number;
|
|
|
|
@PrimaryColumn({ name: 'originator_organization_id' })
|
|
originatorOrganizationId: number;
|
|
|
|
@PrimaryColumn({ name: 'recipient_organization_id', nullable: true })
|
|
recipientOrganizationId: number | null;
|
|
|
|
@PrimaryColumn({ name: 'correspondence_type_id' })
|
|
correspondenceTypeId: number;
|
|
|
|
@PrimaryColumn({ name: 'sub_type_id', default: 0 })
|
|
subTypeId: number;
|
|
|
|
@PrimaryColumn({ name: 'rfa_type_id', default: 0 })
|
|
rfaTypeId: number;
|
|
|
|
@PrimaryColumn({ name: 'discipline_id', default: 0 })
|
|
disciplineId: number;
|
|
|
|
@PrimaryColumn({ name: 'current_year' })
|
|
currentYear: number;
|
|
|
|
@VersionColumn({ name: 'version' })
|
|
version: number;
|
|
|
|
@Column({ name: 'last_number', default: 0 })
|
|
lastNumber: number;
|
|
}
|
|
```
|
|
|
|
**Transaction Handling**:
|
|
|
|
```typescript
|
|
// ใช้ TypeORM Transaction + Optimistic Lock
|
|
await this.connection.transaction(async (manager) => {
|
|
const counter = await manager.findOne(DocumentNumberCounter, {
|
|
where: counterKey
|
|
});
|
|
|
|
counter.lastNumber += 1;
|
|
await manager.save(counter); // auto-check version
|
|
});
|
|
```
|
|
|
|
หาก version conflict → TypeORM throw `OptimisticLockVersionMismatchError` → retry
|
|
|
|
## 3.11.18 Database Constraints
|
|
|
|
**Unique Constraints**:
|
|
|
|
```sql
|
|
-- บน documents table
|
|
ALTER TABLE documents
|
|
ADD CONSTRAINT uq_document_number UNIQUE (document_number);
|
|
```
|
|
|
|
**Foreign Key Constraints**:
|
|
|
|
- `project_id` → `projects(id)` ON DELETE CASCADE
|
|
- `originator_organization_id` → `organizations(id)` ON DELETE CASCADE
|
|
- `recipient_organization_id` → `organizations(id)` ON DELETE CASCADE
|
|
- `correspondence_type_id` → `correspondence_types(id)` ON DELETE CASCADE
|
|
|
|
**Check Constraints**:
|
|
|
|
```sql
|
|
-- ตรวจสอบว่า last_number ≥ 0
|
|
ALTER TABLE document_number_counters
|
|
ADD CONSTRAINT chk_last_number_positive CHECK (last_number >= 0);
|
|
|
|
-- ตรวจสอบว่า current_year เป็นปี ค.ศ. ที่สมเหตุสมผล
|
|
ALTER TABLE document_number_counters
|
|
ADD CONSTRAINT chk_current_year_valid
|
|
CHECK (current_year BETWEEN 2020 AND 2100);
|
|
```
|
|
|
|
## 3.11.19 Retry Mechanism & Error Handling
|
|
|
|
### 3.11.19.1 Scenario 1: Redis Unavailable
|
|
|
|
**Fallback Strategy**: Database-only Pessimistic Locking
|
|
|
|
**Implementation**:
|
|
|
|
```typescript
|
|
// src/document-numbering/services/document-numbering.service.ts
|
|
@Injectable()
|
|
export class DocumentNumberingService {
|
|
async generateDocumentNumber(dto: GenerateNumberDto): Promise<string> {
|
|
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<string> {
|
|
return await this.connection.transaction(async (manager) => {
|
|
// SELECT ... FOR UPDATE = Pessimistic Lock
|
|
const counter = await manager
|
|
.createQueryBuilder(DocumentNumberCounter, 'c')
|
|
.where(counterKeyCondition)
|
|
.setLock('pessimistic_write')
|
|
.getOne();
|
|
|
|
counter.lastNumber += 1;
|
|
await manager.save(counter);
|
|
return this.formatNumber(counter);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Monitoring**:
|
|
|
|
- Log warning พร้อม context (project_id, user_id, timestamp)
|
|
- Alert Ops Team ผ่าน Slack/Email
|
|
- ระบบยังใช้งานได้แต่ performance อาจลดลง 30-50%
|
|
|
|
### 3.11.19.2 Scenario 2: Lock Acquisition Timeout
|
|
|
|
**Retry Strategy**: Exponential Backoff with Jitter
|
|
|
|
```typescript
|
|
// ใช้ @nestjs/common Retry Decorator หรือ custom retry logic
|
|
import { retry } from 'rxjs/operators';
|
|
|
|
const RETRY_CONFIG = {
|
|
maxRetries: 5,
|
|
delays: [1000, 2000, 4000, 8000, 16000], // exponential backoff
|
|
jitter: 0.1 // เพิ่ม randomness ป้องกัน thundering herd
|
|
};
|
|
|
|
async acquireLockWithRetry(key: CounterKey): Promise<Lock> {
|
|
for (let i = 0; i < RETRY_CONFIG.maxRetries; i++) {
|
|
try {
|
|
return await this.lockService.acquireLock(key);
|
|
} catch (error) {
|
|
if (i === RETRY_CONFIG.maxRetries - 1) {
|
|
throw new ServiceUnavailableException(
|
|
'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง'
|
|
);
|
|
}
|
|
const delay = RETRY_CONFIG.delays[i];
|
|
const jitter = delay * RETRY_CONFIG.jitter * Math.random();
|
|
await this.sleep(delay + jitter);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
|
|
- HTTP Status: `503 Service Temporarily Unavailable`
|
|
- Response Body:
|
|
|
|
```json
|
|
{
|
|
"statusCode": 503,
|
|
"message": "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง",
|
|
"error": "Service Unavailable",
|
|
"retryAfter": 30
|
|
}
|
|
```
|
|
|
|
### 3.11.19.3 Scenario 3: Version Conflict (Optimistic Lock)
|
|
|
|
**Retry Strategy**: Immediate Retry (2 attempts)
|
|
|
|
```typescript
|
|
async incrementCounter(counterKey: CounterKey): Promise<number> {
|
|
const MAX_RETRIES = 2;
|
|
|
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
try {
|
|
return await this.connection.transaction(async (manager) => {
|
|
const counter = await manager.findOne(
|
|
DocumentNumberCounter,
|
|
{ where: counterKey }
|
|
);
|
|
|
|
counter.lastNumber += 1;
|
|
await manager.save(counter); // Version check ที่นี่
|
|
return counter.lastNumber;
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof OptimisticLockVersionMismatchError) {
|
|
this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`);
|
|
if (attempt === MAX_RETRIES - 1) {
|
|
throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
|
|
}
|
|
// Retry ทันที (ไม่มี delay)
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
|
|
- HTTP Status: `409 Conflict`
|
|
- Frontend Action: Auto-retry หรือแสดง toast notification
|
|
|
|
### 3.11.19.4 Scenario 4: Database Connection Error
|
|
|
|
**Retry Strategy**: Exponential Backoff (3 attempts)
|
|
|
|
```typescript
|
|
const DB_RETRY_CONFIG = {
|
|
maxRetries: 3,
|
|
delays: [1000, 2000, 4000]
|
|
};
|
|
|
|
// TypeORM connection retry (กำหนดใน ormconfig)
|
|
{
|
|
type: 'mysql',
|
|
extra: {
|
|
connectionLimit: 10,
|
|
acquireTimeout: 10000,
|
|
// Retry connection 3 ครั้ง
|
|
retryAttempts: 3,
|
|
retryDelay: 1000
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
|
|
- HTTP Status: `500 Internal Server Error`
|
|
- Response Body:
|
|
|
|
```json
|
|
{
|
|
"statusCode": 500,
|
|
"message": "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ",
|
|
"error": "Internal Server Error",
|
|
"ref": "ERR-20250102-1234-ABCD"
|
|
}
|
|
```
|
|
|
|
- Alerting: ส่ง PagerDuty/Slack alert ทันที (severity: CRITICAL)
|
|
|
|
## 3.11.20 Configuration Management
|
|
|
|
### 3.11.20.1 Admin Panel Configuration
|
|
|
|
**Features**:
|
|
|
|
- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Web UI
|
|
- Preview document number ก่อนบันทึก
|
|
- Template validation แบบ real-time
|
|
|
|
**Template Validation Logic**:
|
|
|
|
```typescript
|
|
// src/document-numbering/validators/template.validator.ts
|
|
import { Injectable } from '@nestjs/common';
|
|
|
|
@Injectable()
|
|
export class TemplateValidator {
|
|
private readonly ALLOWED_TOKENS = [
|
|
'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE',
|
|
'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV'
|
|
];
|
|
|
|
validate(template: string, correspondenceType: string): ValidationResult {
|
|
const tokens = this.extractTokens(template);
|
|
const errors: string[] = [];
|
|
|
|
// ตรวจสอบ Token ที่ไม่รู้จัก
|
|
for (const token of tokens) {
|
|
if (!this.ALLOWED_TOKENS.includes(token.name)) {
|
|
errors.push(`Unknown token: {${token.name}}`);
|
|
}
|
|
}
|
|
|
|
// กฎพิเศษสำหรับแต่ละประเภท
|
|
if (correspondenceType === 'RFA') {
|
|
if (!tokens.some(t => t.name === 'PROJECT')) {
|
|
errors.push('RFA template ต้องมี {PROJECT}');
|
|
}
|
|
}
|
|
|
|
if (correspondenceType === 'TRANSMITTAL') {
|
|
if (!tokens.some(t => t.name === 'SUB_TYPE')) {
|
|
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
|
|
}
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
}
|
|
```
|
|
|
|
**API Endpoint**:
|
|
|
|
```typescript
|
|
// PUT /api/v1/document-numbering/configs/:configId
|
|
@Put('configs/:configId')
|
|
@Roles('PROJECT_ADMIN')
|
|
async updateTemplate(
|
|
@Param('configId') configId: number,
|
|
@Body() dto: UpdateTemplateDto
|
|
): Promise<DocumentNumberConfig> {
|
|
// Validate template
|
|
const validation = await this.templateValidator.validate(
|
|
dto.template,
|
|
dto.correspondenceType
|
|
);
|
|
|
|
if (!validation.valid) {
|
|
throw new BadRequestException(validation.errors);
|
|
}
|
|
|
|
// บันทึก template (ไม่ส่งผลต่อเอกสารที่สร้างแล้ว)
|
|
return await this.configService.update(configId, dto);
|
|
}
|
|
```
|
|
|
|
### 3.11.20.2 Template Versioning
|
|
|
|
**Database Table**: `document_number_config_history`
|
|
|
|
```sql
|
|
CREATE TABLE document_number_config_history (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
config_id INT NOT NULL,
|
|
template_before TEXT,
|
|
template_after TEXT NOT NULL,
|
|
changed_by INT NOT NULL,
|
|
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
change_reason TEXT,
|
|
|
|
FOREIGN KEY (config_id) REFERENCES document_number_configs(id),
|
|
FOREIGN KEY (changed_by) REFERENCES users(id)
|
|
) ENGINE=InnoDB COMMENT='Template Change History';
|
|
```
|
|
|
|
**Audit Trail Implementation**:
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class ConfigHistoryService {
|
|
async recordChange(
|
|
configId: number,
|
|
oldTemplate: string,
|
|
newTemplate: string,
|
|
userId: number,
|
|
reason: string
|
|
): Promise<void> {
|
|
await this.historyRepo.save({
|
|
configId,
|
|
templateBefore: oldTemplate,
|
|
templateAfter: newTemplate,
|
|
changedBy: userId,
|
|
changeReason: reason
|
|
});
|
|
}
|
|
|
|
async rollback(configId: number, historyId: number): Promise<void> {
|
|
const history = await this.historyRepo.findOne({ where: { id: historyId }});
|
|
await this.configService.update(configId, {
|
|
template: history.templateBefore
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.11.20.3 Counter Reset Policy
|
|
|
|
**Automatic Reset**:
|
|
|
|
- **Yearly Reset**: ทุกวันที่ 1 มกราคม (00:00:00 ICT)
|
|
- ใช้ **BullMQ Cron Job**:
|
|
|
|
```typescript
|
|
// src/document-numbering/jobs/counter-reset.job.ts
|
|
@Processor('document-numbering')
|
|
export class CounterResetJob {
|
|
@Cron('0 0 1 1 *') // 1 Jan every year
|
|
async handleYearlyReset() {
|
|
const newYear = new Date().getFullYear();
|
|
|
|
// ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว
|
|
// แค่เตรียม counter สำหรับปีใหม่
|
|
this.logger.log(`Year changed to ${newYear}, counters ready`);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Manual Reset** (Admin only):
|
|
|
|
```typescript
|
|
// POST /api/v1/document-numbering/configs/:configId/reset-counter
|
|
@Post('configs/:configId/reset-counter')
|
|
@Roles('SUPER_ADMIN')
|
|
@RequireApproval() // Custom decorator: ต้อง approve จาก 2 admins
|
|
async resetCounter(
|
|
@Param('configId') configId: number,
|
|
@Body() dto: ResetCounterDto
|
|
): Promise<void> {
|
|
// Validate reason
|
|
if (!dto.reason || dto.reason.length < 20) {
|
|
throw new BadRequestException('ต้องระบุเหตุผลอย่างน้อย 20 ตัวอักษร');
|
|
}
|
|
|
|
// Audit log
|
|
await this.auditService.logCounterReset({
|
|
configId,
|
|
userId: req.user.id,
|
|
reason: dto.reason,
|
|
previousValue: counter.lastNumber
|
|
});
|
|
|
|
// Reset
|
|
await this.counterService.reset(configId);
|
|
}
|
|
```
|
|
## 3.11.21 Audit Trail
|
|
|
|
### 3.11.21.1 การบันทึก Audit Log
|
|
|
|
**Database Table**: `document_number_audit`
|
|
|
|
```sql
|
|
CREATE TABLE document_number_audit (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
document_id INT NOT NULL,
|
|
generated_number VARCHAR(100) NOT NULL,
|
|
counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)',
|
|
template_used VARCHAR(200) NOT NULL,
|
|
user_id INT NOT NULL,
|
|
ip_address VARCHAR(45),
|
|
user_agent TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- Performance & Error Tracking
|
|
retry_count INT DEFAULT 0,
|
|
lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds',
|
|
total_duration_ms INT COMMENT 'Total generation time',
|
|
fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE',
|
|
|
|
INDEX idx_document_id (document_id),
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_created_at (created_at),
|
|
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail';
|
|
```
|
|
|
|
**Audit Service Implementation**:
|
|
|
|
```typescript
|
|
// src/document-numbering/services/audit.service.ts
|
|
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
@Injectable()
|
|
export class DocumentNumberAuditService {
|
|
async logGeneration(data: AuditLogData): Promise<void> {
|
|
await this.auditRepo.save({
|
|
documentId: data.documentId,
|
|
generatedNumber: data.number,
|
|
counterKey: JSON.stringify(data.counterKey),
|
|
templateUsed: data.template,
|
|
userId: data.userId,
|
|
ipAddress: data.ipAddress,
|
|
userAgent: data.userAgent,
|
|
retryCount: data.retryCount ?? 0,
|
|
lockWaitMs: data.lockWaitMs,
|
|
totalDurationMs: data.totalDurationMs,
|
|
fallbackUsed: data.fallbackUsed ?? 'NONE'
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Usage in Service**:
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class DocumentNumberingService {
|
|
async generateDocumentNumber(dto: GenerateNumberDto, req: Request) {
|
|
const startTime = Date.now();
|
|
let lockWaitMs = 0;
|
|
let retryCount = 0;
|
|
let fallbackUsed = 'NONE';
|
|
|
|
try {
|
|
// ... generate logic ...
|
|
const number = await this.doGenerate(dto);
|
|
|
|
// Audit log
|
|
await this.auditService.logGeneration({
|
|
documentId: dto.documentId,
|
|
number,
|
|
counterKey: dto.counterKey,
|
|
template: config.template,
|
|
userId: req.user.id,
|
|
ipAddress: req.ip,
|
|
userAgent: req.headers['user-agent'],
|
|
retryCount,
|
|
lockWaitMs,
|
|
totalDurationMs: Date.now() - startTime,
|
|
fallbackUsed
|
|
});
|
|
|
|
return number;
|
|
} catch (error) {
|
|
// Log error separately
|
|
await this.errorLogService.log(error, dto);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.11.21.2 Conflict & Error Logging
|
|
|
|
**Separate Error Log Table**: `document_number_errors`
|
|
|
|
```sql
|
|
CREATE TABLE document_number_errors (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
error_type ENUM(
|
|
'LOCK_TIMEOUT',
|
|
'VERSION_CONFLICT',
|
|
'DB_ERROR',
|
|
'REDIS_ERROR',
|
|
'VALIDATION_ERROR'
|
|
) NOT NULL,
|
|
error_message TEXT,
|
|
stack_trace TEXT,
|
|
context_data JSON COMMENT 'Request context (user, project, etc.)',
|
|
user_id INT,
|
|
ip_address VARCHAR(45),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
resolved_at TIMESTAMP NULL,
|
|
|
|
INDEX idx_error_type (error_type),
|
|
INDEX idx_created_at (created_at),
|
|
INDEX idx_user_id (user_id)
|
|
) ENGINE=InnoDB COMMENT='Document Numbering Error Log';
|
|
```
|
|
|
|
**Error Logging Service**:
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class ErrorLogService {
|
|
async log(error: Error, context: any): Promise<void> {
|
|
const errorType = this.classifyError(error);
|
|
|
|
await this.errorRepo.save({
|
|
errorType,
|
|
errorMessage: error.message,
|
|
stackTrace: error.stack,
|
|
contextData: JSON.stringify(context),
|
|
userId: context.userId,
|
|
ipAddress: context.ipAddress
|
|
});
|
|
|
|
// Alert if critical
|
|
if (this.isCritical(errorType)) {
|
|
await this.alertService.sendAlert({
|
|
severity: 'CRITICAL',
|
|
title: `Document Numbering Error: ${errorType}`,
|
|
details: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
private classifyError(error: Error): string {
|
|
if (error instanceof LockTimeoutError) return 'LOCK_TIMEOUT';
|
|
if (error instanceof OptimisticLockVersionMismatchError) return 'VERSION_CONFLICT';
|
|
if (error instanceof QueryFailedError) return 'DB_ERROR';
|
|
if (error instanceof RedisConnectionError) return 'REDIS_ERROR';
|
|
return 'UNKNOWN';
|
|
}
|
|
}
|
|
```
|
|
## 3.11.22 Performance Requirements
|
|
|
|
### 3.11.22.1 Response Time
|
|
|
|
**Target Response Times**:
|
|
- **95th percentile**: ≤ 2 วินาที
|
|
- **99th percentile**: ≤ 5 วินาที
|
|
- **Normal operation** (ไม่มี retry): ≤ 500ms
|
|
|
|
**Performance Optimization Strategies**:
|
|
|
|
```typescript
|
|
// 1. Database Connection Pooling
|
|
{
|
|
type: 'mysql',
|
|
extra: {
|
|
connectionLimit: 20, // Pool size
|
|
queueLimit: 0, // Unlimited queue
|
|
acquireTimeout: 10000 // 10s timeout
|
|
}
|
|
}
|
|
|
|
// 2. Redis Connection Pooling
|
|
import IORedis from 'ioredis';
|
|
|
|
const redis = new IORedis({
|
|
host: process.env.REDIS_HOST,
|
|
port: parseInt(process.env.REDIS_PORT),
|
|
maxRetriesPerRequest: 3,
|
|
enableReadyCheck: true,
|
|
lazyConnect: false,
|
|
// Connection pool
|
|
poolSize: 10
|
|
});
|
|
|
|
// 3. Query Optimization
|
|
// ใช้ Index-covered queries
|
|
const counter = await this.counterRepo
|
|
.createQueryBuilder('c')
|
|
.where('c.project_id = :projectId', { projectId })
|
|
.andWhere('c.correspondence_type_id = :typeId', { typeId })
|
|
.andWhere('c.current_year = :year', { year })
|
|
.useIndex('idx_counter_lookup') // Force index usage
|
|
.getOne();
|
|
```
|
|
|
|
**Performance Monitoring**:
|
|
|
|
```typescript
|
|
// Prometheus metrics
|
|
import { Counter, Histogram } from 'prom-client';
|
|
|
|
const generationDuration = new Histogram({
|
|
name: 'docnum_generation_duration_seconds',
|
|
help: 'Document number generation duration',
|
|
labelNames: ['project', 'type', 'status'],
|
|
buckets: [0.1, 0.5, 1, 2, 5, 10]
|
|
});
|
|
|
|
// Usage
|
|
const timer = generationDuration.startTimer();
|
|
try {
|
|
const number = await this.generate(dto);
|
|
timer({ status: 'success' });
|
|
} catch (error) {
|
|
timer({ status: 'error' });
|
|
throw error;
|
|
}
|
|
```
|
|
|
|
### 3.11.22.2 Throughput
|
|
|
|
**Capacity Requirements**:
|
|
|
|
- **Normal load**: ≥ 50 requests/second
|
|
- **Peak load**: ≥ 100 requests/second (ช่วงเร่งงาน)
|
|
- **Burst capacity**: ≥ 200 requests/second (short duration)
|
|
|
|
**Load Balancing Strategy**:
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
services:
|
|
backend:
|
|
image: lcbp3-backend:latest
|
|
deploy:
|
|
replicas: 3 # 3 instances
|
|
resources:
|
|
limits:
|
|
cpus: '1.0'
|
|
memory: 1G
|
|
reservations:
|
|
cpus: '0.5'
|
|
memory: 512M
|
|
|
|
nginx:
|
|
image: nginx:alpine
|
|
volumes:
|
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
|
ports:
|
|
- "80:80"
|
|
```
|
|
|
|
```nginx
|
|
# nginx.conf - Load Balancing Configuration
|
|
upstream backend {
|
|
least_conn; # Least connections algorithm
|
|
server backend:3000 max_fails=3 fail_timeout=30s;
|
|
server backend:3001 max_fails=3 fail_timeout=30s;
|
|
server backend:3002 max_fails=3 fail_timeout=30s;
|
|
}
|
|
|
|
server {
|
|
location /api/v1/documents/ {
|
|
proxy_pass http://backend;
|
|
proxy_next_upstream error timeout;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 30s;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rate Limiting**:
|
|
|
|
```typescript
|
|
// ใช้ @nestjs/throttler
|
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
|
|
|
@Controller('document-numbering')
|
|
@UseGuards(ThrottlerGuard)
|
|
export class DocumentNumberingController {
|
|
@Throttle(10, 60) // 10 requests per 60 seconds per user
|
|
@Post('generate')
|
|
async generate(@Body() dto: GenerateNumberDto) {
|
|
return await this.service.generate(dto);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.11.22.3 Availability
|
|
|
|
**SLA Targets**:
|
|
|
|
- **Uptime**: ≥ 99.5% (excluding planned maintenance)
|
|
- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน
|
|
- **Recovery Time Objective (RTO)**: ≤ 30 นาที
|
|
- **Recovery Point Objective (RPO)**: ≤ 5 นาที
|
|
|
|
**High Availability Setup**:
|
|
|
|
```yaml
|
|
# High Availability Architecture
|
|
services:
|
|
# MariaDB - Master/Replica
|
|
mariadb-master:
|
|
image: mariadb:11.8
|
|
environment:
|
|
MYSQL_REPLICATION_MODE: master
|
|
|
|
mariadb-replica:
|
|
image: mariadb:11.8
|
|
environment:
|
|
MYSQL_REPLICATION_MODE: slave
|
|
MYSQL_MASTER_HOST: mariadb-master
|
|
|
|
# Redis - Sentinel Mode
|
|
redis-master:
|
|
image: redis:7-alpine
|
|
command: redis-server --appendonly yes
|
|
|
|
redis-replica:
|
|
image: redis:7-alpine
|
|
command: redis-server --replicaof redis-master 6379
|
|
|
|
redis-sentinel:
|
|
image: redis:7-alpine
|
|
command: >
|
|
redis-sentinel /etc/redis/sentinel.conf
|
|
--sentinel monitor mymaster redis-master 6379 2
|
|
```
|
|
|
|
**Health Checks**:
|
|
|
|
```typescript
|
|
// src/health/health.controller.ts
|
|
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
|
|
|
|
@Controller('health')
|
|
export class HealthController {
|
|
@Get()
|
|
@HealthCheck()
|
|
check() {
|
|
return this.health.check([
|
|
() => this.db.pingCheck('database'),
|
|
() => this.redis.pingCheck('redis'),
|
|
() => this.customHealthCheck()
|
|
]);
|
|
}
|
|
|
|
private async customHealthCheck() {
|
|
// ทดสอบ generate document number
|
|
const canGenerate = await this.testGeneration();
|
|
return { documentNumbering: { status: canGenerate ? 'up' : 'down' }};
|
|
}
|
|
}
|
|
```
|
|
## 3.11.23 Monitoring & Alerting
|
|
|
|
### 3.11.23.1 Metrics Collection
|
|
|
|
**Prometheus Metrics Implementation**:
|
|
|
|
```typescript
|
|
// src/document-numbering/metrics/metrics.service.ts
|
|
import { Injectable } from '@nestjs/common';
|
|
import { Counter, Histogram, Gauge } from 'prom-client';
|
|
|
|
@Injectable()
|
|
export class DocumentNumberingMetrics {
|
|
// Lock acquisition metrics
|
|
private lockAcquisitionDuration = new Histogram({
|
|
name: 'docnum_lock_acquisition_duration_ms',
|
|
help: 'Lock acquisition time in milliseconds',
|
|
labelNames: ['project', 'type'],
|
|
buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000]
|
|
});
|
|
|
|
private lockAcquisitionFailures = new Counter({
|
|
name: 'docnum_lock_acquisition_failures_total',
|
|
help: 'Total number of lock acquisition failures',
|
|
labelNames: ['project', 'type', 'reason']
|
|
});
|
|
|
|
// Generation metrics
|
|
private generationDuration = new Histogram({
|
|
name: 'docnum_generation_duration_ms',
|
|
help: 'Total document number generation time',
|
|
labelNames: ['project', 'type', 'status'],
|
|
buckets: [100, 200, 500, 1000, 2000, 5000]
|
|
});
|
|
|
|
private retryCount = new Histogram({
|
|
name: 'docnum_retry_count',
|
|
help: 'Number of retries per generation',
|
|
labelNames: ['project', 'type'],
|
|
buckets: [0, 1, 2, 3, 5, 10]
|
|
});
|
|
|
|
// Connection health
|
|
private redisConnectionStatus = new Gauge({
|
|
name: 'docnum_redis_connection_status',
|
|
help: 'Redis connection status (1=up, 0=down)'
|
|
});
|
|
|
|
private dbConnectionPoolUsage = new Gauge({
|
|
name: 'docnum_db_connection_pool_usage',
|
|
help: 'Database connection pool usage percentage'
|
|
});
|
|
}
|
|
```
|
|
|
|
### 3.11.23.2 Alert Rules
|
|
|
|
**Prometheus Alert Rules** (`prometheus/alerts.yml`):
|
|
|
|
```yaml
|
|
groups:
|
|
- name: document_numbering_alerts
|
|
interval: 30s
|
|
rules:
|
|
# CRITICAL: Redis unavailable
|
|
- alert: RedisUnavailable
|
|
expr: docnum_redis_connection_status == 0
|
|
for: 1m
|
|
labels:
|
|
severity: critical
|
|
component: document-numbering
|
|
annotations:
|
|
summary: "Redis is unavailable for document numbering"
|
|
description: "System is falling back to DB-only locking"
|
|
|
|
# CRITICAL: High lock failure rate
|
|
- alert: HighLockFailureRate
|
|
expr: |
|
|
rate(docnum_lock_acquisition_failures_total[5m]) > 0.1
|
|
for: 5m
|
|
labels:
|
|
severity: critical
|
|
annotations:
|
|
summary: "Lock acquisition failure rate > 10%"
|
|
description: "Check Redis and database performance"
|
|
|
|
# WARNING: Elevated lock failure rate
|
|
- alert: ElevatedLockFailureRate
|
|
expr: |
|
|
rate(docnum_lock_acquisition_failures_total[5m]) > 0.05
|
|
for: 5m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: "Lock acquisition failure rate > 5%"
|
|
|
|
# WARNING: Slow lock acquisition
|
|
- alert: SlowLockAcquisition
|
|
expr: |
|
|
histogram_quantile(0.95,
|
|
rate(docnum_lock_acquisition_duration_ms_bucket[5m])
|
|
) > 1000
|
|
for: 5m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: "P95 lock acquisition time > 1 second"
|
|
|
|
# WARNING: High retry count
|
|
- alert: HighRetryCount
|
|
expr: |
|
|
sum by (project) (
|
|
rate(docnum_retry_count_sum[1h])
|
|
) > 100
|
|
for: 1h
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: "Retry count > 100 per hour in project {{ $labels.project }}"
|
|
|
|
# WARNING: Slow generation
|
|
- alert: SlowDocumentNumberGeneration
|
|
expr: |
|
|
histogram_quantile(0.95,
|
|
rate(docnum_generation_duration_ms_bucket[5m])
|
|
) > 2000
|
|
for: 5m
|
|
labels:
|
|
severity: warning
|
|
annotations:
|
|
summary: "P95 generation time > 2 seconds"
|
|
```
|
|
|
|
**AlertManager Configuration** (`alertmanager/config.yml`):
|
|
|
|
```yaml
|
|
route:
|
|
group_by: ['alertname', 'severity']
|
|
group_wait: 30s
|
|
group_interval: 5m
|
|
repeat_interval: 4h
|
|
receiver: 'ops-team'
|
|
routes:
|
|
# CRITICAL alerts → PagerDuty + Slack
|
|
- match:
|
|
severity: critical
|
|
receiver: 'pagerduty-critical'
|
|
continue: true
|
|
|
|
# WARNING alerts → Slack only
|
|
- match:
|
|
severity: warning
|
|
receiver: 'slack-warnings'
|
|
|
|
receivers:
|
|
- name: 'pagerduty-critical'
|
|
pagerduty_configs:
|
|
- service_key: <SERVICE_KEY>
|
|
description: '{{ .CommonAnnotations.summary }}'
|
|
|
|
- name: 'slack-warnings'
|
|
slack_configs:
|
|
- api_url: <SLACK_WEBHOOK>
|
|
channel: '#lcbp3-alerts'
|
|
title: '⚠️ {{ .GroupLabels.alertname }}'
|
|
text: '{{ .CommonAnnotations.description }}'
|
|
|
|
- name: 'ops-team'
|
|
email_configs:
|
|
- to: 'ops@example.com'
|
|
```
|
|
|
|
### 3.11.23.3 Grafana Dashboard
|
|
|
|
**Dashboard Configuration** (`grafana/dashboards/document-numbering.json`):
|
|
|
|
```json
|
|
{
|
|
"title": "Document Numbering Performance",
|
|
"panels": [
|
|
{
|
|
"title": "Lock Acquisition Success Rate",
|
|
"targets": [{
|
|
"expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))"
|
|
}],
|
|
"type": "graph",
|
|
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 }
|
|
},
|
|
{
|
|
"title": "Lock Acquisition Time (Percentiles)",
|
|
"targets": [
|
|
{
|
|
"expr": "histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
|
|
"legendFormat": "P50"
|
|
},
|
|
{
|
|
"expr": "histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
|
|
"legendFormat": "P95"
|
|
},
|
|
{
|
|
"expr": "histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
|
|
"legendFormat": "P99"
|
|
}
|
|
],
|
|
"type": "graph",
|
|
"gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 }
|
|
},
|
|
{
|
|
"title": "Generation Rate (per minute)",
|
|
"targets": [{
|
|
"expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60"
|
|
}],
|
|
"type": "stat",
|
|
"gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 }
|
|
},
|
|
{
|
|
"title": "Redis Connection Status",
|
|
"targets": [{
|
|
"expr": "docnum_redis_connection_status"
|
|
}],
|
|
"type": "stat",
|
|
"gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 },
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{ "value": 0, "color": "red" },
|
|
{ "value": 1, "color": "green" }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"title": "Error Rate by Type",
|
|
"targets": [{
|
|
"expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))"
|
|
}],
|
|
"type": "graph",
|
|
"gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Key Dashboard Panels**:
|
|
|
|
- **Lock Acquisition Success Rate**: Real-time success %
|
|
- **Lock Wait Time Percentiles**: P50, P95, P99 latency
|
|
- **Generation Rate**: Documents/minute
|
|
- **Error Breakdown**: By error type (LOCK_TIMEOUT, VERSION_CONFLICT, etc.)
|
|
- **Redis/DB Health**: Connection status
|
|
- **Retry Distribution**: Histogram of retry counts
|
|
|
|
## 3.11.24 API Reference
|
|
|
|
เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้ (รายละเอียดใน `specs/02-architecture/api-design.md`):
|
|
|
|
- `POST /api/v1/documents/{documentId}/generate-number` - สร้างเลขที่เอกสาร
|
|
- `GET /api/v1/document-numbering/configs` - ดูการตั้งค่า template
|
|
- `PUT /api/v1/document-numbering/configs/{configId}` - แก้ไข template (Admin only)
|
|
- `POST /api/v1/document-numbering/configs/{configId}/reset-counter` - Reset counter (Admin only)
|
|
|
|
## 3.11.25 Database Schema Reference
|
|
|
|
เอกสารนี้อ้างอิงถึง tables ต่อไปนี้ (รายละเอียดใน `specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md`):
|
|
|
|
- `document_number_configs` - เก็บ template และ counter configuration
|
|
- `document_number_counters` - เก็บ current counter value
|
|
- `document_number_audit` - เก็บ audit trail
|
|
- `documents` - เก็บ document number ที่ถูกสร้าง
|
|
|
|
## 3.11.26 Database Schema Requirements
|
|
|
|
### 3.11.26.1 Counter Table Schema
|
|
|
|
ตาราง `document_number_counters` ต้องมีโครงสร้างดังนี้:
|
|
|
|
```sql
|
|
CREATE TABLE document_number_counters (
|
|
project_id INT NOT NULL,
|
|
originator_organization_id INT NOT NULL,
|
|
recipient_organization_id INT NULL, -- NULL for RFA
|
|
correspondence_type_id INT NOT NULL,
|
|
sub_type_id INT DEFAULT 0, -- for TRANSMITTAL
|
|
rfa_type_id INT DEFAULT 0, -- for RFA
|
|
discipline_id INT DEFAULT 0, -- for RFA
|
|
current_year INT NOT NULL,
|
|
version INT DEFAULT 0 NOT NULL, -- Optimistic Lock
|
|
last_number INT DEFAULT 0,
|
|
|
|
PRIMARY KEY (
|
|
project_id,
|
|
originator_organization_id,
|
|
COALESCE(recipient_organization_id, 0),
|
|
correspondence_type_id,
|
|
sub_type_id,
|
|
rfa_type_id,
|
|
discipline_id,
|
|
current_year
|
|
),
|
|
|
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE
|
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
|
|
COMMENT = 'ตารางเก็บ Running Number Counters';
|
|
```
|
|
|
|
### 3.11.26.2 Index Requirements
|
|
|
|
```sql
|
|
-- Index สำหรับ Performance
|
|
CREATE INDEX idx_counter_lookup
|
|
ON document_number_counters (
|
|
project_id,
|
|
correspondence_type_id,
|
|
current_year
|
|
);
|
|
|
|
-- Index สำหรับ Originator lookup
|
|
CREATE INDEX idx_counter_org
|
|
ON document_number_counters (
|
|
originator_organization_id,
|
|
current_year
|
|
);
|
|
```
|
|
|
|
### 3.11.26.3 Important Notes
|
|
|
|
> **💡 Counter Key Design**
|
|
>
|
|
> - ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL
|
|
> - `version` column สำหรับ Optimistic Locking (ป้องกัน race condition)
|
|
> - `last_number` เริ่มจาก 0 และเพิ่มขึ้นทีละ 1
|
|
> - Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน)
|
|
|
|
> **⚠️ Migration Notes**
|
|
>
|
|
> - ไม่มีข้อมูลเก่า ไม่ต้องทำ backward compatibility
|
|
> - สามารถสร้าง table ใหม่ได้เลยตาม schema ข้างต้น
|
|
> - ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน
|
|
|
|
### 3.11.26.4 Example Counter Records
|
|
|
|
```sql
|
|
-- Example: LETTER from คคง. to สคฉ.3 in LCBP3-C2 year 2025
|
|
INSERT INTO document_number_counters (
|
|
project_id, originator_organization_id, recipient_organization_id,
|
|
correspondence_type_id, sub_type_id, rfa_type_id, discipline_id,
|
|
current_year, version, last_number
|
|
) VALUES (
|
|
2, -- LCBP3-C2
|
|
22, -- คคง.
|
|
10, -- สคฉ.3
|
|
6, -- LETTER
|
|
0, 0, 0,
|
|
2025, 0, 0
|
|
);
|
|
|
|
-- Example: RFA from ผรม.2 in LCBP3-C2, discipline TER, type RPT, year 2025
|
|
INSERT INTO document_number_counters (
|
|
project_id, originator_organization_id, recipient_organization_id,
|
|
correspondence_type_id, sub_type_id, rfa_type_id, discipline_id,
|
|
current_year, version, last_number
|
|
) VALUES (
|
|
2, -- LCBP3-C2
|
|
42, -- ผรม.2
|
|
NULL, -- RFA ไม่มี specific recipient
|
|
1, -- RFA
|
|
0,
|
|
18, -- RPT (Report)
|
|
5, -- TER (Terminal)
|
|
2025, 0, 0
|
|
);
|
|
```
|
|
|
|
## 3.11.27 Security Considerations
|
|
|
|
### 3.11.27.1 Authorization
|
|
|
|
- เฉพาะ authenticated users เท่านั้นที่สามารถ request document number
|
|
- เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้
|
|
- เฉพาะ Super Admin เท่านั้นที่ reset counter ได้
|
|
|
|
### 3.11.27.2 Rate Limiting
|
|
|
|
- Limit ต่อ user: **10 requests/minute** (prevent abuse)
|
|
- Limit ต่อ IP: **50 requests/minute**
|
|
|
|
### 3.11.27.3 Audit & Compliance
|
|
|
|
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
|
|
- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
|