Files
lcbp3/specs/01-requirements/03.11-document-numbering.md
admin ec35521258
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled
251213:1509 Docunment Number Businee Rule not correct
2025-12-13 15:09:01 +07:00

62 KiB
Raw Blame History

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

📖 เอกสารที่เกี่ยวข้อง

3.11.1. วัตถุประสงค์

  • ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติและยืดหยุ่นสูง
  • ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร
  • ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์
  • ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย

3.11.2. Logic การนับเลข (Counter Logic)

การนับเลขจะแยกตาม Counter Key ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร

Counter Key Components

Component Required? Description Database Source Default if NULL
project_id Yes ID โครงการ Derived from user context or organization -
originator_organization_id Yes ID องค์กรผู้ส่ง correspondences.originator_id -
recipient_organization_id Depends on type ID องค์กรผู้รับหลัก (TO) correspondence_recipients where recipient_type = 'TO' NULL for RFA
correspondence_type_id Yes ID ประเภทเอกสาร correspondence_types.id -
sub_type_id TRANSMITTAL only ID ประเภทย่อย correspondence_sub_types.id 0
rfa_type_id RFA only ID ประเภท RFA rfa_types.id 0
discipline_id RFA only ID สาขางาน disciplines.id 0
current_year Yes ปี ค.ศ. System year (ปัจจุบัน) -

Counter Key แยกตามประเภทเอกสาร

LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER:

(project_id, originator_organization_id, recipient_organization_id,
 correspondence_type_id, 0, 0, 0, current_year)

หมายเหตุ: ไม่ใช้ discipline_id, sub_type_id, rfa_type_id

TRANSMITTAL:

(project_id, originator_organization_id, recipient_organization_id,
 correspondence_type_id, sub_type_id, 0, 0, current_year)

หมายเหตุ: ใช้ sub_type_id เพิ่มเติม

RFA:

(project_id, originator_organization_id, NULL,
 correspondence_type_id, 0, rfa_type_id, discipline_id, current_year)

หมายเหตุ: RFA ไม่ใช้ recipient_organization_id เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER)

วิธีการหา project_id

เนื่องจาก Template ของ LETTER/TRANSMITTAL ไม่มี {PROJECT} token ระบบจะหา project_id จาก:

  1. User Context (แนะนำ):

    • เมื่อ User สร้างเอกสาร UI จะให้เลือก Project/Contract ก่อน
    • ใช้ project_id จาก Context ที่เลือก
  2. จาก Organization:

    • Query project_organizations หรือ contract_organizations
    • ใช้ originator_organization_id หา project ที่เกี่ยวข้อง
    • ถ้ามีหลาย project ให้ User เลือก
  3. Validation:

    • ตรวจสอบว่า organization มีสิทธิ์ใน project นั้น
    • ตรวจสอบว่า project/contract เป็น active

Fallback สำหรับค่า NULL

  • discipline_id: ใช้ 0 (ไม่ระบุสาขางาน)
  • sub_type_id: ใช้ 0 (ไม่มีประเภทย่อย)
  • rfa_type_id: ใช้ 0 (ไม่ระบุประเภท RFA)
  • recipient_organization_id: ใช้ NULL สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL

3.11.3. Format Templates by Correspondence Type

📝 หมายเหตุสำคัญ

  • Templates ด้านล่างเป็น ตัวอย่าง สำหรับประเภทเอกสารหลัก
  • ระบบรองรับ ทุกประเภทเอกสาร ที่อยู่ใน correspondence_types table
  • หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ
  • Admin สามารถกำหนด Template เฉพาะสำหรับแต่ละประเภทผ่าน Admin Panel

3.11.3.1. Letter (TYPE = LETTER)

Template:

{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}

Example: คคง.-สคฉ.3-0001-2568

Token Breakdown:

  • คคง. = {ORIGINATOR} = รหัสองค์กรผู้ส่ง
  • สคฉ.3 = {RECIPIENT} = รหัสองค์กรผู้รับหลัก (TO)
  • 0001 = {SEQ:4} = Running number (เริ่ม 0001, 0002, ...)
  • 2568 = {YEAR:B.E.} = ปี พ.ศ.

