# 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 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
