251217:1704 Docunment Number: Update to 1.6.2
This commit is contained in:
1
.aignore
1
.aignore
@@ -5,5 +5,6 @@ backend/dist/
|
|||||||
frontend/dist/
|
frontend/dist/
|
||||||
backend/build/
|
backend/build/
|
||||||
frontend/build/
|
frontend/build/
|
||||||
|
docs/backup/
|
||||||
.git/
|
.git/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
backend/node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
*.min.js
|
*.min.js
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ export class DocumentNumberingService implements OnModuleInit {
|
|||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
const host = this.configService.get<string>('REDIS_HOST', 'localhost');
|
const host = this.configService.get<string>('REDIS_HOST', 'localhost');
|
||||||
const port = this.configService.get<number>('REDIS_PORT', 6379);
|
const port = this.configService.get<number>('REDIS_PORT', 6379);
|
||||||
|
const password = this.configService.get<string>('REDIS_PASSWORD');
|
||||||
|
|
||||||
this.redisClient = new Redis({
|
this.redisClient = new Redis({
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
password,
|
||||||
retryStrategy: (times) => Math.min(times * 50, 2000),
|
retryStrategy: (times) => Math.min(times * 50, 2000),
|
||||||
maxRetriesPerRequest: 3,
|
maxRetriesPerRequest: 3,
|
||||||
});
|
});
|
||||||
|
|||||||
682
docs/backup/03.11-document-numbering-add.md
Normal file
682
docs/backup/03.11-document-numbering-add.md
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
# Document Numbering Requirements
|
||||||
|
|
||||||
|
**Version**: 1.6.1
|
||||||
|
**Last Updated**: 2025-01-16
|
||||||
|
**Status**: draft
|
||||||
|
**Related ADRs**: ADR-018-document-numbering-strategy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
### 1.1 Purpose
|
||||||
|
ระบบ Document Numbering สำหรับสร้างเลขที่เอกสารอัตโนมัติที่มีความเป็นเอกลักษณ์ (unique) และสามารถติดตามได้ (traceable) สำหรับเอกสารทุกประเภทในระบบ LCBP3-DMS
|
||||||
|
|
||||||
|
### 1.2 Scope
|
||||||
|
- Auto-generation ของเลขที่เอกสารตามรูปแบบที่กำหนด
|
||||||
|
- Manual override สำหรับการ import เอกสารเก่า
|
||||||
|
- Cancelled number handling (ไม่ reuse)
|
||||||
|
- Void & Replace pattern สำหรับการแทนที่เอกสาร
|
||||||
|
- Distributed locking เพื่อป้องกัน race condition
|
||||||
|
- Complete audit trail สำหรับทุก operation
|
||||||
|
|
||||||
|
### 1.3 Document Types Supported
|
||||||
|
- Correspondences (COR)
|
||||||
|
- Request for Approvals (RFA)
|
||||||
|
- Contract Drawings (CD)
|
||||||
|
- Shop Drawings (SD)
|
||||||
|
- Transmittals (TRN)
|
||||||
|
- Circulation Sheets (CIR)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Functional Requirements
|
||||||
|
|
||||||
|
### 2.1 Auto Number Generation
|
||||||
|
|
||||||
|
#### FR-DN-001: Generate Sequential Number
|
||||||
|
**Priority**: CRITICAL
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องสามารถสร้างเลขที่เอกสารอัตโนมัติตามลำดับ (sequential) โดยไม่ซ้ำกัน
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- เลขที่เอกสารต้องเป็น unique ใน scope ที่กำหนด
|
||||||
|
- ต้องเพิ่มขึ้นทีละ 1 (increment by 1)
|
||||||
|
- ต้องรองรับ concurrent requests โดยไม่มีเลขที่ซ้ำ
|
||||||
|
- Response time < 100ms (p95)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
COR-00001-2025
|
||||||
|
COR-00002-2025
|
||||||
|
COR-00003-2025
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FR-DN-002: Configurable Number Format
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องรองรับการกำหนดรูปแบบเลขที่เอกสารที่หลากหลาย
|
||||||
|
|
||||||
|
**Format Tokens**:
|
||||||
|
- `{PREFIX}` - คำนำหน้าตามประเภทเอกสาร (e.g., COR, RFA)
|
||||||
|
- `{YYYY}` - ปี 4 หลัก (e.g., 2025)
|
||||||
|
- `{YY}` - ปี 2 หลัก (e.g., 25)
|
||||||
|
- `{MM}` - เดือน 2 หลัก (e.g., 01-12)
|
||||||
|
- `{SEQ:n}` - sequence number ความยาว n หลัก (e.g., {SEQ:5} = 00001)
|
||||||
|
- `{PROJECT}` - รหัสโครงการ
|
||||||
|
- `{CONTRACT}` - รหัสสัญญา
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- รองรับ format tokens ที่ระบุ
|
||||||
|
- Admin สามารถกำหนด format ผ่าน UI ได้
|
||||||
|
- Validate format ก่อน save
|
||||||
|
- แสดง preview ของเลขที่ที่จะถูกสร้าง
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```typescript
|
||||||
|
// Correspondence format
|
||||||
|
"COR-{YYYY}-{SEQ:5}"
|
||||||
|
→ COR-2025-00001
|
||||||
|
|
||||||
|
// RFA format with project
|
||||||
|
"RFA-{PROJECT}-{YYYY}{MM}-{SEQ:4}"
|
||||||
|
→ RFA-LCBP3-202501-0001
|
||||||
|
|
||||||
|
// Drawing format
|
||||||
|
"{CONTRACT}-CD-{SEQ:6}"
|
||||||
|
→ C001-CD-000001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FR-DN-003: Scope-based Sequences
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องรองรับการสร้าง sequence ที่แยกตาม scope ที่ต่างกัน
|
||||||
|
|
||||||
|
**Scopes**:
|
||||||
|
1. **Global**: Sequence ระดับระบบทั้งหมด
|
||||||
|
2. **Project**: Sequence แยกตามโครงการ
|
||||||
|
3. **Contract**: Sequence แยกตามสัญญา
|
||||||
|
4. **Yearly**: Sequence reset ทุกปี
|
||||||
|
5. **Monthly**: Sequence reset ทุกเดือน
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- เลขที่ไม่ซ้ำภายใน scope เดียวกัน
|
||||||
|
- Scope ที่ต่างกันสามารถมีเลขที่เดียวกันได้
|
||||||
|
- Support multiple active scopes
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
Project A: COR-A-2025-00001, COR-A-2025-00002
|
||||||
|
Project B: COR-B-2025-00001, COR-B-2025-00002
|
||||||
|
|
||||||
|
Yearly Reset:
|
||||||
|
COR-2024-00999 (Dec 2024)
|
||||||
|
COR-2025-00001 (Jan 2025)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Manual Override
|
||||||
|
|
||||||
|
#### FR-DN-004: Manual Number Assignment
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override)
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
1. Import เอกสารเก่าจากระบบเดิม
|
||||||
|
2. External documents จาก client/consultant
|
||||||
|
3. Correction หลังพบความผิดพลาด
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- ตรวจสอบ duplicate ก่อน save
|
||||||
|
- Validate format ตามรูปแบบที่กำหนด
|
||||||
|
- Auto-update sequence counter ถ้าเลขที่สูงกว่า current
|
||||||
|
- บันทึก audit log ว่าเป็น manual override
|
||||||
|
- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
```typescript
|
||||||
|
interface ManualNumberValidation {
|
||||||
|
format_match: boolean; // ตรง format หรือไม่
|
||||||
|
not_duplicate: boolean; // ไม่ซ้ำ
|
||||||
|
in_valid_range: boolean; // อยู่ในช่วงที่กำหนด
|
||||||
|
permission_granted: boolean; // มีสิทธิ์
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FR-DN-005: Bulk Import Support
|
||||||
|
**Priority**: MEDIUM
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องรองรับการ import เอกสารหลายรายการพร้อมกัน
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- รองรับไฟล์ CSV/Excel
|
||||||
|
- Validate ทุกรายการก่อน import
|
||||||
|
- แสดง preview ก่อน confirm
|
||||||
|
- Rollback ทั้งหมดถ้ามีรายการใดผิดพลาด (transactional)
|
||||||
|
- Auto-update sequence counters หลัง import
|
||||||
|
- Generate import report
|
||||||
|
|
||||||
|
**CSV Format**:
|
||||||
|
```csv
|
||||||
|
document_type,document_number,created_at,metadata
|
||||||
|
COR,COR-2024-00001,2024-01-01,{"imported":true}
|
||||||
|
COR,COR-2024-00002,2024-01-05,{"imported":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Cancelled & Void Handling
|
||||||
|
|
||||||
|
#### FR-DN-006: Skip Cancelled Numbers
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
เลขที่เอกสารที่ถูกยกเลิกต้องไม่ถูก reuse
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- รักษา audit trail ที่ชัดเจน
|
||||||
|
- ป้องกันความสับสน
|
||||||
|
- Legal compliance
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- Cancelled number ยังคงอยู่ในฐานข้อมูลพร้อม status
|
||||||
|
- ระบบข้าม (skip) cancelled number เมื่อสร้างเลขที่ใหม่
|
||||||
|
- บันทึกเหตุผลการยกเลิก
|
||||||
|
- แสดง cancelled numbers ใน audit trail
|
||||||
|
|
||||||
|
**Example Timeline**:
|
||||||
|
```
|
||||||
|
2025-00001 ✅ ACTIVE (created 2025-01-01)
|
||||||
|
2025-00002 ❌ CANCELLED (created 2025-01-02, cancelled 2025-01-03)
|
||||||
|
2025-00003 ✅ ACTIVE (created 2025-01-04)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FR-DN-007: Void and Replace
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องรองรับการ void เอกสารและสร้างเอกสารใหม่แทน
|
||||||
|
|
||||||
|
**Workflow**:
|
||||||
|
1. User เลือกเอกสารที่ต้องการ void
|
||||||
|
2. ระบุเหตุผล (required)
|
||||||
|
3. ระบบเปลี่ยน status เอกสารเดิมเป็น VOID
|
||||||
|
4. สร้างเอกสารใหม่ด้วยเลขที่ใหม่
|
||||||
|
5. Link เอกสารใหม่กับเดิม (voided_from_id)
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- เอกสารเดิม status = VOID (ไม่ลบ)
|
||||||
|
- เอกสารใหม่ได้เลขที่ต่อเนื่องจาก sequence
|
||||||
|
- มี reference link ระหว่างเอกสาร
|
||||||
|
- บันทึก void reason
|
||||||
|
- แสดง void history chain (A→B→C)
|
||||||
|
|
||||||
|
**Database Relationship**:
|
||||||
|
```sql
|
||||||
|
-- Original document
|
||||||
|
id: 100
|
||||||
|
document_number: COR-2025-00005
|
||||||
|
status: VOID
|
||||||
|
void_reason: "ข้อมูลผิด"
|
||||||
|
voided_at: 2025-01-10
|
||||||
|
|
||||||
|
-- Replacement document
|
||||||
|
id: 101
|
||||||
|
document_number: COR-2025-00006
|
||||||
|
status: ACTIVE
|
||||||
|
voided_from_id: 100
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Concurrency & Performance
|
||||||
|
|
||||||
|
#### FR-DN-008: Prevent Race Conditions
|
||||||
|
**Priority**: CRITICAL
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ระบบต้องป้องกันการสร้างเลขที่ซ้ำเมื่อมีการ request พร้อมกัน
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Distributed locking (Redlock)
|
||||||
|
- Database pessimistic locking
|
||||||
|
- Two-phase commit pattern
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- Zero duplicate numbers ภายใต้ concurrent load (1000 req/s)
|
||||||
|
- Lock acquisition time < 50ms (avg)
|
||||||
|
- Automatic retry on lock failure (max 3 times)
|
||||||
|
- Timeout handling (30 seconds)
|
||||||
|
|
||||||
|
**Load Test Requirements**:
|
||||||
|
```bash
|
||||||
|
# Must pass without duplicates
|
||||||
|
concurrent_users: 100
|
||||||
|
requests_per_second: 500
|
||||||
|
test_duration: 5 minutes
|
||||||
|
expected_duplicates: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FR-DN-009: Two-Phase Commit
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
ใช้ Two-phase commit pattern เพื่อความสมบูรณ์ของข้อมูล
|
||||||
|
|
||||||
|
**Phase 1: Reserve**
|
||||||
|
- ล็อกเลขที่และ reserve ไว้ชั่วคราว
|
||||||
|
- Set TTL 5 นาที
|
||||||
|
- Return reservation token
|
||||||
|
|
||||||
|
**Phase 2: Confirm or Cancel**
|
||||||
|
- Confirm: บันทึกลงฐานข้อมูลถาวร
|
||||||
|
- Cancel: คืน lock และ reservation
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- Reservation ต้อง expire หลัง 5 นาที
|
||||||
|
- Auto-cleanup expired reservations
|
||||||
|
- Support explicit cancel
|
||||||
|
- Idempotent confirmation
|
||||||
|
|
||||||
|
**API Flow**:
|
||||||
|
```typescript
|
||||||
|
// Phase 1
|
||||||
|
const { token, number } = await reserveNumber({
|
||||||
|
document_type: 'COR',
|
||||||
|
project_id: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do some work...
|
||||||
|
|
||||||
|
// Phase 2
|
||||||
|
await confirmReservation(token);
|
||||||
|
// OR
|
||||||
|
await cancelReservation(token);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 Monitoring & Audit
|
||||||
|
|
||||||
|
#### FR-DN-010: Complete Audit Trail
|
||||||
|
**Priority**: HIGH
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
บันทึกทุก operation ที่เกิดขึ้นกับเลขที่เอกสาร
|
||||||
|
|
||||||
|
**Events to Log**:
|
||||||
|
- Number reserved
|
||||||
|
- Number confirmed
|
||||||
|
- Number cancelled
|
||||||
|
- Manual override
|
||||||
|
- Void document
|
||||||
|
- Sequence adjusted
|
||||||
|
- Format changed
|
||||||
|
|
||||||
|
**Audit Fields**:
|
||||||
|
```typescript
|
||||||
|
interface AuditLog {
|
||||||
|
id: number;
|
||||||
|
operation: string;
|
||||||
|
document_type: string;
|
||||||
|
document_number: string;
|
||||||
|
old_value?: any;
|
||||||
|
new_value?: any;
|
||||||
|
user_id: number;
|
||||||
|
ip_address: string;
|
||||||
|
user_agent: string;
|
||||||
|
timestamp: Date;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- Log ทุก operation
|
||||||
|
- Searchable by user, date, type
|
||||||
|
- Export to CSV
|
||||||
|
- Retain for 7 years
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FR-DN-011: Metrics & Alerting
|
||||||
|
**Priority**: MEDIUM
|
||||||
|
**Status**: Required
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
แสดงสถิติและส่ง alert เมื่อเกิดปัญหา
|
||||||
|
|
||||||
|
**Metrics**:
|
||||||
|
- Sequence utilization (% of max)
|
||||||
|
- Average lock wait time
|
||||||
|
- Failed lock attempts
|
||||||
|
- Numbers generated per day
|
||||||
|
- Manual overrides per day
|
||||||
|
|
||||||
|
**Alerts**:
|
||||||
|
- Sequence >90% used (WARNING)
|
||||||
|
- Sequence >95% used (CRITICAL)
|
||||||
|
- Lock wait time >1s (WARNING)
|
||||||
|
- Redis unavailable (CRITICAL)
|
||||||
|
- High error rate (WARNING)
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- Real-time dashboard (Grafana)
|
||||||
|
- Email/LINE notifications
|
||||||
|
- Alert history tracking
|
||||||
|
- Configurable thresholds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Non-Functional Requirements
|
||||||
|
|
||||||
|
### 3.1 Performance
|
||||||
|
|
||||||
|
#### NFR-DN-001: Response Time
|
||||||
|
- Number generation: <100ms (p95)
|
||||||
|
- Lock acquisition: <50ms (avg)
|
||||||
|
- Bulk import: <5s per 100 records
|
||||||
|
|
||||||
|
#### NFR-DN-002: Throughput
|
||||||
|
- Support >500 req/s
|
||||||
|
- Scale horizontally (add Redis nodes)
|
||||||
|
|
||||||
|
### 3.2 Reliability
|
||||||
|
|
||||||
|
#### NFR-DN-003: Availability
|
||||||
|
- System uptime: 99.9%
|
||||||
|
- Graceful degradation (fallback to DB-only)
|
||||||
|
- Auto-recovery from Redis failure
|
||||||
|
|
||||||
|
#### NFR-DN-004: Data Integrity
|
||||||
|
- Zero duplicate numbers (100% guarantee)
|
||||||
|
- ACID transactions
|
||||||
|
- Backup & restore procedures
|
||||||
|
|
||||||
|
### 3.3 Security
|
||||||
|
|
||||||
|
#### NFR-DN-005: Access Control
|
||||||
|
- Admin only: Format configuration, sequence adjustment
|
||||||
|
- Manager+: Manual override, void document
|
||||||
|
- User: Auto-generate only
|
||||||
|
- Audit all operations
|
||||||
|
|
||||||
|
#### NFR-DN-006: Data Protection
|
||||||
|
- Encrypt sensitive data (audit logs)
|
||||||
|
- Secure Redis connections (TLS)
|
||||||
|
- Rate limiting (100 req/min per user)
|
||||||
|
|
||||||
|
### 3.4 Scalability
|
||||||
|
|
||||||
|
#### NFR-DN-007: Capacity Planning
|
||||||
|
- Support 10,000 documents/day
|
||||||
|
- Store 10M+ historical numbers
|
||||||
|
- Archive old audit logs (>2 years)
|
||||||
|
|
||||||
|
### 3.5 Maintainability
|
||||||
|
|
||||||
|
#### NFR-DN-008: Code Quality
|
||||||
|
- Unit test coverage: >70%
|
||||||
|
- Integration test coverage: >50%
|
||||||
|
- E2E test coverage: >20 critical paths
|
||||||
|
- Documentation: Complete API docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Business Rules
|
||||||
|
|
||||||
|
### BR-DN-001: Sequence Scope Rules
|
||||||
|
- Correspondence: Project-level + Yearly reset
|
||||||
|
- RFA: Contract-level + Yearly reset
|
||||||
|
- Drawings: Contract-level + No reset
|
||||||
|
- Transmittal: Project-level + Monthly reset
|
||||||
|
|
||||||
|
### BR-DN-002: Number Format Rules
|
||||||
|
- Min length: 10 characters
|
||||||
|
- Max length: 50 characters
|
||||||
|
- Must include {SEQ} token
|
||||||
|
- Must be ASCII only (no Thai/Chinese)
|
||||||
|
|
||||||
|
### BR-DN-003: Manual Override Rules
|
||||||
|
- Only for document_types with allow_manual_override=true
|
||||||
|
- Must validate format
|
||||||
|
- Must check duplicate
|
||||||
|
- Requires Admin permission
|
||||||
|
|
||||||
|
### BR-DN-004: Void Rules
|
||||||
|
- Can only void ACTIVE documents
|
||||||
|
- Cannot void already-VOID documents
|
||||||
|
- Must provide reason (min 10 chars)
|
||||||
|
- Replacement is optional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Data Model
|
||||||
|
|
||||||
|
### 5.1 Numbering Configuration
|
||||||
|
```sql
|
||||||
|
CREATE TABLE document_numbering_configs (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
document_type VARCHAR(50) NOT NULL,
|
||||||
|
format VARCHAR(200) NOT NULL,
|
||||||
|
scope ENUM('GLOBAL','PROJECT','CONTRACT','YEARLY','MONTHLY'),
|
||||||
|
allow_manual_override BOOLEAN DEFAULT FALSE,
|
||||||
|
max_value INT DEFAULT 999999,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY (document_type, scope)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Sequence Counter
|
||||||
|
```sql
|
||||||
|
CREATE TABLE document_numbering_sequences (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
config_id INT NOT NULL,
|
||||||
|
scope_value VARCHAR(50), -- project_id, contract_id, year, etc.
|
||||||
|
current_value INT DEFAULT 0,
|
||||||
|
last_used_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (config_id) REFERENCES document_numbering_configs(id),
|
||||||
|
UNIQUE KEY (config_id, scope_value)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Audit Log
|
||||||
|
```sql
|
||||||
|
CREATE TABLE document_numbering_audit_logs (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
operation VARCHAR(50) NOT NULL,
|
||||||
|
document_type VARCHAR(50),
|
||||||
|
document_number VARCHAR(50),
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent VARCHAR(500),
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
metadata JSON,
|
||||||
|
INDEX idx_document_number (document_number),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_timestamp (timestamp)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API Specifications
|
||||||
|
|
||||||
|
### 6.1 Reserve Number
|
||||||
|
```http
|
||||||
|
POST /api/document-numbering/reserve
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"document_type": "COR",
|
||||||
|
"project_id": 1,
|
||||||
|
"contract_id": null,
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response 201:
|
||||||
|
{
|
||||||
|
"token": "uuid-v4",
|
||||||
|
"document_number": "COR-2025-00042",
|
||||||
|
"expires_at": "2025-01-16T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Confirm Reservation
|
||||||
|
```http
|
||||||
|
POST /api/document-numbering/confirm
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "uuid-v4"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
{
|
||||||
|
"document_number": "COR-2025-00042",
|
||||||
|
"confirmed_at": "2025-01-16T10:25:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Manual Override
|
||||||
|
```http
|
||||||
|
POST /api/document-numbering/manual
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <admin-token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"document_type": "COR",
|
||||||
|
"document_number": "COR-2024-99999",
|
||||||
|
"reason": "Import from legacy system",
|
||||||
|
"skip_validation": false
|
||||||
|
}
|
||||||
|
|
||||||
|
Response 201:
|
||||||
|
{
|
||||||
|
"document_number": "COR-2024-99999",
|
||||||
|
"is_manual": true,
|
||||||
|
"created_at": "2025-01-16T10:25:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Testing Requirements
|
||||||
|
|
||||||
|
### 7.1 Unit Tests
|
||||||
|
- Format parsing and validation
|
||||||
|
- Sequence increment logic
|
||||||
|
- Manual override validation
|
||||||
|
- Scope resolution
|
||||||
|
|
||||||
|
### 7.2 Integration Tests
|
||||||
|
- Redis locking mechanism
|
||||||
|
- Database transactions
|
||||||
|
- Two-phase commit flow
|
||||||
|
- Bulk import
|
||||||
|
|
||||||
|
### 7.3 Load Tests
|
||||||
|
- Concurrent number generation (1000 req/s)
|
||||||
|
- Lock contention under load
|
||||||
|
- Redis failover scenarios
|
||||||
|
- Database connection pool exhaustion
|
||||||
|
|
||||||
|
### 7.4 E2E Tests
|
||||||
|
- Complete document creation flow
|
||||||
|
- Void and replace workflow
|
||||||
|
- Bulk import with validation
|
||||||
|
- Admin configuration UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Migration Plan
|
||||||
|
|
||||||
|
### 8.1 Legacy Data Import
|
||||||
|
1. Export existing document numbers from old system
|
||||||
|
2. Validate format and detect duplicates
|
||||||
|
3. Bulk import using manual override API
|
||||||
|
4. Update sequence counters to max values
|
||||||
|
5. Verify data integrity
|
||||||
|
|
||||||
|
### 8.2 Rollout Strategy
|
||||||
|
- Week 1-2: Deploy to staging, test with dummy data
|
||||||
|
- Week 3: Deploy to production, enable for test project
|
||||||
|
- Week 4: Enable for all projects
|
||||||
|
- Week 5+: Monitor and optimize
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Success Criteria
|
||||||
|
|
||||||
|
### 9.1 Functional Success
|
||||||
|
- ✅ All FRs implemented and tested
|
||||||
|
- ✅ Zero duplicate numbers in production
|
||||||
|
- ✅ Migration of 50,000+ legacy documents
|
||||||
|
- ✅ UAT approved by stakeholders
|
||||||
|
|
||||||
|
### 9.2 Performance Success
|
||||||
|
- ✅ Response time <100ms (p95)
|
||||||
|
- ✅ Throughput >500 req/s
|
||||||
|
- ✅ Lock acquisition <50ms (avg)
|
||||||
|
- ✅ Zero downtime during deployment
|
||||||
|
|
||||||
|
### 9.3 Business Success
|
||||||
|
- ✅ Document creation speed +30%
|
||||||
|
- ✅ Manual numbering errors -80%
|
||||||
|
- ✅ User satisfaction >4.5/5
|
||||||
|
- ✅ System stability >99.9%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Appendix
|
||||||
|
|
||||||
|
### 10.1 Glossary
|
||||||
|
- **Sequence**: ลำดับตัวเลขที่เพิ่มขึ้นอัตโนมัติ
|
||||||
|
- **Scope**: ขอบเขตที่ sequence แยกตาม (project, contract, etc.)
|
||||||
|
- **Token**: Format placeholder (e.g., {YYYY}, {SEQ})
|
||||||
|
- **Redlock**: Distributed locking algorithm สำหรับ Redis
|
||||||
|
|
||||||
|
### 10.2 References
|
||||||
|
- [ADR-018: Document Numbering Strategy](../05-decisions/adr-018-document-numbering.md)
|
||||||
|
- [Two-Phase Commit Pattern](https://en.wikipedia.org/wiki/Two-phase_commit_protocol)
|
||||||
|
- [Redlock Algorithm](https://redis.io/docs/manual/patterns/distributed-locks/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Approval Sign-off**:
|
||||||
|
|
||||||
|
| Role | Name | Date | Signature |
|
||||||
|
|------|------|------|-----------|
|
||||||
|
| Product Owner | ___________ | _______ | _________ |
|
||||||
|
| Tech Lead | ___________ | _______ | _________ |
|
||||||
|
| QA Lead | ___________ | _______ | _________ |
|
||||||
1871
docs/backup/03.11-document-numbering.md
Normal file
1871
docs/backup/03.11-document-numbering.md
Normal file
File diff suppressed because it is too large
Load Diff
1813
docs/backup/document-numbering-add.md
Normal file
1813
docs/backup/document-numbering-add.md
Normal file
File diff suppressed because it is too large
Load Diff
813
docs/backup/document-numbering.md
Normal file
813
docs/backup/document-numbering.md
Normal file
@@ -0,0 +1,813 @@
|
|||||||
|
# Document Numbering Implementation Guide
|
||||||
|
|
||||||
|
---
|
||||||
|
title: 'Implementation Guide: Document Numbering System'
|
||||||
|
version: 1.6.1
|
||||||
|
status: implemented
|
||||||
|
owner: Development Team
|
||||||
|
last_updated: 2025-12-16
|
||||||
|
related:
|
||||||
|
|
||||||
|
- specs/01-requirements/03.11-document-numbering.md
|
||||||
|
- specs/04-operations/document-numbering-operations.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
เอกสารนี้อธิบาย implementation details สำหรับระบบ Document Numbering ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Backend Framework**: NestJS 10.x
|
||||||
|
- **ORM**: TypeORM 0.3.x
|
||||||
|
- **Database**: MariaDB 11.8
|
||||||
|
- **Cache/Lock**: Redis 7.x + Redlock
|
||||||
|
- **Message Queue**: BullMQ
|
||||||
|
- **Monitoring**: Prometheus + Grafana
|
||||||
|
|
||||||
|
## 1. Database Implementation
|
||||||
|
|
||||||
|
### 1.1. Counter Table Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE document_number_formats (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
project_id INT NOT NULL,
|
||||||
|
correspondence_type_id INT NULL, -- NULL indicates default format for the project
|
||||||
|
format_template VARCHAR(100) NOT NULL,
|
||||||
|
reset_sequence_yearly TINYINT(1) DEFAULT 1,
|
||||||
|
description VARCHAR(255),
|
||||||
|
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
|
||||||
|
UNIQUE KEY idx_unique_project_type (project_id, correspondence_type_id),
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE document_number_counters (
|
||||||
|
project_id INT NOT NULL,
|
||||||
|
originator_organization_id INT NOT NULL,
|
||||||
|
recipient_organization_id INT NULL,
|
||||||
|
correspondence_type_id INT NOT NULL,
|
||||||
|
sub_type_id INT DEFAULT 0,
|
||||||
|
rfa_type_id INT DEFAULT 0,
|
||||||
|
discipline_id INT DEFAULT 0,
|
||||||
|
current_year INT NOT NULL,
|
||||||
|
version INT DEFAULT 0 NOT NULL,
|
||||||
|
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,
|
||||||
|
|
||||||
|
INDEX idx_counter_lookup (project_id, correspondence_type_id, current_year),
|
||||||
|
INDEX idx_counter_org (originator_organization_id, current_year),
|
||||||
|
|
||||||
|
CONSTRAINT chk_last_number_positive CHECK (last_number >= 0),
|
||||||
|
CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||||
|
COMMENT='ตารางเก็บ Running Number Counters';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2. Audit Table Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE document_number_audit (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
document_id INT NULL COMMENT 'FK to documents (NULL initially, updated after doc creation)',
|
||||||
|
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 NULL COMMENT 'FK to users (Allow NULL for system generation)',
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_success BOOLEAN DEFAULT TRUE COMMENT 'Track success/failure status',
|
||||||
|
|
||||||
|
-- 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';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3. Error Log Table
|
||||||
|
|
||||||
|
```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';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. NestJS Implementation
|
||||||
|
|
||||||
|
### 2.1. Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/document-numbering/
|
||||||
|
├── document-numbering.module.ts
|
||||||
|
├── controllers/
|
||||||
|
│ └── document-numbering.controller.ts
|
||||||
|
├── services/
|
||||||
|
│ ├── document-numbering.service.ts
|
||||||
|
│ ├── document-numbering-lock.service.ts
|
||||||
|
│ ├── counter.service.ts
|
||||||
|
│ ├── template.service.ts
|
||||||
|
│ └── audit.service.ts
|
||||||
|
├── entities/
|
||||||
|
│ ├── document-number-counter.entity.ts
|
||||||
|
│ ├── document-number-audit.entity.ts
|
||||||
|
│ └── document-number-error.entity.ts
|
||||||
|
├── dto/
|
||||||
|
│ ├── generate-number.dto.ts
|
||||||
|
│ └── update-template.dto.ts
|
||||||
|
├── validators/
|
||||||
|
│ └── template.validator.ts
|
||||||
|
├── jobs/
|
||||||
|
│ └── counter-reset.job.ts
|
||||||
|
└── metrics/
|
||||||
|
└── metrics.service.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. Number Generation Process
|
||||||
|
|
||||||
|
#### 2.2.1. Resolve Format Template:
|
||||||
|
* Query document_number_formats by project_id + type_id.
|
||||||
|
* If no result, query by project_id + NULL (Default Project Format).
|
||||||
|
* If still no result, apply System Default Template: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`.
|
||||||
|
* Determine resetSequenceYearly flag from the found format (default: true)
|
||||||
|
|
||||||
|
#### 2.2.2. Determine Counter Key:
|
||||||
|
* If resetSequenceYearly is True: Use Current Year (e.g., 2025).
|
||||||
|
* If resetSequenceYearly is False: Use 0 (Continuous).
|
||||||
|
* Use type_id from the resolved format (Specific ID or NULL).
|
||||||
|
|
||||||
|
#### 2.2.3. Generate Number:
|
||||||
|
* Use format template to generate number.
|
||||||
|
* Replace tokens with actual values:
|
||||||
|
* {PROJECT} -> Project Code
|
||||||
|
* {ORG} -> Originator Organization Code
|
||||||
|
* {RECIPIENT} -> Recipient Organization Code
|
||||||
|
* {TYPE} -> Type Code
|
||||||
|
* {YEAR} -> Current Year
|
||||||
|
* {SEQ} -> Sequence Number
|
||||||
|
* {REV} -> Revision Number
|
||||||
|
|
||||||
|
#### 2.2.4. Validate Number:
|
||||||
|
* Check if generated number is unique.
|
||||||
|
* If not unique, increment sequence and retry.
|
||||||
|
|
||||||
|
#### 2.2.5. Update Counter:
|
||||||
|
* Update document_number_counters with new sequence.
|
||||||
|
|
||||||
|
#### 2.2.6. Generate Audit Record:
|
||||||
|
* Create audit record with:
|
||||||
|
* Generated number
|
||||||
|
* Counter key used
|
||||||
|
* Template used
|
||||||
|
* User ID
|
||||||
|
* IP Address
|
||||||
|
* User Agent
|
||||||
|
|
||||||
|
#### 2.2.7. Return Generated Number:
|
||||||
|
* Return generated number to caller.
|
||||||
|
|
||||||
|
### 2.3. TypeORM Entity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4. Redis Lock Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/services/document-numbering-lock.service.ts
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import Redlock from 'redlock';
|
||||||
|
import { InjectRedis } from '@nestjs-modules/ioredis';
|
||||||
|
import { Redis } from 'ioredis';
|
||||||
|
|
||||||
|
interface CounterKey {
|
||||||
|
projectId: number;
|
||||||
|
originatorOrgId: number;
|
||||||
|
recipientOrgId: number | null;
|
||||||
|
correspondenceTypeId: number;
|
||||||
|
subTypeId: number;
|
||||||
|
rfaTypeId: number;
|
||||||
|
disciplineId: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentNumberingLockService {
|
||||||
|
private readonly logger = new Logger(DocumentNumberingLockService.name);
|
||||||
|
private redlock: Redlock;
|
||||||
|
|
||||||
|
constructor(@InjectRedis() private readonly redis: Redis) {
|
||||||
|
this.redlock = new Redlock([redis], {
|
||||||
|
driftFactor: 0.01,
|
||||||
|
retryCount: 5,
|
||||||
|
retryDelay: 100,
|
||||||
|
retryJitter: 50,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireLock(counterKey: CounterKey): Promise<Redlock.Lock> {
|
||||||
|
const lockKey = this.buildLockKey(counterKey);
|
||||||
|
const ttl = 5000; // 5 วินาที
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lock = await this.redlock.acquire([lockKey], ttl);
|
||||||
|
this.logger.debug(`Acquired lock: ${lockKey}`);
|
||||||
|
return lock;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to acquire lock: ${lockKey}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseLock(lock: Redlock.Lock): Promise<void> {
|
||||||
|
try {
|
||||||
|
await lock.release();
|
||||||
|
this.logger.debug('Released lock');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Failed to release lock (may have expired)', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4. Counter Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/services/counter.service.ts
|
||||||
|
import { Injectable, ConflictException, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, DataSource } from 'typeorm';
|
||||||
|
import { DocumentNumberCounter } from '../entities/document-number-counter.entity';
|
||||||
|
import { OptimisticLockVersionMismatchError } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CounterService {
|
||||||
|
private readonly logger = new Logger(CounterService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DocumentNumberCounter)
|
||||||
|
private counterRepo: Repository<DocumentNumberCounter>,
|
||||||
|
private dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async incrementCounter(counterKey: CounterKey): Promise<number> {
|
||||||
|
const MAX_RETRIES = 2;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
return await this.dataSource.transaction(async (manager) => {
|
||||||
|
// ใช้ Optimistic Locking
|
||||||
|
const counter = await manager.findOne(DocumentNumberCounter, {
|
||||||
|
where: this.buildWhereClause(counterKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!counter) {
|
||||||
|
// สร้าง counter ใหม่
|
||||||
|
const newCounter = manager.create(DocumentNumberCounter, {
|
||||||
|
...counterKey,
|
||||||
|
lastNumber: 1,
|
||||||
|
version: 0,
|
||||||
|
});
|
||||||
|
await manager.save(newCounter);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.lastNumber += 1;
|
||||||
|
await manager.save(counter); // Auto-check version
|
||||||
|
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('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildWhereClause(key: CounterKey) {
|
||||||
|
return {
|
||||||
|
projectId: key.projectId,
|
||||||
|
originatorOrganizationId: key.originatorOrgId,
|
||||||
|
recipientOrganizationId: key.recipientOrgId,
|
||||||
|
correspondenceTypeId: key.correspondenceTypeId,
|
||||||
|
subTypeId: key.subTypeId,
|
||||||
|
rfaTypeId: key.rfaTypeId,
|
||||||
|
disciplineId: key.disciplineId,
|
||||||
|
currentYear: key.year,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5. Main Service with Retry Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/services/document-numbering.service.ts
|
||||||
|
import { Injectable, ServiceUnavailableException, Logger } from '@nestjs/common';
|
||||||
|
import { DocumentNumberingLockService } from './document-numbering-lock.service';
|
||||||
|
import { CounterService } from './counter.service';
|
||||||
|
import { AuditService } from './audit.service';
|
||||||
|
import { RedisConnectionError } from 'ioredis';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentNumberingService {
|
||||||
|
private readonly logger = new Logger(DocumentNumberingService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private lockService: DocumentNumberingLockService,
|
||||||
|
private counterService: CounterService,
|
||||||
|
private auditService: AuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async generateDocumentNumber(dto: GenerateNumberDto): Promise<string> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
let lockWaitMs = 0;
|
||||||
|
let retryCount = 0;
|
||||||
|
let fallbackUsed = 'NONE';
|
||||||
|
|
||||||
|
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');
|
||||||
|
fallbackUsed = 'DB_LOCK';
|
||||||
|
return await this.generateWithDbLock(dto);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// บันทึก audit log
|
||||||
|
await this.auditService.logGeneration({
|
||||||
|
documentId: dto.documentId,
|
||||||
|
counterKey: dto.counterKey,
|
||||||
|
lockWaitMs,
|
||||||
|
totalDurationMs: Date.now() - startTime,
|
||||||
|
fallbackUsed,
|
||||||
|
retryCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateWithRedisLock(dto: GenerateNumberDto): Promise<string> {
|
||||||
|
const lock = await this.lockService.acquireLock(dto.counterKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextNumber = await this.counterService.incrementCounter(dto.counterKey);
|
||||||
|
return this.formatNumber(dto.template, nextNumber, dto.counterKey);
|
||||||
|
} finally {
|
||||||
|
await this.lockService.releaseLock(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateWithDbLock(dto: GenerateNumberDto): Promise<string> {
|
||||||
|
// ใช้ pessimistic lock
|
||||||
|
// Implementation details...
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(template: string, seq: number, key: CounterKey): string {
|
||||||
|
// Template formatting logic
|
||||||
|
// Example: `คคง.-สคฉ.3-0001-2568`
|
||||||
|
return template
|
||||||
|
.replace('{SEQ:4}', seq.toString().padStart(4, '0'))
|
||||||
|
.replace('{YEAR:B.E.}', (key.year + 543).toString());
|
||||||
|
// ... more replacements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Template Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/validators/template.validator.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (!tokens.some((t) => t.name === 'DISCIPLINE')) {
|
||||||
|
errors.push('RFA template ต้องมี {DISCIPLINE}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (correspondenceType === 'TRANSMITTAL') {
|
||||||
|
if (!tokens.some((t) => t.name === 'SUB_TYPE')) {
|
||||||
|
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ทุก template ต้องมี {SEQ}
|
||||||
|
if (!tokens.some((t) => t.name.startsWith('SEQ'))) {
|
||||||
|
errors.push('Template ต้องมี {SEQ:n}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokens(template: string) {
|
||||||
|
const regex = /\{([^}]+)\}/g;
|
||||||
|
const tokens: Array<{ name: string; full: string }> = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(template)) !== null) {
|
||||||
|
const tokenName = match[1].split(':')[0]; // SEQ:4 → SEQ
|
||||||
|
tokens.push({ name: tokenName, full: match[1] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. BullMQ Job for Counter Reset
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/jobs/counter-reset.job.ts
|
||||||
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
|
||||||
|
@Processor('document-numbering')
|
||||||
|
@Injectable()
|
||||||
|
export class CounterResetJob extends WorkerHost {
|
||||||
|
private readonly logger = new Logger(CounterResetJob.name);
|
||||||
|
|
||||||
|
@Cron('0 0 1 1 *') // 1 Jan every year at 00:00
|
||||||
|
async handleYearlyReset() {
|
||||||
|
const newYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
// ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว
|
||||||
|
// แค่เตรียม counter สำหรับปีใหม่
|
||||||
|
this.logger.log(`Year changed to ${newYear}, counters are ready`);
|
||||||
|
|
||||||
|
// สามารถทำ cleanup counter ปีเก่าได้ (optional)
|
||||||
|
// await this.cleanupOldCounters(newYear - 5); // เก็บ 5 ปี
|
||||||
|
}
|
||||||
|
|
||||||
|
async process() {
|
||||||
|
// BullMQ job processing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. API Controller
|
||||||
|
|
||||||
|
### 5.1. Main Controller (`/document-numbering`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/document-numbering.controller.ts
|
||||||
|
import {
|
||||||
|
Controller, Get, Post, Patch,
|
||||||
|
Body, Param, Query, UseGuards, ParseIntPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { DocumentNumberingService } from './document-numbering.service';
|
||||||
|
import { PreviewNumberDto } from './dto/preview-number.dto';
|
||||||
|
|
||||||
|
@Controller('document-numbering')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class DocumentNumberingController {
|
||||||
|
constructor(private readonly numberingService: DocumentNumberingService) {}
|
||||||
|
|
||||||
|
// --- Logs ---
|
||||||
|
|
||||||
|
@Get('logs/audit')
|
||||||
|
@RequirePermission('system.view_logs')
|
||||||
|
getAuditLogs(@Query('limit') limit?: number) {
|
||||||
|
return this.numberingService.getAuditLogs(limit ? Number(limit) : 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('logs/errors')
|
||||||
|
@RequirePermission('system.view_logs')
|
||||||
|
getErrorLogs(@Query('limit') limit?: number) {
|
||||||
|
return this.numberingService.getErrorLogs(limit ? Number(limit) : 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sequences / Counters ---
|
||||||
|
|
||||||
|
@Get('sequences')
|
||||||
|
@RequirePermission('correspondence.read')
|
||||||
|
getSequences(@Query('projectId') projectId?: number) {
|
||||||
|
return this.numberingService.getSequences(projectId ? Number(projectId) : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('counters/:id')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async updateCounter(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body('sequence') sequence: number
|
||||||
|
) {
|
||||||
|
return this.numberingService.setCounterValue(id, sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Preview ---
|
||||||
|
|
||||||
|
@Post('preview')
|
||||||
|
@RequirePermission('correspondence.read')
|
||||||
|
async previewNumber(@Body() dto: PreviewNumberDto) {
|
||||||
|
return this.numberingService.previewNumber(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2. Admin Controller (`/admin/document-numbering`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/document-numbering-admin.controller.ts
|
||||||
|
import {
|
||||||
|
Controller, Get, Post, Delete, Body, Param, Query,
|
||||||
|
UseGuards, ParseIntPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { DocumentNumberingService } from './document-numbering.service';
|
||||||
|
|
||||||
|
@Controller('admin/document-numbering')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class DocumentNumberingAdminController {
|
||||||
|
constructor(private readonly service: DocumentNumberingService) {}
|
||||||
|
|
||||||
|
// --- Template Management ---
|
||||||
|
|
||||||
|
@Get('templates')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async getTemplates(@Query('projectId') projectId?: number) {
|
||||||
|
if (projectId) {
|
||||||
|
return this.service.getTemplatesByProject(projectId);
|
||||||
|
}
|
||||||
|
return this.service.getTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('templates')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async saveTemplate(@Body() dto: any) {
|
||||||
|
return this.service.saveTemplate(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('templates/:id')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async deleteTemplate(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
await this.service.deleteTemplate(id);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Metrics ---
|
||||||
|
|
||||||
|
@Get('metrics')
|
||||||
|
@RequirePermission('system.view_logs')
|
||||||
|
async getMetrics() {
|
||||||
|
const audit = await this.service.getAuditLogs(50);
|
||||||
|
const errors = await this.service.getErrorLogs(50);
|
||||||
|
return { audit, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Admin Operations ---
|
||||||
|
|
||||||
|
@Post('manual-override')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async manualOverride(@Body() dto: {
|
||||||
|
projectId: number;
|
||||||
|
correspondenceTypeId: number | null;
|
||||||
|
year: number;
|
||||||
|
newValue: number;
|
||||||
|
}) {
|
||||||
|
return this.service.manualOverride(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('void-and-replace')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async voidAndReplace(@Body() dto: {
|
||||||
|
documentId: number;
|
||||||
|
reason: string;
|
||||||
|
}) {
|
||||||
|
return this.service.voidAndReplace(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('cancel')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async cancelNumber(@Body() dto: {
|
||||||
|
documentNumber: string;
|
||||||
|
reason: string;
|
||||||
|
}) {
|
||||||
|
return this.service.cancelNumber(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-import')
|
||||||
|
@RequirePermission('system.manage_settings')
|
||||||
|
async bulkImport(@Body() items: any[]) {
|
||||||
|
return this.service.bulkImport(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3. API Endpoints Summary
|
||||||
|
|
||||||
|
| Endpoint | Method | Permission | Description |
|
||||||
|
| -------------------------------------------- | ------ | ------------------------ | --------------------------------- |
|
||||||
|
| `/document-numbering/logs/audit` | GET | `system.view_logs` | Get audit logs |
|
||||||
|
| `/document-numbering/logs/errors` | GET | `system.view_logs` | Get error logs |
|
||||||
|
| `/document-numbering/sequences` | GET | `correspondence.read` | Get counter sequences |
|
||||||
|
| `/document-numbering/counters/:id` | PATCH | `system.manage_settings` | Update counter value |
|
||||||
|
| `/document-numbering/preview` | POST | `correspondence.read` | Preview number without generating |
|
||||||
|
| `/admin/document-numbering/templates` | GET | `system.manage_settings` | Get all templates |
|
||||||
|
| `/admin/document-numbering/templates` | POST | `system.manage_settings` | Create/update template |
|
||||||
|
| `/admin/document-numbering/templates/:id` | DELETE | `system.manage_settings` | Delete template |
|
||||||
|
| `/admin/document-numbering/metrics` | GET | `system.view_logs` | Get metrics (audit + errors) |
|
||||||
|
| `/admin/document-numbering/manual-override` | POST | `system.manage_settings` | Override counter value |
|
||||||
|
| `/admin/document-numbering/void-and-replace` | POST | `system.manage_settings` | Void and replace number |
|
||||||
|
| `/admin/document-numbering/cancel` | POST | `system.manage_settings` | Cancel a number |
|
||||||
|
| `/admin/document-numbering/bulk-import` | POST | `system.manage_settings` | Bulk import counters |
|
||||||
|
|
||||||
|
## 6. Module Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/document-numbering.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||||
|
import { DocumentNumberAudit } from './entities/document-number-audit.entity';
|
||||||
|
import { DocumentNumberError } from './entities/document-number-error.entity';
|
||||||
|
import { DocumentNumberingService } from './services/document-numbering.service';
|
||||||
|
import { DocumentNumberingLockService } from './services/document-numbering-lock.service';
|
||||||
|
import { CounterService } from './services/counter.service';
|
||||||
|
import { AuditService } from './services/audit.service';
|
||||||
|
import { TemplateValidator } from './validators/template.validator';
|
||||||
|
import { CounterResetJob } from './jobs/counter-reset.job';
|
||||||
|
import { DocumentNumberingController } from './controllers/document-numbering.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
DocumentNumberCounter,
|
||||||
|
DocumentNumberAudit,
|
||||||
|
DocumentNumberError,
|
||||||
|
]),
|
||||||
|
BullModule.registerQueue({
|
||||||
|
name: 'document-numbering',
|
||||||
|
}),
|
||||||
|
ThrottlerModule.forRoot({
|
||||||
|
ttl: 60,
|
||||||
|
limit: 10,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [DocumentNumberingController],
|
||||||
|
providers: [
|
||||||
|
DocumentNumberingService,
|
||||||
|
DocumentNumberingLockService,
|
||||||
|
CounterService,
|
||||||
|
AuditService,
|
||||||
|
TemplateValidator,
|
||||||
|
CounterResetJob,
|
||||||
|
],
|
||||||
|
exports: [DocumentNumberingService],
|
||||||
|
})
|
||||||
|
export class DocumentNumberingModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Environment Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// .env.example
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=lcbp3
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_DATABASE=lcbp3_db
|
||||||
|
DB_POOL_SIZE=20
|
||||||
|
|
||||||
|
# Prometheus
|
||||||
|
PROMETHEUS_PORT=9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md)
|
||||||
|
- [Backend Guidelines](file:///e:/np-dms/lcbp3/specs/03-implementation/backend-guidelines.md)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user