⚠️ Template vs Counter Separation

  • {CORR_TYPE} ไม่แสดงใน template เพื่อความกระชับ
  • แต่ระบบยังใช้ correspondence_type_id ใน Counter Key เพื่อแยก counter
  • LETTER, MEMO, RFI มี counter แยกกัน แม้ template format เหมือนกัน

Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)


3.11.3.2. Transmittal (TYPE = TRANSMITTAL)

Template:

{ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}

Example: คคง.-สคฉ.3-21-0117-2568

Token Breakdown:

  • คคง. = {ORIGINATOR}
  • สคฉ.3 = {RECIPIENT}
  • 21 = {SUB_TYPE} = หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 14=MET, ...)
  • 0117 = {SEQ:4}
  • 2568 = {YEAR:B.E.}

⚠️ Template vs Counter Separation

  • {CORR_TYPE} ไม่แสดงใน template (เหมือน LETTER)
  • TRANSMITTAL มี counter แยกจาก LETTER

Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)


3.11.3.3. RFA (Request for Approval)

Template:

{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV}

Example: LCBP3-C2-RFA-TER-RPT-0001-A

Token Breakdown:

  • LCBP3-C2 = {PROJECT} = รหัสโครงการ
  • RFA = {CORR_TYPE} = ประเภทเอกสาร (แสดงใน RFA template)
  • TER = {DISCIPLINE} = รหัสสาขางาน (TER=Terminal, STR=Structure, ...)
  • RPT = {RFA_TYPE} = ประเภท RFA (RPT=Report, SDW=Shop Drawing, ...)
  • 0001 = {SEQ:4}
  • A = {REV} = Revision code

📋 RFA Workflow

  • RFA เป็น เอกสารโครงการ (Project-level document)
  • Workflow: CONTRACTOR → CONSULTANT → OWNER
  • ไม่มี specific recipient_id เพราะเป็น workflow ที่กำหนดไว้แล้ว

Counter Key: (project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)


3.11.3.4. Drawing

Status: 🚧 To Be Determined

Drawing Numbering ยังไม่ได้กำหนด Template เนื่องจาก:

  • มีความซับซ้อนสูง (Contract Drawing และ Shop Drawing มีกฎต่างกัน)
  • อาจต้องใช้ระบบ Numbering แยกต่างหาก
  • ต้องพิจารณาร่วมกับ RFA ที่เกี่ยวข้อง

3.11.3.5. Other Correspondence Types

Applicable to: RFI, MEMO, EMAIL, MOM, INSTRUCTION, NOTICE, OTHER

Template:

{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}

Example (RFI): คคง.-สคฉ.3-0042-2568 Example (MEMO): คคง.-ผรม.1-0001-2568

🔑 Counter Separation

  • แม้ template format เหมือนกับ LETTER
  • แต่แต่ละ type มี counter แยกกัน ผ่าน correspondence_type_id
  • RFI counter ≠ MEMO counter ≠ LETTER counter

Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)

หมายเหตุ: ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้ ถ้ามีการเพิ่ม correspondence type ใหม่ใน correspondence_types table จะใช้ Template นี้โดยอัตโนมัติ

3.11.4. Supported Token Types

Token Description Example Database Source
{PROJECT} รหัสโครงการ LCBP3, LCBP3-C2 projects.project_code
{ORIGINATOR} รหัสองค์กรผู้ส่ง คคง., ผรม.1 organizations.organization_code via correspondences.originator_id
{RECIPIENT} รหัสองค์กรผู้รับหลัก (TO) สคฉ.3, กทท. organizations.organization_code via correspondence_recipients where recipient_type = 'TO'
{CORR_TYPE} รหัสประเภทเอกสาร RFA, TRANSMITTAL, LETTER correspondence_types.type_code
{SUB_TYPE} หมายเลขประเภทย่อย 11, 12, 21 correspondence_sub_types.sub_type_number
{RFA_TYPE} รหัสประเภท RFA SDW, RPT, MAT rfa_types.type_code
{DISCIPLINE} รหัสสาขาวิชา STR, TER, GEO disciplines.discipline_code
{SEQ:n} Running number (n = จำนวนหลัก) 0001, 0029, 0985 Based on document_number_counters.last_number + 1
{YEAR:B.E.} ปี พ.ศ. 2568 document_number_counters.current_year + 543
{YEAR:A.D.} ปี ค.ศ. 2025 document_number_counters.current_year
{REV} Revision Code A, B, AA correspondence_revisions.revision_label

Token Usage Notes

{SEQ:n}:

  • n = จำนวนหลักที่ต้องการ (typically 4)
  • Counter เริ่มจาก 0001 และเพิ่มทีละ 1 (0001, 0002, 0003, ...)
  • Padding ด้วย 0 ทางซ้าย
  • Reset ทุกปี (ตาม current_year ใน Counter Key)

{RECIPIENT}:

  • ใช้เฉพาะผู้รับที่มี recipient_type = 'TO' เท่านั้น
  • ถ้ามีหลาย TO ให้ใช้คนแรก (ตาม sort order)
  • ไม่ใช้สำหรับ RFA (RFA ไม่มี {RECIPIENT} ใน template)

{CORR_TYPE}:

  • รองรับทุกค่าจาก correspondence_types.type_code
  • ถ้าม<EFBFBD>การเพิ่มประเภทใหม่ จะใช้งานได้ทันที
  • แสดงใน 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

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

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

3.11.8. Audit Trail Requirements

3.11.8.1. Audit Logging

Requirements:

ระบบต้องบันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation:

  • document_id - เอกสารที่ถูกสร้าง
  • generated_number - เลขที่ถูกสร้าง
  • counter_key - key ที่ใช้ในการนับ (JSON format)
  • template_used - template ที่ใช้
  • user_id - ผู้ที่ request
  • ip_address - IP address ของผู้ request
  • timestamp - เวลาที่สร้าง
  • retry_count - จำนวนครั้งที่ retry
  • performance_metrics - Lock wait time, total duration

3.11.8.2. Error Logging

Requirements:

  • ระบบต้องบันทึก error แยกต่างหาก พร้อม error type classification
  • ระบบควรalert ops team สำหรับ critical errors

3.11.8.3. Retention Policy

Requirements:

  • Audit log ต้องเก็บอย่างน้อย 7 ปี (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)

3.11.9. Performance Requirements

3.11.9.1. Response Time

SLA Targets:

Metric Target Notes
95th percentile ≤ 2 วินาที ตั้งแต่ request ถึง response
99th percentile ≤ 5 วินาที รวม retry attempts
Normal operation ≤ 500ms ไม่มี retry

3.11.9.2. Throughput

Capacity Targets:

Load Level Target Notes
Normal load ≥ 50 req/s ใช้งานปกติ
Peak load ≥ 100 req/s ช่วงเร่งงาน

3.11.9.3. Availability

SLA Targets:

  • Uptime: ≥ 99.5% (excluding planned maintenance)
  • Maximum downtime: ≤ 3.6 ชั่วโมง/เดือน
  • RTO: ≤ 30 นาที
  • RPO: ≤ 5 นาที

Operations Details: ดู Operations Guide - Section 1

3.11.10. Monitoring & Alerting Requirements

3.11.10.1. Metrics

Requirements:

ระบบต้องcollect metrics ต่อไปนี้:

  • Lock acquisition time (p50, p95, p99)
  • Lock acquisition success/failure rate
  • Counter generation latency
  • Retry count distribution
  • Redis connection status
  • Database connection pool usage

3.11.10.2. Alerts

Requirements:

ระบบต้องalert สำหรับ conditions ต่อไปนี้:

Severity Condition Action
🔴 Critical Redis unavailable > 1 minute PagerDuty + Slack
🔴 Critical Lock failures > 10% in 5 min PagerDuty + Slack
🟡 Warning Lock failures > 5% in 5 min Slack
🟡 Warning Avg lock wait time > 1 sec Slack
🟡 Warning Retry count > 100/hour Slack

3.11.10.3. Dashboard

Requirements:

  • Ops team ต้องมี real-time dashboard แสดง:
    • Lock acquisition success rate
    • Lock wait time percentiles
    • Generation rate (per minute)
    • Error rate by type
    • Connection health status

Operations Details: ดู Operations Guide - Section 3

3.11.11. API Reference

เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้:

Document Number Generation

POST /api/v1/documents/{documentId}/generate-number

สร้างเลขที่เอกสารสำหรับ document ที่ระบุ

Request Body:

{
  "counterKey": {
    "projectId": 2,
    "originatorOrgId": 22,
    "recipientOrgId": 10,
    "correspondenceTypeId": 6,
    "subTypeId": 0,
    "rfaTypeId": 0,
    "disciplineId": 0,
    "year": 2025
  }
}

Response:

{
  "documentNumber": "คคง.-สคฉ.3-0001-2568",
  "generatedAt": "2025-12-02T15:30:00Z"
}

Template Management

GET /api/v1/document-numbering/configs

ดูรายการ template configuration ทั้งหมด

PUT /api/v1/document-numbering/configs/{configId}

แก้ไข template (Project Admin only)

POST /api/v1/document-numbering/configs/{configId}/reset-counter

Reset counter (Super Admin only, requires approval)

รายละเอียดเพิ่มเติม: ดู API Design

3.11.12. Database Schema Reference

เอกสารนี้อ้างอิงถึง database tables ต่อไปนี้:

Core Tables

  • document_number_counters - เก็บ counter values และ template configuration
  • document_number_audit - เก็บ audit trail ของการ generate เลขที่
  • document_number_errors - เก็บ error logs
  • 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

3.11.13. Database Schema Requirements

3.11.13.1. Counter Table Schema Requirements

Primary Table: document_number_counters

Required Columns:

  • Composite primary key: (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)
  • version - สำหรับ optimistic locking
  • last_number - counter value (เริ่มจาก 0)

Important Notes:

  • ใช้ COALESCE(recipient_organization_id, 0) ใน Primary Key เพื่อรองรับ NULL
  • Counter reset ทุกปี (เมื่อ current_year เปลี่ยน)
  • ต้องมี seed data สำหรับ correspondence_types, rfa_types, disciplines ก่อน

Schema Details: ดู Implementation Guide - Section 1.1

3.11.13.2. Audit Table Requirements

Primary Table: document_number_audit

Required Columns:

  • document_id, generated_number, counter_key (JSON)
  • template_used, user_id, ip_address
  • Performance metrics: retry_count, lock_wait_ms, total_duration_ms
  • fallback_used - tracking fallback scenarios

Retention: ≥ 7 ปี

3.11.13.3. Error Log Requirements

Primary Table: document_number_errors

Required Columns:

  • error_type - ENUM classification
  • error_message, stack_trace, context_data (JSON)
  • user_id, ip_address, created_at, resolved_at

3.11.14. Security Considerations

3.11.14.1. Authorization

  • เฉพาะ authenticated users เท่านั้นที่สามารถ request document number
  • เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้
  • เฉพาะ Super Admin เท่านั้นที่ reset counter ได้

3.11.14.2. Rate Limiting

Requirements:

  • Limit ต่อ user: 10 requests/minute (prevent abuse)
  • Limit ต่อ IP: 50 requests/minute

Implementation Details: ดู Implementation Guide - Section 5

3.11.14.3. Audit & Compliance

Requirements:

  • บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
  • เก็บ audit log อย่างน้อย 7 ปี (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
  • Audit log ต้องไม่สามารถแก้ไขได้ (immutable)

References

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):

// 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.5.2. Optimistic Locking

ใช้ TypeORM Optimistic Lock ร่วมกับ @Version() decorator:

Entity Definition:

// 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:

// ใช้ TypeORM Transaction + Optimistic Lock
await this.connection.transaction(async (manager) => {
  const counter = await manager.findOne(DocumentNumberCounter, {
    where: counterKey
  });

  counter.lastNumber += 1;
  await manager.save(counter); // auto-check version
});

หาก version conflict → TypeORM throw OptimisticLockVersionMismatchError → retry

3.11.5.3. Database Constraints

Unique Constraints:

-- บน documents table
ALTER TABLE documents
ADD CONSTRAINT uq_document_number UNIQUE (document_number);

Foreign Key Constraints:

  • project_idprojects(id) ON DELETE CASCADE
  • originator_organization_idorganizations(id) ON DELETE CASCADE
  • recipient_organization_idorganizations(id) ON DELETE CASCADE
  • correspondence_type_idcorrespondence_types(id) ON DELETE CASCADE

Check Constraints:

-- ตรวจสอบว่า last_number ≥ 0
ALTER TABLE document_number_counters
ADD CONSTRAINT chk_last_number_positive CHECK (last_number >= 0);

-- ตรวจสอบว่า current_year เป็นปี ค.ศ. ที่สมเหตุสมผล
ALTER TABLE document_number_counters
ADD CONSTRAINT chk_current_year_valid
CHECK (current_year BETWEEN 2020 AND 2100);

3.11.6. Retry Mechanism & Error Handling

3.11.6.1. Scenario 1: Redis Unavailable

Fallback Strategy: Database-only Pessimistic Locking

Implementation:

// 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.6.2. Scenario 2: Lock Acquisition Timeout

Retry Strategy: Exponential Backoff with Jitter

// ใช้ @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:

    {
      "statusCode": 503,
      "message": "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง",
      "error": "Service Unavailable",
      "retryAfter": 30
    }
    

3.11.6.3. Scenario 3: Version Conflict (Optimistic Lock)

Retry Strategy: Immediate Retry (2 attempts)

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.6.4. Scenario 4: Database Connection Error

Retry Strategy: Exponential Backoff (3 attempts)

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:

    {
      "statusCode": 500,
      "message": "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ",
      "error": "Internal Server Error",
      "ref": "ERR-20250102-1234-ABCD"
    }
    
  • Alerting: ส่ง PagerDuty/Slack alert ทันที (severity: CRITICAL)

3.11.7. Configuration Management

3.11.7.1. Admin Panel Configuration

Features:

  • Project Admin สามารถกำหนด/แก้ไข template ผ่าน Web UI
  • Preview document number ก่อนบันทึก
  • Template validation แบบ real-time

Template Validation Logic:

// 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:

// 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.7.2. Template Versioning

Database Table: document_number_config_history

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:

@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.7.3. Counter Reset Policy

Automatic Reset:

  • Yearly Reset: ทุกวันที่ 1 มกราคม (00:00:00 ICT)

    • ใช้ BullMQ Cron Job:
    // 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):

// 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.8. Audit Trail

### 3.11.8.1. การบันทึก Audit Log

**Database Table**: `document_number_audit`

```sql
CREATE TABLE document_number_audit (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  document_id INT NOT NULL,
  generated_number VARCHAR(100) NOT NULL,
  counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)',
  template_used VARCHAR(200) NOT NULL,
  user_id INT NOT NULL,
  ip_address VARCHAR(45),
  user_agent TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  -- Performance & Error Tracking
  retry_count INT DEFAULT 0,
  lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds',
  total_duration_ms INT COMMENT 'Total generation time',
  fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE',

  INDEX idx_document_id (document_id),
  INDEX idx_user_id (user_id),
  INDEX idx_created_at (created_at),
  FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail';

Audit Service Implementation:

// 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:

@Injectable()
export class DocumentNumberingService {
  async generateDocumentNumber(dto: GenerateNumberDto, req: Request) {
    const startTime = Date.now();
    let lockWaitMs = 0;
    let retryCount = 0;
    let fallbackUsed = 'NONE';

    try {
      // ... generate logic ...
      const number = await this.doGenerate(dto);

      // Audit log
      await this.auditService.logGeneration({
        documentId: dto.documentId,
        number,
        counterKey: dto.counterKey,
        template: config.template,
        userId: req.user.id,
        ipAddress: req.ip,
        userAgent: req.headers['user-agent'],
        retryCount,
        lockWaitMs,
        totalDurationMs: Date.now() - startTime,
        fallbackUsed
      });

      return number;
    } catch (error) {
      // Log error separately
      await this.errorLogService.log(error, dto);
      throw error;
    }
  }
}

3.11.8.2. Conflict & Error Logging

Separate Error Log Table: document_number_errors

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:

@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.9. Performance Requirements

### 3.11.9.1. Response Time

**Target Response Times**:
- **95th percentile**:  2 วินาที
- **99th percentile**:  5 วินาที
- **Normal operation** (ไม่มี retry):  500ms

**Performance Optimization Strategies**:

```typescript
// 1. Database Connection Pooling
{
  type: 'mysql',
  extra: {
    connectionLimit: 20,      // Pool size
    queueLimit: 0,            // Unlimited queue
    acquireTimeout: 10000     // 10s timeout
  }
}

// 2. Redis Connection Pooling
import IORedis from 'ioredis';

const redis = new IORedis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT),
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
  lazyConnect: false,
  // Connection pool
  poolSize: 10
});

// 3. Query Optimization
// ใช้ Index-covered queries
const counter = await this.counterRepo
  .createQueryBuilder('c')
  .where('c.project_id = :projectId', { projectId })
  .andWhere('c.correspondence_type_id = :typeId', { typeId })
  .andWhere('c.current_year = :year', { year })
  .useIndex('idx_counter_lookup')  // Force index usage
  .getOne();

Performance Monitoring:

// Prometheus metrics
import { Counter, Histogram } from 'prom-client';

const generationDuration = new Histogram({
  name: 'docnum_generation_duration_seconds',
  help: 'Document number generation duration',
  labelNames: ['project', 'type', 'status'],
  buckets: [0.1, 0.5, 1, 2, 5, 10]
});

// Usage
const timer = generationDuration.startTimer();
try {
  const number = await this.generate(dto);
  timer({ status: 'success' });
} catch (error) {
  timer({ status: 'error' });
  throw error;
}

3.11.9.2. Throughput

Capacity Requirements:

  • Normal load: ≥ 50 requests/second
  • Peak load: ≥ 100 requests/second (ช่วงเร่งงาน)
  • Burst capacity: ≥ 200 requests/second (short duration)

Load Balancing Strategy:

# 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.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:

// ใช้ @nestjs/throttler
import { ThrottlerGuard } from '@nestjs/throttler';

@Controller('document-numbering')
@UseGuards(ThrottlerGuard)
export class DocumentNumberingController {
  @Throttle(10, 60)  // 10 requests per 60 seconds per user
  @Post('generate')
  async generate(@Body() dto: GenerateNumberDto) {
    return await this.service.generate(dto);
  }
}

3.11.9.3. Availability

SLA Targets:

  • Uptime: ≥ 99.5% (excluding planned maintenance)
  • Maximum downtime: ≤ 3.6 ชั่วโมง/เดือน
  • Recovery Time Objective (RTO): ≤ 30 นาที
  • Recovery Point Objective (RPO): ≤ 5 นาที

High Availability Setup:

# 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:

// src/health/health.controller.ts
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
      () => this.redis.pingCheck('redis'),
      () => this.customHealthCheck()
    ]);
  }

  private async customHealthCheck() {
    // ทดสอบ generate document number
    const canGenerate = await this.testGeneration();
    return { documentNumbering: { status: canGenerate ? 'up' : 'down' }};
  }
}

## 3.11.10. Monitoring & Alerting

### 3.11.10.1. Metrics Collection

**Prometheus Metrics Implementation**:

```typescript
// src/document-numbering/metrics/metrics.service.ts
import { Injectable } from '@nestjs/common';
import { Counter, Histogram, Gauge } from 'prom-client';

@Injectable()
export class DocumentNumberingMetrics {
  // Lock acquisition metrics
  private lockAcquisitionDuration = new Histogram({
    name: 'docnum_lock_acquisition_duration_ms',
    help: 'Lock acquisition time in milliseconds',
    labelNames: ['project', 'type'],
    buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000]
  });

  private lockAcquisitionFailures = new Counter({
    name: 'docnum_lock_acquisition_failures_total',
    help: 'Total number of lock acquisition failures',
    labelNames: ['project', 'type', 'reason']
  });

  // Generation metrics
  private generationDuration = new Histogram({
    name: 'docnum_generation_duration_ms',
    help: 'Total document number generation time',
    labelNames: ['project', 'type', 'status'],
    buckets: [100, 200, 500, 1000, 2000, 5000]
  });

  private retryCount = new Histogram({
    name: 'docnum_retry_count',
    help: 'Number of retries per generation',
    labelNames: ['project', 'type'],
    buckets: [0, 1, 2, 3, 5, 10]
  });

  // Connection health
  private redisConnectionStatus = new Gauge({
    name: 'docnum_redis_connection_status',
    help: 'Redis connection status (1=up, 0=down)'
  });

  private dbConnectionPoolUsage = new Gauge({
    name: 'docnum_db_connection_pool_usage',
    help: 'Database connection pool usage percentage'
  });
}

3.11.10.2. Alert Rules

Prometheus Alert Rules (prometheus/alerts.yml):

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):

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.10.3. Grafana Dashboard

Dashboard Configuration (grafana/dashboards/document-numbering.json):

{
  "title": "Document Numbering Performance",
  "panels": [
    {
      "title": "Lock Acquisition Success Rate",
      "targets": [{
        "expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))"
      }],
      "type": "graph",
      "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 }
    },
    {
      "title": "Lock Acquisition Time (Percentiles)",
      "targets": [
        {
          "expr": "histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
          "legendFormat": "P50"
        },
        {
          "expr": "histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
          "legendFormat": "P95"
        },
        {
          "expr": "histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
          "legendFormat": "P99"
        }
      ],
      "type": "graph",
      "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 }
    },
    {
      "title": "Generation Rate (per minute)",
      "targets": [{
        "expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60"
      }],
      "type": "stat",
      "gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 }
    },
    {
      "title": "Redis Connection Status",
      "targets": [{
        "expr": "docnum_redis_connection_status"
      }],
      "type": "stat",
      "gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 },
      "thresholds": {
        "mode": "absolute",
        "steps": [
          { "value": 0, "color": "red" },
          { "value": 1, "color": "green" }
        ]
      }
    },
    {
      "title": "Error Rate by Type",
      "targets": [{
        "expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))"
      }],
      "type": "graph",
      "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 }
    }
  ]
}

Key Dashboard Panels:

  • Lock Acquisition Success Rate: Real-time success %
  • Lock Wait Time Percentiles: P50, P95, P99 latency
  • Generation Rate: Documents/minute
  • Error Breakdown: By error type (LOCK_TIMEOUT, VERSION_CONFLICT, etc.)
  • Redis/DB Health: Connection status
  • Retry Distribution: Histogram of retry counts

3.11.12. API Reference

เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้ (รายละเอียดใน specs/02-architecture/api-design.md):

  • POST /api/v1/documents/{documentId}/generate-number - สร้างเลขที่เอกสาร
  • GET /api/v1/document-numbering/configs - ดูการตั้งค่า template
  • PUT /api/v1/document-numbering/configs/{configId} - แก้ไข template (Admin only)
  • POST /api/v1/document-numbering/configs/{configId}/reset-counter - Reset counter (Admin only)

3.11.13. Database Schema Reference

เอกสารนี้อ้างอิงถึง tables ต่อไปนี้ (รายละเอียดใน specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md):

  • document_number_configs - เก็บ template และ counter configuration
  • document_number_counters - เก็บ current counter value
  • document_number_audit - เก็บ audit trail
  • documents - เก็บ document number ที่ถูกสร้าง

3.11.14. Database Schema Requirements

3.11.14.1. Counter Table Schema

ตาราง document_number_counters ต้องมีโครงสร้างดังนี้:

CREATE TABLE document_number_counters (
  project_id INT NOT NULL,
  originator_organization_id INT NOT NULL,
  recipient_organization_id INT NULL,  -- NULL for RFA
  correspondence_type_id INT NOT NULL,
  sub_type_id INT DEFAULT 0,           -- for TRANSMITTAL
  rfa_type_id INT DEFAULT 0,           -- for RFA
  discipline_id INT DEFAULT 0,         -- for RFA
  current_year INT NOT NULL,
  version INT DEFAULT 0 NOT NULL,      -- Optimistic Lock
  last_number INT DEFAULT 0,

  PRIMARY KEY (
    project_id,
    originator_organization_id,
    COALESCE(recipient_organization_id, 0),
    correspondence_type_id,
    sub_type_id,
    rfa_type_id,
    discipline_id,
    current_year
  ),

  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
  FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
  FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
  FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
  COMMENT = 'ตารางเก็บ Running Number Counters';

3.11.14.2. Index Requirements

-- Index สำหรับ Performance
CREATE INDEX idx_counter_lookup
ON document_number_counters (
  project_id,
  correspondence_type_id,
  current_year
);

-- Index สำหรับ Originator lookup
CREATE INDEX idx_counter_org
ON document_number_counters (
  originator_organization_id,
  current_year
);

3.11.14.3. Important Notes

💡 Counter Key Design

  • ใช้ COALESCE(recipient_organization_id, 0) ใน Primary Key เพื่อรองรับ NULL
  • version column สำหรับ Optimistic Locking (ป้องกัน race condition)
  • last_number เริ่มจาก 0 และเพิ่มขึ้นทีละ 1
  • Counter reset ทุกปี (เมื่อ current_year เปลี่ยน)

⚠️ Migration Notes

  • ไม่มีข้อมูลเก่า ไม่ต้องทำ backward compatibility
  • สามารถสร้าง table ใหม่ได้เลยตาม schema ข้างต้น
  • ต้องมี seed data สำหรับ correspondence_types, rfa_types, disciplines ก่อน

3.11.14.4. Example Counter Records

-- Example: LETTER from คคง. to สคฉ.3 in LCBP3-C2 year 2025
INSERT INTO document_number_counters (
  project_id, originator_organization_id, recipient_organization_id,
  correspondence_type_id, sub_type_id, rfa_type_id, discipline_id,
  current_year, version, last_number
) VALUES (
  2,      -- LCBP3-C2
  22,     -- คคง.
  10,     -- สคฉ.3
  6,      -- LETTER
  0, 0, 0,
  2025, 0, 0
);

-- Example: RFA from ผรม.2 in LCBP3-C2, discipline TER, type RPT, year 2025
INSERT INTO document_number_counters (
  project_id, originator_organization_id, recipient_organization_id,
  correspondence_type_id, sub_type_id, rfa_type_id, discipline_id,
  current_year, version, last_number
) VALUES (
  2,      -- LCBP3-C2
  42,     -- ผรม.2
  NULL,   -- RFA ไม่มี specific recipient
  1,      -- RFA
  0,
  18,     -- RPT (Report)
  5,      -- TER (Terminal)
  2025, 0, 0
);

3.11.15. Security Considerations

3.11.14.1. Authorization

  • เฉพาะ authenticated users เท่านั้นที่สามารถ request document number
  • เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้
  • เฉพาะ Super Admin เท่านั้นที่ reset counter ได้

3.11.14.2. Rate Limiting

  • Limit ต่อ user: 10 requests/minute (prevent abuse)
  • Limit ต่อ IP: 50 requests/minute

3.11.14.3. Audit & Compliance

  • บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
  • เก็บ audit log อย่างน้อย 7 ปี (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)