260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
@@ -10,9 +10,11 @@
## 1. Overview
### 1.1 Purpose
ระบบ Document Numbering สำหรับสร้างเลขที่เอกสารอัตโนมัติที่มีความเป็นเอกลักษณ์ (unique) และสามารถติดตามได้ (traceable) สำหรับเอกสารทุกประเภทในระบบ LCBP3-DMS
### 1.2 Scope
- Auto-generation ของเลขที่เอกสารตามรูปแบบที่กำหนด
- Manual override สำหรับการ import เอกสารเก่า
- Cancelled number handling (ไม่ reuse)
@@ -21,6 +23,7 @@
- Complete audit trail สำหรับทุก operation
### 1.3 Document Types Supported
- Correspondences (COR)
- Request for Approvals (RFA)
- Contract Drawings (CD)
@@ -35,6 +38,7 @@
### 2.1 Auto Number Generation
#### FR-DN-001: Generate Sequential Number
**Priority**: CRITICAL
**Status**: Required
@@ -42,12 +46,14 @@
ระบบต้องสามารถสร้างเลขที่เอกสารอัตโนมัติตามลำดับ (sequential) โดยไม่ซ้ำกัน
**Acceptance Criteria**:
- เลขที่เอกสารต้องเป็น unique ใน scope ที่กำหนด
- ต้องเพิ่มขึ้นทีละ 1 (increment by 1)
- ต้องรองรับ concurrent requests โดยไม่มีเลขที่ซ้ำ
- Response time < 100ms (p95)
**Example**:
```
COR-00001-2025
COR-00002-2025
@@ -57,6 +63,7 @@ COR-00003-2025
---
#### FR-DN-002: Configurable Number Format
**Priority**: HIGH
**Status**: Required
@@ -64,6 +71,7 @@ COR-00003-2025
ระบบต้องรองรับการกำหนดรูปแบบเลขที่เอกสารที่หลากหลาย
**Format Tokens**:
- `{PREFIX}` - คำนำหน้าตามประเภทเอกสาร (e.g., COR, RFA)
- `{YYYY}` - ปี 4 หลัก (e.g., 2025)
- `{YY}` - ปี 2 หลัก (e.g., 25)
@@ -73,12 +81,14 @@ COR-00003-2025
- `{CONTRACT}` - รหัสสัญญา
**Acceptance Criteria**:
- รองรับ format tokens ที่ระบุ
- Admin สามารถกำหนด format ผ่าน UI ได้
- Validate format ก่อน save
- แสดง preview ของเลขที่ที่จะถูกสร้าง
**Examples**:
```typescript
// Correspondence format
"COR-{YYYY}-{SEQ:5}"
@@ -96,6 +106,7 @@ COR-00003-2025
---
#### FR-DN-003: Scope-based Sequences
**Priority**: HIGH
**Status**: Required
@@ -103,6 +114,7 @@ COR-00003-2025
ระบบต้องรองรับการสร้าง sequence ที่แยกตาม scope ที่ต่างกัน
**Scopes**:
1. **Global**: Sequence ระดับระบบทั้งหมด
2. **Project**: Sequence แยกตามโครงการ
3. **Contract**: Sequence แยกตามสัญญา
@@ -110,11 +122,13 @@ COR-00003-2025
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
@@ -129,6 +143,7 @@ COR-2025-00001 (Jan 2025)
### 2.2 Manual Override
#### FR-DN-004: Manual Number Assignment
**Priority**: HIGH
**Status**: Required
@@ -136,11 +151,13 @@ COR-2025-00001 (Jan 2025)
ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override)
**Use Cases**:
1. Import เอกสารเก่าจากระบบเดิม
2. External documents จาก client/consultant
3. Correction หลังพบความผิดพลาด
**Acceptance Criteria**:
- ตรวจสอบ duplicate ก่อน save
- Validate format ตามรูปแบบที่กำหนด
- Auto-update sequence counter ถ้าเลขที่สูงกว่า current
@@ -148,11 +165,12 @@ COR-2025-00001 (Jan 2025)
- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น
**Validation Rules**:
```typescript
interface ManualNumberValidation {
format_match: boolean; // ตรง format หรือไม่
not_duplicate: boolean; // ไม่ซ้ำ
in_valid_range: boolean; // อยู่ในช่วงที่กำหนด
format_match: boolean; // ตรง format หรือไม่
not_duplicate: boolean; // ไม่ซ้ำ
in_valid_range: boolean; // อยู่ในช่วงที่กำหนด
permission_granted: boolean; // มีสิทธิ์
}
```
@@ -160,6 +178,7 @@ interface ManualNumberValidation {
---
#### FR-DN-005: Bulk Import Support
**Priority**: MEDIUM
**Status**: Required
@@ -167,6 +186,7 @@ interface ManualNumberValidation {
ระบบต้องรองรับการ import เอกสารหลายรายการพร้อมกัน
**Acceptance Criteria**:
- รองรับไฟล์ CSV/Excel
- Validate ทุกรายการก่อน import
- แสดง preview ก่อน confirm
@@ -175,6 +195,7 @@ interface ManualNumberValidation {
- Generate import report
**CSV Format**:
```csv
document_type,document_number,created_at,metadata
COR,COR-2024-00001,2024-01-01,{"imported":true}
@@ -186,6 +207,7 @@ COR,COR-2024-00002,2024-01-05,{"imported":true}
### 2.3 Cancelled & Void Handling
#### FR-DN-006: Skip Cancelled Numbers
**Priority**: HIGH
**Status**: Required
@@ -193,17 +215,20 @@ COR,COR-2024-00002,2024-01-05,{"imported":true}
เลขที่เอกสารที่ถูกยกเลิกต้องไม่ถูก 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)
@@ -213,6 +238,7 @@ COR,COR-2024-00002,2024-01-05,{"imported":true}
---
#### FR-DN-007: Void and Replace
**Priority**: HIGH
**Status**: Required
@@ -220,6 +246,7 @@ COR,COR-2024-00002,2024-01-05,{"imported":true}
ระบบต้องรองรับการ void เอกสารและสร้างเอกสารใหม่แทน
**Workflow**:
1. User เลือกเอกสารที่ต้องการ void
2. ระบุเหตุผล (required)
3. ระบบเปลี่ยน status เอกสารเดิมเป็น VOID
@@ -227,6 +254,7 @@ COR,COR-2024-00002,2024-01-05,{"imported":true}
5. Link เอกสารใหม่กับเดิม (voided_from_id)
**Acceptance Criteria**:
- เอกสารเดิม status = VOID (ไม่ลบ)
- เอกสารใหม่ได้เลขที่ต่อเนื่องจาก sequence
- มี reference link ระหว่างเอกสาร
@@ -234,6 +262,7 @@ COR,COR-2024-00002,2024-01-05,{"imported":true}
- แสดง void history chain (A→B→C)
**Database Relationship**:
```sql
-- Original document
id: 100
@@ -254,6 +283,7 @@ voided_from_id: 100
### 2.4 Concurrency & Performance
#### FR-DN-008: Prevent Race Conditions
**Priority**: CRITICAL
**Status**: Required
@@ -261,17 +291,20 @@ voided_from_id: 100
ระบบต้องป้องกันการสร้างเลขที่ซ้ำเมื่อมีการ 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
@@ -283,6 +316,7 @@ expected_duplicates: 0
---
#### FR-DN-009: Two-Phase Commit
**Priority**: HIGH
**Status**: Required
@@ -290,26 +324,30 @@ expected_duplicates: 0
ใช้ 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
project_id: 1,
});
// Do some work...
@@ -325,6 +363,7 @@ await cancelReservation(token);
### 2.5 Monitoring & Audit
#### FR-DN-010: Complete Audit Trail
**Priority**: HIGH
**Status**: Required
@@ -332,6 +371,7 @@ await cancelReservation(token);
บันทึกทุก operation ที่เกิดขึ้นกับเลขที่เอกสาร
**Events to Log**:
- Number reserved
- Number confirmed
- Number cancelled
@@ -341,6 +381,7 @@ await cancelReservation(token);
- Format changed
**Audit Fields**:
```typescript
interface AuditLog {
id: number;
@@ -358,6 +399,7 @@ interface AuditLog {
```
**Acceptance Criteria**:
- Log ทุก operation
- Searchable by user, date, type
- Export to CSV
@@ -366,6 +408,7 @@ interface AuditLog {
---
#### FR-DN-011: Metrics & Alerting
**Priority**: MEDIUM
**Status**: Required
@@ -373,6 +416,7 @@ interface AuditLog {
แสดงสถิติและส่ง alert เมื่อเกิดปัญหา
**Metrics**:
- Sequence utilization (% of max)
- Average lock wait time
- Failed lock attempts
@@ -380,6 +424,7 @@ interface AuditLog {
- Manual overrides per day
**Alerts**:
- Sequence >90% used (WARNING)
- Sequence >95% used (CRITICAL)
- Lock wait time >1s (WARNING)
@@ -387,6 +432,7 @@ interface AuditLog {
- High error rate (WARNING)
**Acceptance Criteria**:
- Real-time dashboard (Grafana)
- Email/LINE notifications
- Alert history tracking
@@ -399,22 +445,26 @@ interface AuditLog {
### 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
@@ -422,12 +472,14 @@ interface AuditLog {
### 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)
@@ -435,6 +487,7 @@ interface AuditLog {
### 3.4 Scalability
#### NFR-DN-007: Capacity Planning
- Support 10,000 documents/day
- Store 10M+ historical numbers
- Archive old audit logs (>2 years)
@@ -442,6 +495,7 @@ interface AuditLog {
### 3.5 Maintainability
#### NFR-DN-008: Code Quality
- Unit test coverage: >70%
- Integration test coverage: >50%
- E2E test coverage: >20 critical paths
@@ -452,24 +506,28 @@ interface AuditLog {
## 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)
@@ -480,6 +538,7 @@ interface AuditLog {
## 5. Data Model
### 5.1 Numbering Configuration
```sql
CREATE TABLE document_numbering_configs (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -495,6 +554,7 @@ CREATE TABLE document_numbering_configs (
```
### 5.2 Sequence Counter
```sql
CREATE TABLE document_numbering_sequences (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -509,6 +569,7 @@ CREATE TABLE document_numbering_sequences (
```
### 5.3 Audit Log
```sql
CREATE TABLE document_numbering_audit_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
@@ -533,6 +594,7 @@ CREATE TABLE document_numbering_audit_logs (
## 6. API Specifications
### 6.1 Reserve Number
```http
POST /api/document-numbering/reserve
Content-Type: application/json
@@ -553,6 +615,7 @@ Response 201:
```
### 6.2 Confirm Reservation
```http
POST /api/document-numbering/confirm
Content-Type: application/json
@@ -569,6 +632,7 @@ Response 200:
```
### 6.3 Manual Override
```http
POST /api/document-numbering/manual
Content-Type: application/json
@@ -594,24 +658,28 @@ Response 201:
## 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
@@ -622,6 +690,7 @@ Response 201:
## 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
@@ -629,6 +698,7 @@ Response 201:
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
@@ -639,18 +709,21 @@ Response 201:
## 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
@@ -661,12 +734,14 @@ Response 201:
## 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/)
@@ -675,8 +750,8 @@ Response 201:
**Approval Sign-off**:
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Product Owner | ___________ | _______ | _________ |
| Tech Lead | ___________ | _______ | _________ |
| QA Lead | ___________ | _______ | _________ |
| Role | Name | Date | Signature |
| ------------- | -------------- | ---------- | ---------- |
| Product Owner | ****\_\_\_**** | **\_\_\_** | ****\_**** |
| Tech Lead | ****\_\_\_**** | **\_\_\_** | ****\_**** |
| QA Lead | ****\_\_\_**** | **\_\_\_** | ****\_**** |
@@ -1,6 +1,7 @@
# 3.11 Document Numbering Management (การจัดการเลขที่เอกสาร)
---
title: 'Functional Requirements: Document Numbering Management'
version: 1.6.0
status: draft
@@ -33,6 +34,7 @@ related:
การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร
**Scopes**:
1. **Global**: Sequence ระดับระบบทั้งหมด
2. **Organization**: Sequence แยกตามองค์กรผู้ส่ง
3. **Project**: Sequence แยกตามโครงการ
@@ -41,16 +43,16 @@ related:
### 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` | - |
| Component | Required? | Description | Database Source | Default if NULL |
| ---------------------------- | ---------------- | ------------------------ | --------------------------------------------------------- | --------------- |
| `project_id` | ✅ Yes | ID โครงการ | Derived from user context or organization | - |
| `originator_organization_id` | ✅ Yes | ID องค์กรผู้ส่ง | `correspondences.originator_id` | - |
| `recipient_organization_id` | Depends on type | ID องค์กรผู้รับหลัก (TO) | `correspondence_recipients` where `recipient_type = 'TO'` | NULL for RFA |
| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | 0 |
| `sub_type_id` | TRANSMITTAL only | ID ประเภทย่อย | `correspondence_sub_types.id` | 0 |
| `rfa_type_id` | RFA only | ID ประเภท RFA | `rfa_types.id` | 0 |
| `discipline_id` | RFA only | ID สาขางาน | `disciplines.id` | 0 |
| `current_year` | ✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - |
| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | 0 |
| `sub_type_id` | TRANSMITTAL only | ID ประเภทย่อย | `correspondence_sub_types.id` | 0 |
| `rfa_type_id` | RFA only | ID ประเภท RFA | `rfa_types.id` | 0 |
| `discipline_id` | RFA only | ID สาขางาน | `disciplines.id` | 0 |
| `current_year` | ✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - |
### Counter Key แยกตามประเภทเอกสาร (correspondence_type_id)
@@ -61,8 +63,9 @@ related:
```
**หมายเหตุ**:
- ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id`ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้
- ถ้ามีการเพิ่ม - correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ
- ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id`ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้
- ถ้ามีการเพิ่ม - correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ
#### **TRANSMITTAL**:
@@ -71,7 +74,7 @@ related:
correspondence_type_id, sub_type_id, 0, 0, current_year)
```
*หมายเหตุ*: ใช้ `sub_type_id` เพิ่มเติม
_หมายเหตุ_: ใช้ `sub_type_id` เพิ่มเติม
#### **RFA**:
@@ -80,7 +83,7 @@ related:
correspondence_type_id, 0, rfa_type_id, discipline_id, current_year)
```
*หมายเหตุ*: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER)
_หมายเหตุ_: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER)
##### วิธีการหา project_id
@@ -110,6 +113,7 @@ related:
## 3.11.3. Format Templates by Correspondence Type
> **📝 หมายเหตุสำคัญ**
>
> - Templates ด้านล่างเป็น **ตัวอย่าง** สำหรับประเภทเอกสารหลัก
> - ระบบรองรับ **ทุกประเภทเอกสาร** ที่อยู่ใน `correspondence_types` table
> - หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ
@@ -212,19 +216,19 @@ Drawing Numbering ยังไม่ได้กำหนด 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` |
| 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` |
| `{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
@@ -343,11 +347,13 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื
ระบบต้องรองรับการกำหนดเลขที่เอกสารด้วยตนเอง (manual override)
**Use Cases**:
- 1. Import เอกสารเก่าจากระบบเดิม
- 2. External documents จาก client/consultant
- 3. Correction หลังพบความผิดพลาด
**Implementation Details:**
- ตรวจสอบ duplicate ก่อน save
- Validate format ตามรูปแบบที่กำหนด
- Auto-update sequence counter ถ้าเลขที่สูงกว่า current
@@ -360,11 +366,13 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื
ระบบต้องรองรับการกำหนดเลขที่เอกสารอัตโนมัติ (auto override)
**Use Cases**:
- 1. Import เอกสารเก่าจากระบบเดิม
- 2. External documents จาก client/consultant
- 3. Correction หลังพบความผิดพลาด
**Implementation Details:**
- ตรวจสอบ duplicate ก่อน save
- Validate format ตามรูปแบบที่กำหนด
- Auto-update sequence counter ถ้าเลขที่สูงกว่า current
@@ -372,6 +380,7 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื
- ต้องมีสิทธิ์ Admin ขึ้นไปเท่านั้น
###
## 3.11.9 Audit Trail Requirements
### 3.11.9.1. Audit Logging
@@ -409,18 +418,18 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื
**SLA Targets:**
| Metric | Target | Notes |
| ---------------- | -------- | ------------------------ |
| Metric | Target | Notes |
| ---------------- | ---------- | ---------------------------- |
| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response |
| 99th percentile | ≤ 5 วินาที | รวม retry attempts |
| Normal operation | ≤ 500ms | ไม่มี retry |
| 99th percentile | ≤ 5 วินาที | รวม retry attempts |
| Normal operation | ≤ 500ms | ไม่มี retry |
### 3.11.10.2. Throughput
**Capacity Targets:**
| Load Level | Target | Notes |
| ----------- | ----------- | --------- |
| Load Level | Target | Notes |
| ----------- | ----------- | ----------- |
| Normal load | ≥ 50 req/s | ใช้งานปกติ |
| Peak load | ≥ 100 req/s | ช่วงเร่งงาน |
@@ -456,8 +465,8 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื
ระบบ**ต้อง**alert สำหรับ conditions ต่อไปนี้:
| Severity | Condition | Action |
| ---------- | ---------------------------- | ----------------- |
| 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 |
@@ -477,7 +486,6 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื
**Operations Details:** ดู [Operations Guide - Section 3](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#3-monitoring--metrics)
## 3.11.12 API Reference
เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้:
@@ -567,11 +575,13 @@ Reset counter (Super Admin only, requires approval)
**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` ก่อน
@@ -583,6 +593,7 @@ Reset counter (Super Admin only, requires approval)
**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`
@@ -595,6 +606,7 @@ Reset counter (Super Admin only, requires approval)
**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`
@@ -610,6 +622,7 @@ Reset counter (Super Admin only, requires approval)
### 3.11.15.2 Rate Limiting
**Requirements:**
- Limit ต่อ user: **10 requests/minute** (prevent abuse)
- Limit ต่อ IP: **50 requests/minute**
@@ -618,6 +631,7 @@ Reset counter (Super Admin only, requires approval)
### 3.11.15.3 Audit & Compliance
**Requirements:**
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
- Audit log **ต้องไม่**สามารถแก้ไขได้ (immutable)
@@ -631,7 +645,6 @@ Reset counter (Super Admin only, requires approval)
- [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md)
- [Data Dictionary](file:///e:/np-dms/lcbp3/specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md)
```
lock:docnum:{project_id}:{org_id}:{recip_id}:{type_id}:{sub}:{rfa}:{disc}:{year}
```
@@ -660,9 +673,11 @@ export class DocumentNumberingLockService {
}
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}`;
return (
`lock:docnum:${key.projectId}:${key.originatorOrgId}:` +
`${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` +
`${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`
);
}
}
```
@@ -717,7 +732,7 @@ export class DocumentNumberCounter {
// ใช้ TypeORM Transaction + Optimistic Lock
await this.connection.transaction(async (manager) => {
const counter = await manager.findOne(DocumentNumberCounter, {
where: counterKey
where: counterKey,
});
counter.lastNumber += 1;
@@ -951,8 +966,16 @@ 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'
'PROJECT',
'ORIGINATOR',
'RECIPIENT',
'CORR_TYPE',
'SUB_TYPE',
'RFA_TYPE',
'DISCIPLINE',
'SEQ',
'YEAR',
'REV',
];
validate(template: string, correspondenceType: string): ValidationResult {
@@ -968,13 +991,13 @@ export class TemplateValidator {
// กฎพิเศษสำหรับแต่ละประเภท
if (correspondenceType === 'RFA') {
if (!tokens.some(t => t.name === 'PROJECT')) {
if (!tokens.some((t) => t.name === 'PROJECT')) {
errors.push('RFA template ต้องมี {PROJECT}');
}
}
if (correspondenceType === 'TRANSMITTAL') {
if (!tokens.some(t => t.name === 'SUB_TYPE')) {
if (!tokens.some((t) => t.name === 'SUB_TYPE')) {
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
}
}
@@ -1045,14 +1068,14 @@ export class ConfigHistoryService {
templateBefore: oldTemplate,
templateAfter: newTemplate,
changedBy: userId,
changeReason: reason
changeReason: reason,
});
}
async rollback(configId: number, historyId: number): Promise<void> {
const history = await this.historyRepo.findOne({ where: { id: historyId }});
const history = await this.historyRepo.findOne({ where: { id: historyId } });
await this.configService.update(configId, {
template: history.templateBefore
template: history.templateBefore,
});
}
}
@@ -1108,6 +1131,7 @@ async resetCounter(
await this.counterService.reset(configId);
}
```
## 3.11.21 Audit Trail
### 3.11.21.1 การบันทึก Audit Log
@@ -1161,7 +1185,7 @@ export class DocumentNumberAuditService {
retryCount: data.retryCount ?? 0,
lockWaitMs: data.lockWaitMs,
totalDurationMs: data.totalDurationMs,
fallbackUsed: data.fallbackUsed ?? 'NONE'
fallbackUsed: data.fallbackUsed ?? 'NONE',
});
}
}
@@ -1194,7 +1218,7 @@ export class DocumentNumberingService {
retryCount,
lockWaitMs,
totalDurationMs: Date.now() - startTime,
fallbackUsed
fallbackUsed,
});
return number;
@@ -1249,7 +1273,7 @@ export class ErrorLogService {
stackTrace: error.stack,
contextData: JSON.stringify(context),
userId: context.userId,
ipAddress: context.ipAddress
ipAddress: context.ipAddress,
});
// Alert if critical
@@ -1257,7 +1281,7 @@ export class ErrorLogService {
await this.alertService.sendAlert({
severity: 'CRITICAL',
title: `Document Numbering Error: ${errorType}`,
details: error.message
details: error.message,
});
}
}
@@ -1271,11 +1295,13 @@ export class ErrorLogService {
}
}
```
## 3.11.22 Performance Requirements
### 3.11.22.1 Response Time
**Target Response Times**:
- **95th percentile**: ≤ 2 วินาที
- **99th percentile**: ≤ 5 วินาที
- **Normal operation** (ไม่มี retry): ≤ 500ms
@@ -1327,7 +1353,7 @@ 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]
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
// Usage
@@ -1357,7 +1383,7 @@ services:
backend:
image: lcbp3-backend:latest
deploy:
replicas: 3 # 3 instances
replicas: 3 # 3 instances
resources:
limits:
cpus: '1.0'
@@ -1371,7 +1397,7 @@ services:
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- "80:80"
- '80:80'
```
```nginx
@@ -1402,7 +1428,7 @@ import { ThrottlerGuard } from '@nestjs/throttler';
@Controller('document-numbering')
@UseGuards(ThrottlerGuard)
export class DocumentNumberingController {
@Throttle(10, 60) // 10 requests per 60 seconds per user
@Throttle(10, 60) // 10 requests per 60 seconds per user
@Post('generate')
async generate(@Body() dto: GenerateNumberDto) {
return await this.service.generate(dto);
@@ -1466,17 +1492,18 @@ export class HealthController {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.redis.pingCheck('redis'),
() => this.customHealthCheck()
() => this.customHealthCheck(),
]);
}
private async customHealthCheck() {
// ทดสอบ generate document number
const canGenerate = await this.testGeneration();
return { documentNumbering: { status: canGenerate ? 'up' : 'down' }};
return { documentNumbering: { status: canGenerate ? 'up' : 'down' } };
}
}
```
## 3.11.23 Monitoring & Alerting
### 3.11.23.1 Metrics Collection
@@ -1495,13 +1522,13 @@ export class DocumentNumberingMetrics {
name: 'docnum_lock_acquisition_duration_ms',
help: 'Lock acquisition time in milliseconds',
labelNames: ['project', 'type'],
buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000]
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']
labelNames: ['project', 'type', 'reason'],
});
// Generation metrics
@@ -1509,25 +1536,25 @@ export class DocumentNumberingMetrics {
name: 'docnum_generation_duration_ms',
help: 'Total document number generation time',
labelNames: ['project', 'type', 'status'],
buckets: [100, 200, 500, 1000, 2000, 5000]
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]
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)'
help: 'Redis connection status (1=up, 0=down)',
});
private dbConnectionPoolUsage = new Gauge({
name: 'docnum_db_connection_pool_usage',
help: 'Database connection pool usage percentage'
help: 'Database connection pool usage percentage',
});
}
```
@@ -1549,8 +1576,8 @@ groups:
severity: critical
component: document-numbering
annotations:
summary: "Redis is unavailable for document numbering"
description: "System is falling back to DB-only locking"
summary: 'Redis is unavailable for document numbering'
description: 'System is falling back to DB-only locking'
# CRITICAL: High lock failure rate
- alert: HighLockFailureRate
@@ -1560,8 +1587,8 @@ groups:
labels:
severity: critical
annotations:
summary: "Lock acquisition failure rate > 10%"
description: "Check Redis and database performance"
summary: 'Lock acquisition failure rate > 10%'
description: 'Check Redis and database performance'
# WARNING: Elevated lock failure rate
- alert: ElevatedLockFailureRate
@@ -1571,7 +1598,7 @@ groups:
labels:
severity: warning
annotations:
summary: "Lock acquisition failure rate > 5%"
summary: 'Lock acquisition failure rate > 5%'
# WARNING: Slow lock acquisition
- alert: SlowLockAcquisition
@@ -1583,7 +1610,7 @@ groups:
labels:
severity: warning
annotations:
summary: "P95 lock acquisition time > 1 second"
summary: 'P95 lock acquisition time > 1 second'
# WARNING: High retry count
- alert: HighRetryCount
@@ -1595,7 +1622,7 @@ groups:
labels:
severity: warning
annotations:
summary: "Retry count > 100 per hour in project {{ $labels.project }}"
summary: 'Retry count > 100 per hour in project {{ $labels.project }}'
# WARNING: Slow generation
- alert: SlowDocumentNumberGeneration
@@ -1607,7 +1634,7 @@ groups:
labels:
severity: warning
annotations:
summary: "P95 generation time > 2 seconds"
summary: 'P95 generation time > 2 seconds'
```
**AlertManager Configuration** (`alertmanager/config.yml`):
@@ -1659,9 +1686,11 @@ receivers:
"panels": [
{
"title": "Lock Acquisition Success Rate",
"targets": [{
"expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))"
}],
"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 }
},
@@ -1686,17 +1715,21 @@ receivers:
},
{
"title": "Generation Rate (per minute)",
"targets": [{
"expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60"
}],
"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"
}],
"targets": [
{
"expr": "docnum_redis_connection_status"
}
],
"type": "stat",
"gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 },
"thresholds": {
@@ -1709,9 +1742,11 @@ receivers:
},
{
"title": "Error Rate by Type",
"targets": [{
"expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))"
}],
"targets": [
{
"expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))"
}
],
"type": "graph",
"gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 }
}
@@ -58,12 +58,14 @@ ON document_number_counters (
### 3.11.15.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` ก่อน
@@ -50,14 +50,12 @@
### **2.3 Core Services:**
- **Code Hosting:** Gitea (Self-hosted on QNAP)
- Application name: git
- Service name: gitea
- Domain: `git.np-dms.work`
- หน้าที่: เป็นศูนย์กลางในการเก็บและจัดการเวอร์ชันของโค้ด (Source Code) สำหรับทุกส่วน
- **Backend / Data Platform:** NestJS
- Application name: lcbp3-backend
- Service name: backend
- Domain: `backend.np-dms.work`
@@ -65,7 +63,6 @@
- หน้าที่: จัดการโครงสร้างข้อมูล (Data Models), สร้าง API, จัดการสิทธิ์ผู้ใช้ (Roles & Permissions), และสร้าง Workflow ทั้งหมดของระบบ
- **Database:** MariaDB 10.11
- Application name: lcbp3-db
- Service name: mariadb
- Domain: `db.np-dms.work`
@@ -73,7 +70,6 @@
- Tooling: DBeaver (Community Edition), phpmyadmin สำหรับการออกแบบและจัดการฐานข้อมูล
- **Database Management:** phpMyAdmin
- Application name: lcbp3-db
- Service: phpmyadmin:5-apache
- Service name: pma
@@ -81,7 +77,6 @@
- หน้าที่: จัดการฐานข้อมูล mariadb ผ่าน Web UI
- **Frontend:** Next.js
- Application name: lcbp3-frontend
- Service name: frontend
- Domain: `lcbp3.np-dms.work`
@@ -91,7 +86,6 @@
- หน้าที่: สร้างหน้าตาเว็บแอปพลิเคชันสำหรับให้ผู้ใช้งานเข้ามาดู Dashboard, จัดการเอกสาร, และติดตามงาน โดยจะสื่อสารกับ Backend ผ่าน API
- **Workflow Automation:** n8n
- Application name: lcbp3-n8n
- Service: n8nio/n8n:latest
- Service name: n8n
@@ -99,7 +93,6 @@
- หน้าที่: จัดการ workflow ระหว่าง Backend และ Line
- **Reverse Proxy:** Nginx Proxy Manager
- Application name: lcbp3-npm
- Service: Nginx Proxy Manager (nginx-proxy-manage: latest)
- Service name: npm
@@ -112,20 +105,16 @@
### **2.4 Business Logic & Consistency (ปรับปรุง):**
- **2.4.1 Unified Workflow Engine (หลัก):**
- ระบบการเดินเอกสารทั้งหมด (Correspondence, RFA, Circulation) ต้อง ใช้ Engine กลางเดียวกัน โดยกำหนด Logic ผ่าน Workflow DSL (JSON Configuration) แทนการเขียน Hard-coded ลงในตาราง
- Workflow Versioning (เพิ่ม): ระบบต้องรองรับการกำหนด Version ของ Workflow Definition โดยเอกสารที่เริ่มกระบวนการไปแล้ว (In-progress instances) จะต้องใช้ Workflow Version เดิม จนกว่าจะสิ้นสุดกระบวนการ หรือได้รับคำสั่ง Migrate จาก Admin เพื่อป้องกันความขัดแย้งของ State
- **2.4.2 Separation of Concerns:**
- Module ต่างๆ (Correspondence, RFA, Circulation) จะเก็บเฉพาะข้อมูลของเอกสาร (Data) ส่วนสถานะและการเปลี่ยนสถานะ (State Transition) จะถูกจัดการโดย Workflow Engine
- **2.4.3 Idempotency & Locking:**
- ใช้กลไกเดิมในการป้องกันการทำรายการซ้ำ
- **2.4.4 Optimistic Locking (ใหม่):**
- ใช้ Version Column ใน Database ควบคู่กับ Redis Lock สำหรับการสร้างเลขที่เอกสาร เพื่อเป็น Safety Net ชั้นสุดท้าย
- **2.4.5** **จะไม่มีการใช้ SQL Triggers**
@@ -207,27 +196,22 @@
### **3.6. การจัดการ Workflow (Unified Workflow)**
- 3.6.1 Workflow Definition:
- Admin ต้องสามารถสร้าง/แก้ไข Workflow Rule ได้ผ่านหน้าจอ UI (DSL Editor)
- รองรับการกำหนด State, Transition, Required Role, Condition (JS Expression)
- 3.6.2 Workflow Execution:
- ระบบต้องรองรับการสร้าง Instance ของ Workflow ผูกกับเอกสาร (Polymorphic)
- รองรับการเปลี่ยนสถานะ (Action) เช่น Approve, Reject, Comment, Return
- Auto-Action: รองรับการเปลี่ยนสถานะอัตโนมัติเมื่อครบเงื่อนไข (เช่น Review ครบทุกคน)
- 3.6.3 Flexibility:
- รองรับ Parallel Review (ส่งให้หลายคนตรวจพร้อมกัน)
- รองรับ Conditional Flow (เช่น ถ้ายอดเงิน > X ให้เพิ่มผู้อนุมัติ)
- 3.6.4 Workflow การอนุมัติ:
- รองรับกระบวนการอนุมัติที่ซับซ้อนและเป็นลำดับ เช่น ส่งจาก Originator -> Organization 1 -> Organization 2 -> Organization 3 แล้วส่งผลกลับตามลำดับเดิม (โดยถ้า องกรณ์ใดใน Workflow ให้ส่งกลับ ก็สามารถส่งผลกลับตามลำดับเดิมโดยไม่ต้องรอให้ถึง องกรณืในลำดับถัดไป)
- 3.6.5 การจัดการ:
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ได้
- มีระบบแจ้งเตือน ให้ผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ทราบ เมื่อมี RFA ใหม่ หรือมีการเปลี่ยนสถานะ
- สามารถข้ามขั้นตอนได้ในกรณีพิเศษ (โดยผู้มีสิทธิ์)
@@ -259,13 +243,11 @@
### **3.10. การจัดเก็บไฟล์ (File Handling - ปรับปรุงใหญ่)**
- **3.10.1 Two-Phase Storage Strategy:**
1. **Phase 1 (Upload):** ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ `temp/` และได้รับ `temp_id`
2. **Phase 2 (Commit):** เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก `temp/` ไปยัง `permanent/{YYYY}/{MM}/` และบันทึกลง Database ภายใน Transaction เดียวกัน
3. **Cleanup:** มี Cron Job ลบไฟล์ใน `temp/` ที่ค้างเกิน 24 ชม. (Orphan Files)
- **3.10.2 Security:**
- Virus Scan (ClamAV) ก่อนย้ายเข้า Permanent
- Whitelist File Types: PDF, DWG, DOCX, XLSX, ZIP
- Max Size: 50MB
@@ -313,14 +295,12 @@
### **3.12. การจัดการ JSON Details (JSON & Performance - ปรับปรุง)**
- **3.12.1 วัตถุประสงค์**
- จัดเก็บข้อมูลแบบไดนามิกที่เฉพาะเจาะจงกับแต่ละประเภทของเอกสาร
- รองรับการขยายตัวของระบบโดยไม่ต้องเปลี่ยนแปลง database schema
- จัดการ metadata และข้อมูลประกอบสำหรับ correspondence, routing, และ workflows
- **3.12.2 โครงสร้าง JSON Schema**
ระบบต้องมี predefined JSON schemas สำหรับประเภทเอกสารต่างๆ:
- 3.12.2.1 Correspondence Types
- GENERIC: ข้อมูลพื้นฐานสำหรับเอกสารทั่วไป
- RFI: รายละเอียดคำถามและข้อมูลทางเทคนิค
@@ -337,32 +317,27 @@
- SECURITY_SCAN: ผลการตรวจสอบความปลอดภัย
- **3.12.3 Virtual Columns (ปรับปรุง):**
- สำหรับ Field ใน JSON ที่ต้องใช้ในการค้นหา (Search) หรือจัดเรียง (Sort) บ่อยๆ ต้องสร้าง Generated Column (Virtual Column) ใน Database และทำ Index ไว้ เพื่อประสิทธิภาพสูงสุด
- Schema Consistency: Field ที่ถูกกำหนดเป็น Virtual Column ห้าม เปลี่ยนแปลง Key Name หรือ Data Type ใน JSON Schema Version ถัดไป หากจำเป็นต้องเปลี่ยน ต้องมีแผนการ Re-index หรือ Migration ข้อมูลเดิมที่ชัดเจน
- **3.12.4 Validation Rules**
- ต้องมี JSON schema validation สำหรับแต่ละประเภท
- ต้องรองรับ versioning ของ schema
- ต้องมี default values สำหรับ field ที่ไม่บังคับ
- ต้องตรวจสอบ data types และ format ให้ถูกต้อง
- **3.12.5 Performance Requirements**
- JSON field ต้องมีขนาดไม่เกิน 50KB
- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย
- ต้องมี compression สำหรับ JSON ขนาดใหญ่
- **3.12.6 Security Requirements**
- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks
- ต้อง validate JSON structure ก่อนบันทึก
- ต้อง encrypt sensitive data ใน JSON fields
- 3.12.7 JSON Schema Migration Strategy (เพิ่มเติม)
สำหรับ Schema Breaking Changes:
- Phase 1 - Add New Column
ALTER TABLE correspondence_revisions
ADD COLUMN ref_project_id_v2 INT GENERATED ALWAYS AS
@@ -374,7 +349,6 @@
- Phase 3 - Switch Application Code
- Deploy code ที่ใช้ path ใหม่
- Phase 4 - Remove Old Column
- หลังจาก verify แล้วว่าไม่มี error
- Drop old virtual column
@@ -537,9 +511,7 @@
### 6.1. การบันทึกการกระทำ (Audit Log)
- ทุกการกระทำที่สำคัญของผู้ใช้ (สร้าง, แก้ไข, ลบ, ส่ง) จะถูกบันทึกไว้ใน audit_logs เพื่อการตรวจสอบย้อนหลัง
- 6.1.1 ขอบเขตการบันทึก Audit Log:
- ทุกการสร้าง/แก้ไข/ลบ ข้อมูลสำคัญ (correspondences, RFAs, drawings, users, permissions)
- ทุกการเข้าถึงข้อมูล sensitive (user data, financial information)
- ทุกการเปลี่ยนสถานะ workflow (status transitions)
@@ -8,13 +8,16 @@
## 📌 1. Objectives
# 📌 Section 1: Objectives (วัตถุประสงค์)
---
title: 'Objectives'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
สร้างเว็บแอปพลิเคชันสำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System - DMS) แบบครบวงจร ที่เน้นความปลอดภัยสูงสุด ความถูกต้องของข้อมูล (Data Integrity) และรองรับการขยายตัวในอนาคต (Scalability) โดยแก้ไขปัญหา Race Condition และเพิ่มความเสถียรในการจัดการไฟล์ และใช้ Unified Workflow Engine ในการจัดการกระบวนการอนุมัติทั้งหมดเพื่อความยืดหยุ่น
@@ -29,18 +32,22 @@ related: -
## 🛠️ 2. System Architecture
# 🛠️ Section 2: System Architecture (สถาปัตยกรรมและเทคโนโลยี)
---
title: 'System Architecture'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: - specs/01-objectives.md
---
ชื่อกำหนด สถาปัตยกรรมแบบ Headless/API-First ที่ทันสมัย ทำงานทั้งหมดบน QNAP Server ผ่าน Container Station เพื่อความสะดวกในการจัดการและบำรุงรักษา
### 2.1 Infrastructure & Environment
- Domain: `np-dms.work`, `www.np-dms.work`
- IP: 159.192.126.103
- Server: QNAP (Model: TS-473A, RAM: 32GB, CPU: AMD Ryzen V1500B)
@@ -50,6 +57,7 @@ related: - specs/01-objectives.md
- ข้อจำกัด: ไม่สามารถใช้ .env ในการกำหนดตัวแปรภายนอกได้ ต้องกำหนดใน docker-compose.yml เท่านั้น
### 2.2 Configuration Management
- ใช้ docker-compose.yml สำหรับ environment variables ตามข้อจำกัดของ QNAP
- Secrets Management: ใช้ docker-compose.override.yml (gitignore) สำหรับ secret injection, Docker secrets หรือ Hashicorp Vault, encrypted env vars
- Development environment ยังใช้ .env ได้ แต่ต้องไม่ commit เข้า version control
@@ -58,6 +66,7 @@ related: - specs/01-objectives.md
- Docker Network: lcbp3
### 2.3 Core Services
- Code Hosting: Gitea (`git.np-dms.work`)
- Backend / Data Platform: NestJS (`backend.np-dms.work`)
- Database: MariaDB 10.11 (`db.np-dms.work`)
@@ -69,6 +78,7 @@ related: - specs/01-objectives.md
- Cache: Redis
### 2.4 Business Logic & Consistency
- Unified Workflow Engine (central) with DSL JSON configuration
- Versioning of workflow definitions, optimistic locking with Redis lock for document numbering
- No SQL triggers; all business logic in NestJS services
@@ -78,14 +88,18 @@ related: - specs/01-objectives.md
### 3.1 Project Management
# 3.1 Project Management (การจัดการโครงสร้างโครงการและองค์กร)
---
title: "Functional Requirements: Project Management"
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
- 3.1.1. โครงการ (Projects): ระบบต้องสามารถจัดการเอกสารภายในหลายโครงการได้ (ปัจจุบันมี 4 โครงการ และจะเพิ่มขึ้นในอนาคต)
- 3.1.2. สัญญา (Contracts): ระบบต้องสามารถจัดการเอกสารภายในแต่ละสัญญาได้ ในแต่ละโครงการ มีได้หลายสัญญา หรืออย่างน้อย 1 สัญญา
- 3.1.3. องค์กร (Organizations):
@@ -95,14 +109,18 @@ related: -
### 3.2 Correspondence Management
# 3.2 การจัดการเอกสารโต้ตอบ (Correspondence Management)
---
title: 'Functional Requirements: Correspondence Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
- 3.2.1. วัตถุประสงค์: เอกสารโต้ตอบระหว่างองค์กรภายในและภายนอกโครงการ, รองรับ To และ CC หลายองค์กร
- 3.2.2. ประเภทเอกสาร: PDF, ZIP; Types include Letter, Email, RFI, RFA (with revisions)
- 3.2.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์สร้าง Draft, Submit requires Admin approval
@@ -112,14 +130,18 @@ related: -
### 3.3 RFA Management
# 3.3 RFA Management (การจัดการเอกสาขออนุมัติ)
---
title: 'Functional Requirements: RFA Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
- 3.3.1. วัตถุประสงค์: เอกสารขออนุมัติภายในโครงการ
- 3.3.2. ประเภทเอกสาร: PDF, รองรับหลาย revision และหลายประเภท RFA
- 3.3.3. การสร้างเอกสาร: Draft creation by Document Control, Submit requires Admin
@@ -129,14 +151,18 @@ related: -
### 3.4 Contract Drawing Management
# 3.4 Contract Drawing Management (การจัดการแบบคู่สัญญา)
---
title: 'Functional Requirements: Contract Drawing Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
- 3.4.1. วัตถุประสงค์: ใช้เพื่ออ้างอิงและตรวจสอบ
- 3.4.2. ประเภทเอกสาร: PDF
- 3.4.3. การสร้างเอกสาร: ผู้มีสิทธิ์สร้างและแก้ไข
@@ -145,14 +171,18 @@ related: -
### 3.5 Shop Drawing Management
# 3.5 Shop Drawing Management (การจัดการแบบก่อสร้าง)
---
title: 'Functional Requirements: Shop Drawing Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
- 3.5.1. วัตถุประสงค์: ใช้ในการตรวจสอบและจัดส่งด้วย RFA
- 3.5.2. ประเภทเอกสาร: PDF, DWG, ZIP
- 3.5.3. การสร้างเอกสาร: ผู้มีสิทธิ์สร้าง/แก้ไข, Draft visibility control
@@ -161,14 +191,18 @@ related: -
### 3.6 Unified Workflow Management
# 3.6 Unified Workflow Management (การจัดการ Workflow)
---
title: 'Functional Requirements: Unified Workflow Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related: -
---
- 3.6.1 Workflow Definition: Admin can create/edit rules via UI DSL Editor, define State, Transition, Role, Condition
- 3.6.2 Workflow Execution: Create instances polymorphic to documents, support actions Approve, Reject, Comment, Return, auto-actions
- 3.6.3 Flexibility: Parallel Review, Conditional Flow
@@ -176,101 +210,134 @@ related: -
- 3.6.5 Management: Deadline setting, notifications, step skipping, backtrack
# 3.7 Transmittals Management (การจัดการเอกสารนำส่ง)
---
title: 'Functional Requirements: Transmittals Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
---
## 3.7.1. วัตถุประสงค์:
- เอกสารนำส่ง ใช้สำหรับ นำส่ง Request for Approval (RFAS) หลายฉบับ ไปยังองค์กรอื่น
## 3.7.2. ประเภทเอกสาร:
- ไฟล์ PDF
## 3.7.3. การสร้างเอกสาร:
- ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
## 3.7.4. การอ้างอิงและจัดกลุ่ม:
- เอกสารนำส่ง เป็นส่วนหนึ่งใน Correspondence
# 3.8 Circulation Sheet Management (การจัดการใบเวียนเอกสาร)
---
title: 'Functional Requirements: Circulation Sheet Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
---
## 3.8.1. วัตถุประสงค์:
- การสื่อสาร เอกสาร (Correspondence) ทุกฉบับ จะมีใบเวียนเอกสารเพื่อควบคุมและมอบหมายงานภายในองค์กร (สามารถดูและแก้ไขได้เฉพาะคนในองค์กร)
## 3.8.2. ประเภทเอกสาร:
- ไฟล์ PDF
## 3.8.3. การสร้างเอกสาร:
- ผู้ใช้ที่มีสิทธิ์ในองค์กรนั้น สามารถสร้างและแก้ไขได้
## 3.8.4. การอ้างอิงและจัดกลุ่ม:
- การระบุผู้รับผิดชอบ:
- ผู้รับผิดชอบหลัก (Main): มีได้หลายคน
- ผู้ร่วมปฏิบัติงาน (Action): มีได้หลายคน
- ผู้ที่ต้องรับทราบ (Information): มีได้หลายคน
## 3.8.5. การติดตามงาน:
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบประเภท Main และ Action ได้
- มีระบบแจ้งเตือนเมื่อมี Circulation ใหม่ และแจ้งเตือนล่วงหน้าก่อนถึงวันแล้วเสร็จ
- สามารถปิด Circulation ได้เมื่อดำเนินการตอบกลับไปยังองค์กรผู้ส่ง (Originator) แล้ว หรือ รับทราบแล้ว (For Information)
# 3.9 Logs Management (ประวัติการแก้ไข)
---
title: 'Functional Requirements: Logs Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
---
## 3.9.1. วัตถุประสงค์:
- เพื่อ บันทึกการกระทำ CRUD ของเอกสารทั้งหมด รวมถึงการ เข้าใช้งาน ของ users
- admin สามารถดูประวัติการแก้ไขของเอกสารทั้งหมด พร้อม จัดทำรายงายตามข้อกำหนดที่ ต้องการได้
# 3.10 File Handling Management (การจัดการไฟล์)
---
title: 'Functional Requirements: File Handling Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
---
## 3.10.1 Two-Phase Storage Strategy:
1. Phase 1 (Upload): ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ temp/ และได้รับ temp_id
2. Phase 2 (Commit): เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก temp/ ไปยัง permanent/{YYYY}/{MM}/ และบันทึกลง Database ภายใน Transaction เดียวกัน
3. Cleanup: มี Cron Job ลบไฟล์ใน temp/ ที่ค้างเกิน 24 ชม. (Orphan Files)
## 3.10.2 Security:
- Virus Scan (ClamAV) ก่อนย้ายเข้า permanent
- Whitelist File Types: PDF, DWG, DOCX, XLSX, ZIP
- Max Size: 50MB
- Access Control: ตรวจสอบสิทธิ์ผ่าน Junction Table ก่อนให้ Download Link
## 3.10.3 ความปลอดภัยของการจัดเก็บไฟล์:
- ต้องมีการ scan virus สำหรับไฟล์ที่อัปโหลดทั้งหมด โดยใช้ ClamAV หรือบริการ third-party
- จำกัดประเภทไฟล์ที่อนุญาต: PDF, DWG, DOCX, XLSX, ZIP (ต้องระบุรายการที่ชัดเจน)
- ขนาดไฟล์สูงสุด: 50MB ต่อไฟล์
@@ -280,64 +347,80 @@ related:
- ต้องบันทึก audit log ทุกครั้งที่มีการดาวน์โหลดไฟล์สำคัญ
# 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
- 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` | - |
| 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 (ปัจจุบัน) | - |
| `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`
_หมายเหตุ_: ไม่ใช้ `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` เพิ่มเติม
_หมายเหตุ_: ใช้ `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)
_หมายเหตุ_: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER)
### วิธีการหา project_id
1. **User Context** (แนะนำ):
- เมื่อ User สร้างเอกสาร UI จะให้เลือก Project/Contract ก่อน
- ใช้ `project_id` จาก Context ที่เลือก
@@ -350,97 +433,135 @@ related:
- ตรวจสอบว่า 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
### 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**
> **⚠️ 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)`
**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**
> **⚠️ 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)`
**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 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)`
**Counter Key**: `(project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)`
---
## 3.11.4. Security & Data Integrity Requirements
### 3.11.4.1. Concurrency Control
**Requirements:**
- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน
- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์
- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary
- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน
### 3.11.4.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. Validation Rules
- ต้องมี JSON schema validation สำหรับแต่ละประเภท
- ต้องรองรับ versioning ของ schema
- ต้องมี default values สำหรับ field ที่ไม่บังคับ
- ต้องตรวจสอบ data types และ format ให้ถูกต้อง
---
## 3.11.6. Performance Requirements
- JSON field ต้องมีขนาดไม่เกิน 50KB
- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย
- ต้องมี compression สำหรับ JSON ขนาดใหญ่
---
## 3.11.7. Security Requirements
- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks
- ต้อง validate JSON structure ก่อนบันทึก
- ต้อง encrypt sensitive data ใน JSON fields
---
## 3.11.8. JSON Schema Migration Strategy
- สำหรับ Schema Breaking Changes:
- Phase 1 - Add New Column
ALTER TABLE correspondence_revisions
@@ -457,24 +578,34 @@ related:
- สำหรับ Non-Breaking Changes
- เพิ่ม optional field ใน schema
- Old records ที่ไม่มี field = ใช้ default value
---
# 3.12 JSON Details Management (การจัดการ JSON Details)
---
title: 'Functional Requirements: JSON Details Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
---
## 3.12.1 วัตถุประสงค์
- จัดเก็บข้อมูลแบบไดนามิกที่เฉพาะเจาะจงกับแต่ละประเภทของเอกสาร
- รองรับการขยายตัวของระบบโดยไม่ต้องเปลี่ยนแปลง database schema
- จัดการ metadata และข้อมูลประกอบสำหรับ correspondence, routing, และ workflows
## 3.12.2 โครงสร้าง JSON Schema
- ระบบต้องมี predefined JSON schemas สำหรับประเภทเอกสารต่างๆ:
- 3.12.2.1 Correspondence Types
- GENERIC: ข้อมูลพื้นฐานสำหรับเอกสารทั่วไป
@@ -490,27 +621,37 @@ related:
- 3.12.2.3 Audit Types
- AUDIT_LOG: ข้อมูลการตรวจสอบ
- SECURITY_SCAN: ผลการตรวจสอบความปลอดภัย
## 3.12.3 Virtual Columns (ปรับปรุง)
- สำหรับ Field ใน JSON ที่ต้องใช้ในการค้นหา (Search) หรือจัดเรียง (Sort) บ่อยๆ ต้องสร้าง Generated Column (Virtual Column) ใน Database และทำ Index ไว้ เพื่อประสิทธิภาพสูงสุด
- Schema Consistency: Field ที่ถูกกำหนดเป็น Virtual Column ห้าม เปลี่ยนแปลง Key Name หรือ Data Type ใน JSON Schema Version ถัดไป หากจำเป็นต้องเปลี่ยน ต้องมีแผนการ Re-index หรือ Migration ข้อมูลเดิมที่ชัดเจน
## 3.12.4 Validation Rules
- ต้องมี JSON schema validation สำหรับแต่ละประเภท
- ต้องรองรับ versioning ของ schema
- ต้องมี default values สำหรับ field ที่ไม่บังคับ
- ต้องตรวจสอบ data types และ format ให้ถูกต้อง
## 3.12.5 Performance Requirements
- JSON field ต้องมีขนาดไม่เกิน 50KB
- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย
- ต้องมี compression สำหรับ JSON ขนาดใหญ่
## 3.12.6 Security Requirements
- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks
- ต้อง validate JSON structure ก่อนบันทึก
- ต้อง encrypt sensitive data ใน JSON fields
---
## 📂 4. NonFunctional Requirements
# 4.1 Access Control
# 🔐 Section 4: Access Control (ข้อกำหนดด้านสิทธิ์และการเข้าถึง)
## 4.1. Overview:
@@ -567,14 +708,14 @@ related:
### **4.7. Master Data Management**
| Master Data | Manager | Scope |
| :-------------------------------------- | :------------------------------ | :------------------------------ |
| Document Type (Correspondence, RFA) | **Superadmin** | Global |
| Document Status (Draft, Approved, etc.) | **Superadmin** | Global |
| Master Data | Manager | Scope |
| :-------------------------------------- | :------------------------------ | :--------------------------------- |
| Document Type (Correspondence, RFA) | **Superadmin** | Global |
| Document Status (Draft, Approved, etc.) | **Superadmin** | Global |
| Shop Drawing Category | **Project Manager** | Project (สร้างใหม่ได้ภายในโครงการ) |
| Tags | **Org Admin / Project Manager** | Organization / Project |
| Custom Roles | **Superadmin / Org Admin** | Global / Organization |
| Document Numbering Formats | **Superadmin / Admin** | Global / Organization |
| Tags | **Org Admin / Project Manager** | Organization / Project |
| Custom Roles | **Superadmin / Org Admin** | Global / Organization |
| Document Numbering Formats | **Superadmin / Admin** | Global / Organization |
## 4.8. การบันทึกการกระทำ (Audit Log)
@@ -616,7 +757,6 @@ related:
- มีการใช้ Caching กับข้อมูลที่เรียกใช้บ่อย และใช้ Pagination ในตารางข้อมูลเพื่อจัดการข้อมูลจำนวนมาก
- ตัวชี้วัดประสิทธิภาพ:
- **API Response Time:** < 200ms (90th percentile) สำหรับ operation ทั่วไป
- **Search Query Performance:** < 500ms สำหรับการค้นหาขั้นสูง
- **File Upload Performance:** < 30 seconds สำหรับไฟล์ขนาด 50MB
@@ -626,7 +766,6 @@ related:
- **Application Startup Time:** < 30 seconds
- Caching Strategy:
- **Master Data Cache:** Roles, Permissions, Organizations, Project metadata (TTL: 1 hour)
- **User Session Cache:** User permissions และ profile data (TTL: 30 minutes)
- **Search Result Cache:** Frequently searched queries (TTL: 15 minutes)
@@ -716,7 +855,7 @@ related:
## 4.15. กลยุทธ์การแจ้งเตือน (Notification Strategy - ปรับปรุง)
- ระบบจะส่งการแจ้งเตือน (ผ่าน Email หรือ Line [cite: 2.7]) เมื่อมีการกระทำที่สำคัญ** ดังนี้:
- ระบบจะส่งการแจ้งเตือน (ผ่าน Email หรือ Line [cite: 2.7]) เมื่อมีการกระทำที่สำคัญ\*\* ดังนี้:
1. เมื่อมีเอกสารใหม่ (Correspondence, RFA) ถูกส่งมาถึงองค์กรณ์ของเรา
2. เมื่อมีใบเวียน (Circulation) ใหม่ มอบหมายงานมาที่เรา
3. (ทางเลือก) เมื่อเอกสารที่เราส่งไป ถูกดำเนินการ (เช่น อนุมัติ/ปฏิเสธ)
@@ -773,10 +912,12 @@ related:
- ต้องมี default fallback values
- ต้องบันทึก error logs สำหรับ validation failures
# 5. UI/UX Guidelines
# 👥 Section 5: UI/UX Requirements (ข้อกำหนดด้านผู้ใช้งาน)
---
title: 'UI/UX Requirements'
version: 1.5.0
status: first-draft
@@ -786,6 +927,7 @@ related:
- specs/02-architecture/data-model.md#correspondence
- specs/03-implementation/backend-guidelines.md#correspondencemodule
---
## 5.1. Layout หลัก
@@ -860,6 +1002,7 @@ related:
- 5.13.3 Performance: ต้องรองรับการส่งข้อมูลแบบ Streaming (Range Requests) เพื่อให้เปิดดูไฟล์ขนาดใหญ่ (เช่น แบบแปลน 50MB+) ได้รวดเร็วโดยไม่ต้องรอโหลดเสร็จทั้งไฟล์
## 🧪 6. Testing Requirements
## 6.1 Unit Testing
- ต้องมี unit tests สำหรับ business logic ทั้งหมด
@@ -907,5 +1050,7 @@ related:
- **Permission Test:** ทดสอบ CASL Integration ทั้งฝั่ง Backend และ Frontend ให้ตรงกัน
---
*Version History*
_Version History_
- **v1.5.1** 20251204 Consolidated requirement specifications into single document.
@@ -487,13 +487,11 @@ Backend (NestJS) ควรเป็น **Stateless** (ไม่เก็บส
1. User สร้างเอกสาร → เลือก routing template
2. System สร้าง routing instances ตาม template
3. สำหรับแต่ละ routing step:
- กำหนด due date (จาก expected_days)
- ส่ง notification ไปยังองค์กรผู้รับ
- อัพเดทสถานะเป็น SENT
4. เมื่อองค์กรผู้รับดำเนินการ:
- อัพเดทสถานะเป็น ACTIONED/FORWARDED/REPLIED
- บันทึก processed_by และ processed_at
- ส่ง notification ไปยังขั้นตอนต่อไป (ถ้ามี)
@@ -707,27 +705,23 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
สำหรับ Next.js App Router เราจะใช้ State Management แบบ Simplified โดยแบ่งเป็น 3 ระดับหลัก:
- 4.10.ๅ. **Server State (สถานะข้อมูลจากเซิร์ฟเวอร์)**
- **เครื่องมือ:** **TanStack Query (React Query)**
- **ใช้เมื่อ:** จัดการข้อมูลที่ดึงมาจาก NestJS API ทั้งหมด
- **ครอบคลุม:** รายการ correspondences, rfas, drawings, users, permissions
- **ประโยชน์:** จัดการ Caching, Re-fetching, Background Sync อัตโนมัติ
- 4.10.2. **Form State (สถานะของฟอร์ม):**
- **เครื่องมือ:** **React Hook Form** + **Zod** (สำหรับ validation)
- **ใช้เมื่อ:** จัดการฟอร์มที่ซับซ้อนทั้งหมด
- **ครอบคลุม:** ฟอร์มสร้าง/แก้ไข RFA, Correspondence, Circulation
- **รวมฟีเจอร์:** Auto-save drafts ลง LocalStorage
- 4.10.3. **UI State (สถานะ UI ชั่วคราว):**
- **เครื่องมือ:** **useState**, **useReducer** (ใน Component)
- **ใช้เมื่อ:** จัดการสถานะเฉพาะ Component
- **ครอบคลุม:** Modal เปิด/ปิด, Dropdown state, Loading states
- **ยกเลิกการใช้:**
- ❌ Zustand (ไม่จำเป็น เนื่องจากใช้ React Query และ React Hook Form)
- ❌ Context API สำหรับ Server State (ใช้ React Query แทน)
@@ -48,14 +48,14 @@
### **2.4 ข้อตกลงในการตั้งชื่อ (Naming Conventions)**
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
| :-------------------- | :----------------- | :--------------------------------- |
| Classes | PascalCase | UserService |
| Property | snake_case | user_id |
| Variables & Functions | camelCase | getUserInfo |
| Files & Folders | kebab-case | user-service.ts |
| Environment Variables | UPPERCASE | DATABASE_URL |
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
| :----------------------- | :------------------ | :--------------------------------- |
| Classes | PascalCase | UserService |
| Property | snake_case | user_id |
| Variables & Functions | camelCase | getUserInfo |
| Files & Folders | kebab-case | user-service.ts |
| Environment Variables | UPPERCASE | DATABASE_URL |
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
ใช้คำเต็ม — ไม่ใช้อักษรย่อ — ยกเว้นคำมาตรฐาน (เช่น API, URL, req, res, err, ctx)
@@ -504,34 +504,34 @@ export class IdempotencyInterceptor implements NestInterceptor {
### **3.13 เเทคโนโลยีที่ใช้ (Technology Stack)**
| ส่วน | Library/Tool | หมายเหตุ |
| ----------------------- | ---------------------------------------------------- | -------------------------------------- |
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
| **Database** | `MariaDB 10.11` | ฐานข้อมูลหลัก |
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
| ส่วน | Library/Tool | หมายเหตุ |
| ----------------------- | ---------------------------------------------------- | -------------------------------------------- |
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
| **Database** | `MariaDB 10.11` | ฐานข้อมูลหลัก |
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
| **Scheduling** | `@nestjs/schedule` | 📬สำหรับ Cron Jobs (เช่น แจ้งเตือน Deadline) |
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
| **File Processing** | `clamscan` | 🦠 Virus scanning |
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
| **Compression** | `compression` | 📦 JSON compression |
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
| **File Processing** | `clamscan` | 🦠 Virus scanning |
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
| **Compression** | `compression` | 📦 JSON compression |
### **3.14 Security Testing:**
@@ -587,13 +587,11 @@ Backend (NestJS) ควรเป็น **Stateless** (ไม่เก็บส
1. User สร้างเอกสาร → เลือก routing template
2. System สร้าง routing instances ตาม template
3. สำหรับแต่ละ routing step:
- กำหนด due date (จาก expected_days)
- ส่ง notification ไปยังองค์กรผู้รับ
- อัพเดทสถานะเป็น SENT
4. เมื่อองค์กรผู้รับดำเนินการ:
- อัพเดทสถานะเป็น ACTIONED/FORWARDED/REPLIED
- บันทึก processed_by และ processed_at
- ส่ง notification ไปยังขั้นตอนต่อไป (ถ้ามี)
@@ -730,24 +728,20 @@ export function DynamicForm({ schemaName, onSubmit }: DynamicFormProps) {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{Object.entries(schema.schema_definition.properties).map(
([key, prop]: [string, any]) => (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
<FormLabel>{prop.title || key}</FormLabel>
<FormControl>
{renderFieldByType(prop.type, field)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)
)}
{Object.entries(schema.schema_definition.properties).map(([key, prop]: [string, any]) => (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
<FormLabel>{prop.title || key}</FormLabel>
<FormControl>{renderFieldByType(prop.type, field)}</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<Button type="submit">บันทึก</Button>
</form>
</Form>
@@ -808,9 +802,7 @@ export function ResponsiveTable({ data }: { data: Correspondence[] }) {
<Card key={item.id}>
<CardContent className="pt-6">
<div className="space-y-2">
<div className="text-sm text-muted-foreground">
เลขที่เอกสาร
</div>
<div className="text-sm text-muted-foreground">เลขที่เอกสาร</div>
<div className="font-medium">{item.doc_number}</div>
<div className="text-sm text-muted-foreground">เรื่อง</div>
<div>{item.title}</div>
@@ -933,10 +925,7 @@ export function useCreateCorrespondence() {
await queryClient.cancelQueries({ queryKey: ['correspondences'] });
const previous = queryClient.getQueryData(['correspondences']);
queryClient.setQueryData(['correspondences'], (old: any) => [
...old,
newCorrespondence,
]);
queryClient.setQueryData(['correspondences'], (old: any) => [...old, newCorrespondence]);
return { previous };
},
@@ -1100,24 +1089,17 @@ export function FileUploadZone({
className={`
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
transition-colors
${
isDragActive
? 'border-primary bg-primary/5'
: 'border-muted-foreground/25'
}
${isDragActive ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
hover:border-primary hover:bg-primary/5
`}
>
<input {...getInputProps()} />
<Upload className="mx-auto h-12 w-12 text-muted-foreground" />
<p className="mt-2 text-sm text-muted-foreground">
{isDragActive
? 'วางไฟล์ที่นี่...'
: 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'}
{isDragActive ? 'วางไฟล์ที่นี่...' : 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'}
</p>
<p className="mt-1 text-xs text-muted-foreground">
รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์,{' '}
{maxSize / 1024 / 1024}MB/ไฟล์)
รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์, {maxSize / 1024 / 1024}MB/ไฟล์)
</p>
</div>
);
@@ -1198,15 +1180,15 @@ updateRFA(@Param('id') id: string) {
## 🔗 **7. แนวทางการบูรณาการ Full Stack (Full Stack Integration Guidelines)**
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
| :----------------------- | :------------------------- | :---------------------------- | :------------------------------------- |
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
| :------------------------- | :------------------------- | :----------------------------- | :------------------------------------- |
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
## 🗂️ **8. ข้อตกลงเฉพาะสำหรับ DMS (LCBP3-DMS)**
@@ -1216,17 +1198,17 @@ updateRFA(@Param('id') id: string) {
บันทึกการดำเนินการ CRUD และการจับคู่ทั้งหมดลงในตาราง audit_logs
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
| :----------- | :------------- | :----------------------------------------------- |
| audit_id | BIGINT | Primary Key |
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
| created_at | TIMESTAMP | Timestamp (UTC) |
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
| :------------ | :------------- | :----------------------------------------------- |
| audit_id | BIGINT | Primary Key |
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
| created_at | TIMESTAMP | Timestamp (UTC) |
### 📂**8.2 การจัดการไฟล์ (File Handling)**
@@ -1,4 +1,5 @@
# “Phase 6A + Technical Design Document : Workflow DSL (Mini-Language)”**
# “Phase 6A + Technical Design Document : Workflow DSL (Mini-Language)”\*\*
ออกแบบสำหรับระบบ Workflow Engine กลางของโครงการ
**ไม่มีโค้ดผูกกับ Framework** เพื่อให้สามารถนำไป Implement ใน NestJS หรือ Microservice ใด ๆ ได้
@@ -10,20 +11,19 @@
ใน Phase นี้ จะเริ่มสร้าง “Workflow DSL (Domain-Specific Language)” สำหรับนิยามกฎการเดินงาน (Workflow Transition Rules) ให้สามารถ:
* แยก **Business Workflow Logic** ออกจาก Source Code
* แก้ไขกฎ Workflow ได้โดย **ไม่ต้องแก้โค้ดและไม่ต้อง Deploy ใหม่**
* รองรับ Document หลายประเภท เช่น
- แยก **Business Workflow Logic** ออกจาก Source Code
- แก้ไขกฎ Workflow ได้โดย **ไม่ต้องแก้โค้ดและไม่ต้อง Deploy ใหม่**
- รองรับ Document หลายประเภท เช่น
- Correspondence
- RFA
- Internal Circulation
- Document Transmittal
* Correspondence
* RFA
* Internal Circulation
* Document Transmittal
* รองรับ Multi-step routing, skip, reject, rollback, parallel assignments
* สามารถนำไปใช้งานทั้งใน
* Backend (NestJS)
* Frontend (UI Driven)
* External Microservices
- รองรับ Multi-step routing, skip, reject, rollback, parallel assignments
- สามารถนำไปใช้งานทั้งใน
- Backend (NestJS)
- Frontend (UI Driven)
- External Microservices
---
@@ -35,12 +35,12 @@
### 🧩 Output ของ Phase 6A
* DSL Specification (Grammar)
* JSON Schema for Workflow Definition
* Workflow Rule Interpreter (Parser + Executor)
* Validation Engine (Compile-time and Runtime)
* Storage (DB Table / Registry)
* Execution API:
- DSL Specification (Grammar)
- JSON Schema for Workflow Definition
- Workflow Rule Interpreter (Parser + Executor)
- Validation Engine (Compile-time and Runtime)
- Storage (DB Table / Registry)
- Execution API:
| Action | Description |
| -------------------------------- | ------------------------------- |
@@ -59,22 +59,22 @@
#### Functional Requirements
* นิยาม Workflow เป็นภาษาคล้าย State Machine
* แต่ละเอกสารมี **State, Actions, Entry/Exit Events**
* สามารถมี:
* Required approvals
* Conditional transition
* Auto-transition
* Parallel approval
* Return/rollback
- นิยาม Workflow เป็นภาษาคล้าย State Machine
- แต่ละเอกสารมี **State, Actions, Entry/Exit Events**
- สามารถมี:
- Required approvals
- Conditional transition
- Auto-transition
- Parallel approval
- Return/rollback
####
* Running time: < 20ms ต่อคำสั่ง
* Hot reload ไม่ต้อง Compile ใหม่ทั้ง Backend
* DSL ต้อง Debug ได้ง่าย
* ต้อง Versioned
* ต้องรองรับ Audit 100%
- Running time: < 20ms ต่อคำสั่ง
- Hot reload ไม่ต้อง Compile ใหม่ทั้ง Backend
- DSL ต้อง Debug ได้ง่าย
- ต้อง Versioned
- ต้องรองรับ Audit 100%
---
@@ -122,12 +122,8 @@ states:
"transitions": {
"SUBMIT": {
"to": "IN_REVIEW",
"requirements": [
{ "role": "ENGINEER" }
],
"events": [
{ "type": "notify", "target": "reviewer" }
]
"requirements": [{ "role": "ENGINEER" }],
"events": [{ "type": "notify", "target": "reviewer" }]
}
}
},
@@ -136,9 +132,7 @@ states:
"APPROVE": { "to": "APPROVED" },
"REJECT": {
"to": "DRAFT",
"events": [
{ "type": "notify", "target": "creator" }
]
"events": [{ "type": "notify", "target": "creator" }]
}
}
},
@@ -162,14 +156,14 @@ version = "version" ":" number ;
states = "states:" state_list ;
state_list = { state } ;
state = "- name:" identifier
[ "initial:" boolean ]
state = "- name:" identifier
[ "initial:" boolean ]
[ "terminal:" boolean ]
[ "on:" transition_list ] ;
transition_list = { transition } ;
transition = action ":"
transition = action ":"
indent "to:" identifier
[ indent "require:" requirements ]
[ indent "events:" event_list ] ;
@@ -186,23 +180,23 @@ event = "- notify:" identifier ;
#### 5.1 State Rules
* ต้องมีอย่างน้อย 1 state ที่ `initial: true`
* หาก `terminal: true` → ต้องไม่มี transition ต่อไป
- ต้องมีอย่างน้อย 1 state ที่ `initial: true`
- หาก `terminal: true` → ต้องไม่มี transition ต่อไป
#### 5.2 Transition Rules
ตรวจสอบว่า:
* `to` ชี้ไปยัง state ที่มีอยู่
* `require.role` ต้องเป็น role ที่ระบบรู้จัก
* Action name ต้องเป็น **UPPER_CASE**
- `to` ชี้ไปยัง state ที่มีอยู่
- `require.role` ต้องเป็น role ที่ระบบรู้จัก
- Action name ต้องเป็น **UPPER_CASE**
#### 5.3 Version Safety
* ทุกชุด Workflow DSL ต้องขึ้นกับ version
* แก้ไขต้องสร้าง version ใหม่
* ไม่ overwrite version เก่า
* “Document ที่กำลังอยู่ใน step เดิมยังต้องใช้กฎเดิมได้”
- ทุกชุด Workflow DSL ต้องขึ้นกับ version
- แก้ไขต้องสร้าง version ใหม่
- ไม่ overwrite version เก่า
- “Document ที่กำลังอยู่ใน step เดิมยังต้องใช้กฎเดิมได้”
---
@@ -240,14 +234,13 @@ interface WorkflowContext {
```ts
class WorkflowEngine {
load(dsl: string | object): CompiledWorkflow
load(dsl: string | object): CompiledWorkflow;
compile(dsl: string | object): CompiledWorkflow
compile(dsl: string | object): CompiledWorkflow;
evaluate(state: string, action: string, context: WorkflowContext): EvalResult
evaluate(state: string, action: string, context: WorkflowContext): EvalResult;
getAvailableActions(state: string, context: WorkflowContext): string[]
getAvailableActions(state: string, context: WorkflowContext): string[];
}
```
@@ -328,21 +321,21 @@ flowchart TD
#### Unit Tests
* Parse DSL → JSON
* Invalid syntax → throw error
* Invalid transitions → throw error
- Parse DSL → JSON
- Invalid syntax → throw error
- Invalid transitions → throw error
#### Integration Tests
* Evaluate() ผ่าน 20+ cases
* RFA ย้อนกลับ
* Approve chain
* Parallel review
- Evaluate() ผ่าน 20+ cases
- RFA ย้อนกลับ
- Approve chain
- Parallel review
#### Load Tests
* 1,000 documents running workflow
* Evaluate < 20ms ต่อ action
- 1,000 documents running workflow
- Evaluate < 20ms ต่อ action
---
@@ -350,9 +343,9 @@ flowchart TD
#### Hot Reload Options
* DSL stored in DB
* Cache in Redis
* Touched timestamp triggers:
- DSL stored in DB
- Cache in Redis
- Touched timestamp triggers:
```
invalidate cache → recompile
@@ -366,9 +359,9 @@ invalidate cache → recompile
DSL Engine แยกเป็น:
* `workflow-engine-core` → Pure JS library
* `workflow-service` → NestJS module
* API public:
- `workflow-engine-core` → Pure JS library
- `workflow-service` → NestJS module
- API public:
```
POST /workflow/evaluate
@@ -378,9 +371,9 @@ POST /workflow/compile
ภายหลังสามารถนำไปวางบน:
* Kubernetes
* Worker Node
* API Gateway
- Kubernetes
- Worker Node
- API Gateway
---
@@ -394,4 +387,3 @@ POST /workflow/compile
✔ Execution API สำหรับ Backend และ Frontend
✔ รองรับ Business Workflow ซับซ้อนทั้งหมด
✔ Ready สำหรับ Microservice model ในอนาคต
@@ -373,7 +373,6 @@
### **Phase 0: Tasks**
- **[ ] T0.1 Secure Configuration Setup**
- [ ] ปรับปรุง `ConfigModule` ให้รองรับการอ่านค่าจาก Environment Variables
- [ ] สร้าง Template `docker-compose.override.yml.example` สำหรับ Dev
- [ ] Validate Config ด้วย Joi/Zod ตอน Start App (Throw error ถ้าขาด Secrets)
@@ -382,7 +381,6 @@
- [ ] **Dependencies:** None (Task เริ่มต้น)
- **[ ] T0.2 Redis & Queue Infrastructure**
- [ ] Setup Redis Container
- [ ] Setup BullMQ Module ใน NestJS สำหรับจัดการ Background Jobs
- [ ] Setup Redis Client สำหรับ Distributed Lock (Redlock)
@@ -391,7 +389,6 @@
- [ ] **Dependencies:** T0.1
- **[ ] T0.3 Setup Database Connection**
- [ ] Import SQL Schema v1.4.2 เข้า MariaDB
- [ ] Run Seed Data (organizations, users, roles, permissions)
- [ ] Configure TypeORM ใน AppModule
@@ -401,7 +398,6 @@
- [ ] **Dependencies:** T0.1
- **[ ] T0.4 Setup Git Repository**
- [ ] สร้าง Repository ใน Gitea (git.np-dms.work)
- [ ] Setup .gitignore, README.md, SECURITY.md
- [ ] Commit Initial Project
@@ -417,7 +413,6 @@
### **Phase 1: Tasks**
- **[ ] T1.1 CommonModule - Base Infrastructure**
- [ ] สร้าง Base Entity (id, created_at, updated_at, deleted_at)
- [ ] สร้าง Global Exception Filter (ไม่เปิดเผย sensitive information)
- [ ] สร้าง Response Transform Interceptor
@@ -432,7 +427,6 @@
- [ ] **Dependencies:** T0.2, T0.3
- **[ ] T1.2 AuthModule - JWT Authentication**
- [ ] สร้าง Entity: User
- [ ] สร้าง AuthService:
- [ ] login(username, password) → JWT Token
@@ -451,7 +445,6 @@
- [ ] **Dependencies:** T1.1, T0.3
- **[ ] T1.3 UserModule - User Management**
- [ ] สร้าง Entities: User, Role, Permission, UserRole, UserAssignment, **UserPreference**
- [ ] สร้าง UserService CRUD (พร้อม soft delete)
- [ ] สร้าง RoleService CRUD
@@ -472,7 +465,6 @@
- [ ] **Dependencies:** T1.1, T1.2
- **[ ] T1.4 RBAC Guard - 4-Level Authorization**
- [ ] สร้าง @RequirePermission() Decorator
- [ ] สร้าง RbacGuard ที่ตรวจสอบ 4 ระดับ:
- [ ] Global Permissions
@@ -486,7 +478,6 @@
- [ ] **Dependencies:** T1.1, T1.3
- **[ ] T1.5 ProjectModule - Base Structures**
- [ ] สร้าง Entities:
- [ ] Organization
- [ ] Project
@@ -511,7 +502,6 @@
### **Phase 2: Tasks**
- **[ ] T2.1 Virtual Columns for JSON**
- [ ] ออกแบบ Migration Script สำหรับตารางที่มี JSON Details
- [ ] เพิ่ม **Generated Columns (Virtual)** สำหรับฟิลด์ที่ใช้ Search บ่อยๆ (เช่น `project_id`, `type`) พร้อม Index
- [ ] **Security:** Implement admin-only access สำหรับ master data
@@ -519,7 +509,6 @@
- [ ] **Dependencies:** T0.3, T1.1, T1.5
- **[ ] T2.2 FileStorageService - Two-Phase Storage**
- [ ] สร้าง Attachment Entity
- [ ] สร้าง FileStorageService:
- [ ] **Phase 1 (Upload):** API รับไฟล์ → Scan Virus → Save ลง `temp/` → Return `temp_id`
@@ -537,7 +526,6 @@
- [ ] **Dependencies:** T1.1, T1.4
- **[ ] T2.3 DocumentNumberingModule - Double-Lock Mechanism**
- [ ] สร้าง Entities:
- [ ] DocumentNumberFormat
- [ ] DocumentNumberCounter
@@ -558,7 +546,6 @@
- [ ] **Deliverable:** Flexible Numbering System
- **[ ] T2.4 SecurityModule - Enhanced Security**
- [ ] สร้าง Input Validation Service:
- [ ] XSS Prevention
- [ ] SQL Injection Prevention
@@ -572,7 +559,6 @@
- [ ] **Dependencies:** T1.1
- **[ ] T2.5 JSON Details & Schema Management**
- [ ] T2.5.1 JsonSchemaModule - Schema Management: สร้าง Service สำหรับ Validate, get, register JSON schemas
- [ ] T2.5.2 DetailsService - Data Processing: สร้าง Service สำหรับ sanitize, transform, compress/decompress JSON
- [ ] T2.5.3 JSON Security & Validation: Implement security checks และ validation rules
@@ -674,11 +660,7 @@ export class JsonSchemaService {
this.registerCustomValidators();
}
async validateData(
schemaName: string,
data: any,
options: ValidationOptions = {}
): Promise<ValidationResult> {
async validateData(schemaName: string, data: any, options: ValidationOptions = {}): Promise<ValidationResult> {
const schema = await this.getSchema(schemaName);
const validate = this.ajv.compile(schema);
@@ -702,11 +684,7 @@ export class JsonSchemaService {
};
}
private async sanitizeData(
data: any,
schema: any,
options: ValidationOptions
): Promise<any> {
private async sanitizeData(data: any, schema: any, options: ValidationOptions): Promise<any> {
const sanitized = { ...data };
// Remove unknown properties if not allowed
@@ -765,10 +743,7 @@ export class VirtualColumnService {
private configService: ConfigService
) {}
async setupVirtualColumns(
tableName: string,
schemaConfig: VirtualColumnConfig[]
): Promise<void> {
async setupVirtualColumns(tableName: string, schemaConfig: VirtualColumnConfig[]): Promise<void> {
const connection = this.dataSource.manager.connection;
for (const config of schemaConfig) {
@@ -776,10 +751,7 @@ export class VirtualColumnService {
}
}
private async createVirtualColumn(
tableName: string,
config: VirtualColumnConfig
): Promise<void> {
private async createVirtualColumn(tableName: string, config: VirtualColumnConfig): Promise<void> {
const columnDefinition = this.generateColumnDefinition(config);
const sql = `
@@ -802,10 +774,7 @@ export class VirtualColumnService {
return `${dataType} GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '${jsonPath}'))) VIRTUAL`;
}
private async createIndex(
tableName: string,
config: VirtualColumnConfig
): Promise<void> {
private async createIndex(tableName: string, config: VirtualColumnConfig): Promise<void> {
const indexName = `idx_${tableName}_${config.column_name}`;
const sql = `
CREATE ${config.index_type} INDEX ${indexName}
@@ -922,13 +891,7 @@ const rfaDwgSchema: UiSchema = {
widget: 'select',
title: 'Discipline',
enum: ['CIVIL', 'STRUCTURAL', 'MECHANICAL', 'ELECTRICAL', 'PLUMBING'],
enumNames: [
'Civil',
'Structural',
'Mechanical',
'Electrical',
'Plumbing',
],
enumNames: ['Civil', 'Structural', 'Mechanical', 'Electrical', 'Plumbing'],
},
drawingReferences: {
type: 'array',
@@ -961,21 +924,11 @@ const rfaDwgSchema: UiSchema = {
```typescript
@Injectable()
export class SchemaMigrationService {
async migrateData(
entityType: string,
entityId: string,
targetVersion: number
): Promise<MigrationResult> {
async migrateData(entityType: string, entityId: string, targetVersion: number): Promise<MigrationResult> {
const currentData = await this.getCurrentData(entityType, entityId);
const currentVersion = await this.getCurrentSchemaVersion(
entityType,
entityId
);
const currentVersion = await this.getCurrentSchemaVersion(entityType, entityId);
const migrationPath = await this.findMigrationPath(
currentVersion,
targetVersion
);
const migrationPath = await this.findMigrationPath(currentVersion, targetVersion);
let migratedData = currentData;
@@ -984,24 +937,13 @@ export class SchemaMigrationService {
}
// Validate migrated data against target schema
const validationResult = await this.validateAgainstSchema(
migratedData,
targetVersion
);
const validationResult = await this.validateAgainstSchema(migratedData, targetVersion);
if (!validationResult.isValid) {
throw new MigrationError(
'MIGRATION_VALIDATION_FAILED',
validationResult.errors
);
throw new MigrationError('MIGRATION_VALIDATION_FAILED', validationResult.errors);
}
await this.saveMigratedData(
entityType,
entityId,
migratedData,
targetVersion
);
await this.saveMigratedData(entityType, entityId, migratedData, targetVersion);
return {
success: true,
@@ -1011,10 +953,7 @@ export class SchemaMigrationService {
};
}
private async applyMigrationStep(
step: MigrationStep,
data: any
): Promise<any> {
private async applyMigrationStep(step: MigrationStep, data: any): Promise<any> {
switch (step.type) {
case 'FIELD_RENAME':
return this.renameField(data, step.config);
@@ -1070,11 +1009,7 @@ const migrationSteps = [
```typescript
@Injectable()
export class JsonSecurityService {
async applyFieldLevelSecurity(
data: any,
schema: any,
userContext: UserContext
): Promise<any> {
async applyFieldLevelSecurity(data: any, schema: any, userContext: UserContext): Promise<any> {
const securedData = { ...data };
const securityRules = await this.getSecurityRules(schema.name);
@@ -1101,10 +1036,7 @@ export class JsonSecurityService {
for (const fieldPath of sensitiveFields) {
const fieldValue = this.getFieldValue(data, fieldPath);
if (fieldValue) {
const encrypted = await this.cryptoService.encrypt(
fieldValue,
'field-level'
);
const encrypted = await this.cryptoService.encrypt(fieldValue, 'field-level');
this.setFieldValue(encryptedData, fieldPath, encrypted);
}
}
@@ -1149,15 +1081,8 @@ export class JsonSecurityService {
export class JsonSchemaController {
@Post('validate/:schemaName')
@RequirePermission('schema.validate')
async validateData(
@Param('schemaName') schemaName: string,
@Body() dto: ValidateDataDto
): Promise<ValidationResult> {
return this.jsonSchemaService.validateData(
schemaName,
dto.data,
dto.options
);
async validateData(@Param('schemaName') schemaName: string, @Body() dto: ValidateDataDto): Promise<ValidationResult> {
return this.jsonSchemaService.validateData(schemaName, dto.data, dto.options);
}
@Post('schemas')
@@ -1173,18 +1098,12 @@ export class JsonSchemaController {
@Param('entityId') entityId: string,
@Body() dto: MigrateDataDto
): Promise<MigrationResult> {
return this.migrationService.migrateData(
entityType,
entityId,
dto.targetVersion
);
return this.migrationService.migrateData(entityType, entityId, dto.targetVersion);
}
@Get('ui-schema/:schemaName')
@RequirePermission('schema.view')
async getUiSchema(
@Param('schemaName') schemaName: string
): Promise<UiSchema> {
async getUiSchema(@Param('schemaName') schemaName: string): Promise<UiSchema> {
return this.schemaService.getUiSchema(schemaName);
}
}
@@ -1206,32 +1125,22 @@ export class CorrespondenceService {
private detailsService: DetailsService
) {}
async createCorrespondence(
dto: CreateCorrespondenceDto
): Promise<Correspondence> {
async createCorrespondence(dto: CreateCorrespondenceDto): Promise<Correspondence> {
// 1. Validate details against schema
const validationResult = await this.jsonSchemaService.validateData(
`CORRESPONDENCE_${dto.type}`,
dto.details
);
const validationResult = await this.jsonSchemaService.validateData(`CORRESPONDENCE_${dto.type}`, dto.details);
if (!validationResult.isValid) {
throw new ValidationError('INVALID_DETAILS', validationResult.errors);
}
// 2. Apply security and sanitization
const secureDetails = await this.detailsService.sanitizeDetails(
validationResult.sanitizedData,
dto.type
);
const secureDetails = await this.detailsService.sanitizeDetails(validationResult.sanitizedData, dto.type);
// 3. Create correspondence entity
const correspondence = this.correspondenceRepository.create({
...dto,
details: secureDetails,
schema_version: await this.getCurrentSchemaVersion(
`CORRESPONDENCE_${dto.type}`
),
schema_version: await this.getCurrentSchemaVersion(`CORRESPONDENCE_${dto.type}`),
});
// 4. Setup virtual columns for performance
@@ -1240,9 +1149,7 @@ export class CorrespondenceService {
return this.correspondenceRepository.save(correspondence);
}
async searchCorrespondences(
filters: SearchFilters
): Promise<Correspondence[]> {
async searchCorrespondences(filters: SearchFilters): Promise<Correspondence[]> {
// Use virtual columns for efficient filtering
const query = this.correspondenceRepository.createQueryBuilder('c');
@@ -1363,7 +1270,6 @@ describe('VirtualColumnService', () => {
### **Phase 3: Tasks**
- **[ ] T3.1 WorkflowEngineModule (New)**
- [ ] ออกแบบ Generic Schema สำหรับ Workflow State Machine
- [ ] Implement Service: `initializeWorkflow()`, `processAction()`, `getNextStep()`
- [ ] รองรับ Logic การ "ข้ามขั้นตอน" และ "ส่งกลับ" ภายใน Engine เดียว
@@ -1438,7 +1344,6 @@ states:
```
- **[ ] T3.1.2 Workflow Core Entities & Database Schema**
- [ ] WorkflowDefinition Entity
- [ ] WorkflowInstance Entity
- [ ] WorkflowHistory Entity
@@ -1505,7 +1410,6 @@ export class WorkflowInstance {
```
- **[ ] T3.1.3 DSL Parser & Compiler Service**
- [ ] YAML Parser สำหรับอ่าน DSL definitions
- [ ] Syntax Validator สำหรับ compile-time validation
- [ ] Schema Compiler สำหรับแปลง DSL → Normalized JSON
@@ -1537,9 +1441,7 @@ export class WorkflowDslService {
// Terminal states ต้องไม่มี transitions
() => !definition.states.filter((s) => s.terminal).some((s) => s.on),
// State names must be unique
() =>
new Set(definition.states.map((s) => s.name)).size ===
definition.states.length,
() => new Set(definition.states.map((s) => s.name)).size === definition.states.length,
// Transition targets must exist
() => this.validateTransitionTargets(definition),
];
@@ -1683,11 +1585,7 @@ interface StateWithTimeout {
```typescript
@Injectable()
export class WorkflowEventService {
async executeEvents(
events: WorkflowEvent[],
instance: WorkflowInstance,
context: WorkflowContext
): Promise<void> {
async executeEvents(events: WorkflowEvent[], instance: WorkflowInstance, context: WorkflowContext): Promise<void> {
for (const event of events) {
switch (event.type) {
case 'notify':
@@ -1711,16 +1609,8 @@ export class WorkflowEventService {
instance: WorkflowInstance,
context: WorkflowContext
): Promise<void> {
const recipients = await this.resolveRecipients(
event.target,
instance,
context
);
const message = await this.renderTemplate(
event.template,
instance,
context
);
const recipients = await this.resolveRecipients(event.target, instance, context);
const message = await this.renderTemplate(event.template, instance, context);
await this.notificationService.send({
type: 'workflow',
@@ -1752,35 +1642,24 @@ export class WorkflowEngineController {
@Param('id') instanceId: string,
@Body() dto: WorkflowTransitionDto
): Promise<TransitionResult> {
return this.workflowEngine.processTransition(
instanceId,
dto.action,
dto.context
);
return this.workflowEngine.processTransition(instanceId, dto.action, dto.context);
}
@Get('instances/:id/actions')
@RequirePermission('workflow.view')
async getAvailableActions(
@Param('id') instanceId: string,
@Query() context: WorkflowContext
): Promise<string[]> {
async getAvailableActions(@Param('id') instanceId: string, @Query() context: WorkflowContext): Promise<string[]> {
return this.workflowEngine.getAvailableActions(instanceId, context);
}
@Post('definitions')
@RequirePermission('workflow.manage')
async createWorkflowDefinition(
@Body() dto: CreateWorkflowDefinitionDto
): Promise<WorkflowDefinition> {
async createWorkflowDefinition(@Body() dto: CreateWorkflowDefinitionDto): Promise<WorkflowDefinition> {
return this.workflowDslService.compileAndSave(dto.dslContent);
}
@Get('instances/:id/history')
@RequirePermission('workflow.view')
async getWorkflowHistory(
@Param('id') instanceId: string
): Promise<WorkflowHistory[]> {
async getWorkflowHistory(@Param('id') instanceId: string): Promise<WorkflowHistory[]> {
return this.workflowHistoryService.getHistory(instanceId);
}
}
@@ -1801,13 +1680,8 @@ export class CorrespondenceWorkflowService {
private correspondenceService: CorrespondenceService
) {}
async submitCorrespondence(
correspondenceId: string,
userId: string
): Promise<void> {
const correspondence = await this.correspondenceService.findById(
correspondenceId
);
async submitCorrespondence(correspondenceId: string, userId: string): Promise<void> {
const correspondence = await this.correspondenceService.findById(correspondenceId);
// Create workflow instance
const instance = await this.workflowEngine.createInstance({
@@ -1830,7 +1704,6 @@ export class CorrespondenceWorkflowService {
```
- [ ] **T3.1.9 Cleanup Legacy Code:**
- [ ] ลบ Entity/Repository ของตารางเก่า (_\_routings, _\_templates) ออกจาก Codebase
- [ ] อัปเดต SQL Migration ให้ Drop ตารางเก่าทิ้ง
@@ -1849,11 +1722,7 @@ describe('WorkflowEngineService', () => {
const context = { userId: 'user1', roles: ['APPROVER'] };
// Act
const result = await workflowEngine.processTransition(
instance.id,
'APPROVE',
context
);
const result = await workflowEngine.processTransition(instance.id, 'APPROVE', context);
// Assert
expect(result.success).toBe(true);
@@ -1866,23 +1735,19 @@ describe('WorkflowEngineService', () => {
const context = { userId: 'user2', roles: ['VIEWER'] };
// Act & Assert
await expect(
workflowEngine.processTransition(instance.id, 'APPROVE', context)
).rejects.toThrow(WorkflowError);
await expect(workflowEngine.processTransition(instance.id, 'APPROVE', context)).rejects.toThrow(WorkflowError);
});
});
});
```
- **🔗 Critical Dependencies of T3.1.1-T3.1.8**
- T1.1 (Common Module) - สำหรับ base entities และ shared services
- T1.4 (RBAC Guard) - สำหรับ permission checking
- T2.5 (JSON Schema) - สำหรับ DSL validation
- T6.2 (Notification) - สำหรับ event handling
- **🎯 Success Metrics**
- ✅ Support ทั้ง Correspondence Routing และ RFA Workflow
- ✅ DSL ที่ human-readable และ editable โดยไม่ต้องแก้โค้ด
- ✅ Performance: < 50ms ต่อ state transition
@@ -1890,7 +1755,6 @@ describe('WorkflowEngineService', () => {
- ✅ Complete audit trail สำหรับทุก workflow instance
- **[ ] T3.2 CorrespondenceModule - Basic CRUD**
- [ ] สร้าง Entities (Correspondence, Revision, Recipient, Tag, Reference, Attachment)
- [ ] สร้าง CorrespondenceService (Create with Document Numbering, Update with new Revision, Soft Delete)
- [ ] สร้าง Controllers (POST/GET/PUT/DELETE /correspondences)
@@ -1900,7 +1764,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T1.1, T1.2, T1.3, T1.4, T1.5, T2.3, T2.2, T2.5
- **[ ] T3.3 CorrespondenceModule - Advanced Features**
- [ ] Implement Status Transitions (DRAFT → SUBMITTED)
- [ ] Implement References (Link Documents)
- [ ] Implement Search (Basic)
@@ -1909,7 +1772,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T3.2
- **[ ] T3.4 Correspondence Integration with Workflow**
- [ ] เชื่อมต่อ `CorrespondenceService` เข้ากับ `WorkflowEngineModule`
- [ ] ย้าย Logic การ Routing เดิมมาใช้ Engine ใหม่
- [ ] สร้าง API endpoints สำหรับ Frontend (Templates, Pending Tasks, Bulk Action)
@@ -1926,7 +1788,6 @@ describe('WorkflowEngineService', () => {
### **Phase 4: Tasks**
- **[ ] T4.1 DrawingModule - Contract Drawings**
- [ ] สร้าง Entities (ContractDrawing, Volume, Category, SubCategory, Attachment)
- [ ] สร้าง ContractDrawingService CRUD
- [ ] สร้าง Controllers (GET/POST /drawings/contract)
@@ -1935,7 +1796,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T1.1, T1.2, T1.4, T1.5, T2.2
- **[ ] T4.2 DrawingModule - Shop Drawings**
- [ ] สร้าง Entities (ShopDrawing, Revision, Main/SubCategory, ContractRef, RevisionAttachment)
- [ ] สร้าง ShopDrawingService CRUD (รวมการสร้าง Revision)
- [ ] สร้าง Controllers (GET/POST /drawings/shop, /drawings/shop/:id/revisions)
@@ -1953,7 +1813,6 @@ describe('WorkflowEngineService', () => {
### **Phase 5: Tasks**
- **[ ] T5.1 RfaModule with Unified Workflow**
- [ ] สร้าง Entities (Rfa, RfaRevision, RfaItem)
- [ ] สร้าง RfaService (Create RFA, Link Shop Drawings)
- [ ] Implement RFA Workflow โดยใช้ Configuration ของ `WorkflowEngineModule`
@@ -1963,7 +1822,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T3.2, T4.2, T2.5, T6.2
- **[ ] T5.2 CirculationModule - Internal Routing**
- [ ] สร้าง Entities (Circulation, Template, Routing, Attachment)
- [ ] สร้าง CirculationService (Create 1:1 with Correspondence, Assign User, Complete/Close Step)
- [ ] สร้าง Controllers (POST/GET /circulations, POST /circulations/:id/steps/...)
@@ -1972,7 +1830,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T3.2, T2.5, T6.2
- **[ ] T5.3 TransmittalModule - Document Forwarding**
- [ ] สร้าง Entities (Transmittal, TransmittalItem)
- [ ] สร้าง TransmittalService (Create Correspondence + Transmittal, Link Multiple Correspondences)
- [ ] สร้าง Controllers (POST/GET /transmittals)
@@ -1989,7 +1846,6 @@ describe('WorkflowEngineService', () => {
### **Phase 6: Tasks**
- **[ ] T6.1 SearchModule - Elasticsearch Integration**
- [ ] Setup Elasticsearch Container
- [ ] สร้าง SearchService (index/update/delete documents, search)
- [ ] Index ทุก Document Type
@@ -1999,7 +1855,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T3.2, T5.1, T4.2, T5.2, T5.3
- **[ ] T6.2 Notification Queue & Digest**
- [ ] สร้าง NotificationService (sendEmail/Line/System)
- [ ] **Producer:** Push Event ลง BullMQ Queue
- [ ] **Consumer:** จัดกลุ่ม Notification (Digest Message) และส่งผ่าน Email/Line
@@ -2010,7 +1865,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T1.1, T6.4
- **[ ] T6.3 MonitoringModule - Observability**
- [ ] สร้าง Health Check Controller (GET /health)
- [ ] สร้าง Metrics Service (API response times, Error rates)
- [ ] สร้าง Performance Interceptor (Track request duration)
@@ -2019,7 +1873,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T1.1
- **[ ] T6.4 ResilienceModule - Circuit Breaker & Retry**
- [ ] สร้าง Circuit Breaker Service (@CircuitBreaker() decorator)
- [ ] สร้าง Retry Service (@Retry() decorator)
- [ ] สร้าง Fallback Strategies
@@ -2028,7 +1881,6 @@ describe('WorkflowEngineService', () => {
- [ ] **Dependencies:** T1.1
- **[ ] T6.5 Data Partitioning Strategy**
- [ ] ออกแบบ Table Partitioning สำหรับ `audit_logs` และ `notifications` (แบ่งตาม Range: Year)
- [ ] เขียน Raw SQL Migration สำหรับสร้าง Partition Table
- [ ] **Deliverable:** Database Performance และ Scalability ดีขึ้น
@@ -2043,21 +1895,18 @@ describe('WorkflowEngineService', () => {
### **Phase 7: Tasks**
- **[ ] T7.1 Concurrency Testing**
- [ ] เขียน Test Scenarios ยิง Request ขอเลขที่เอกสารพร้อมกัน 100 Request (ต้องไม่ซ้ำและไม่ข้าม)
- [ ] ทดสอบ Optimistic Lock ทำงานถูกต้องเมื่อ Redis ถูกปิด
- [ ] ทดสอบ File Upload พร้อมกันหลายไฟล์
- [ ] **Deliverable:** ระบบทนทานต่อ Concurrency Issues
- **[ ] T7.2 Transaction Integrity Testing**
- [ ] ทดสอบ Upload ไฟล์แล้ว Kill Process ก่อน Commit
- [ ] ทดสอบ Two-Phase File Storage ทำงานถูกต้อง
- [ ] ทดสอบ Database Transaction Rollback Scenarios
- [ ] **Deliverable:** Data Integrity รับประกันได้
- **[ ] T7.3 Security & Idempotency Test**
- [ ] ทดสอบ Replay Attack โดยใช้ `Idempotency-Key` ซ้ำ
- [ ] ทดสอบ Maintenance Mode Block API ได้จริง
- [ ] ทดสอบ RBAC 4-Level ทำงานถูกต้อง 100%
@@ -2070,7 +1919,6 @@ describe('WorkflowEngineService', () => {
- **[ ] T7.6 E2E Testing**
- **[ ] T7.7 Performance Testing**
- [ ] Load Testing: 100 concurrent users
- [ ] **(สำคัญ)** การจูนและทดสอบ Load Test จะต้องทำในสภาพแวดล้อมที่จำลอง Spec ของ QNAP Server (TS-473A, AMD Ryzen V1500B) เพื่อให้ได้ค่า Response Time และ Connection Pool ที่เที่ยงตรง
- [ ] Stress Testing
@@ -2078,14 +1926,12 @@ describe('WorkflowEngineService', () => {
- [ ] **Deliverable:** Performance targets บรรลุ
- **[ ] T7.8 Security Testing**
- [ ] Penetration Testing (OWASP Top 10)
- [ ] Security Audit (Code review, Dependency scanning)
- [ ] File Upload Security Testing
- [ ] **Deliverable:** Security tests ผ่าน
- **[ ] T7.9 Performance Optimization**
- [ ] Implement Caching (Master Data, User Permissions, Search Results)
- [ ] Database Optimization (Review Indexes, Query Optimization, Pagination)
- [ ] **Deliverable:** Response Time < 200ms (90th percentile)
@@ -10,6 +10,7 @@
## 🎯 **ภาพรวมโครงการ (Project Overview)**
พัฒนา Backend สำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System) เวอร์ชัน 1.5.1 โดยเน้นการปรับปรุงสถาปัตยกรรมหลัก 3 ส่วนสำคัญ:
1. **Unified Workflow Engine:** ระบบ Workflow แบบ Dynamic ที่ยืดหยุ่น รองรับการกำหนด Rule ผ่าน DSL
2. **Advanced Document Numbering:** ระบบสร้างเลขที่เอกสารที่ซับซ้อน (8-component key) พร้อม Double-Lock Mechanism ป้องกัน Race Condition
3. **Enhanced Master Data:** การจัดการข้อมูลหลักที่ครอบคลุม (Discipline, SubType) และ JSON Schema Management
@@ -20,14 +21,14 @@
### **Technology Stack**
- **Framework:** NestJS (TypeScript, ESM)
- **Database:** MariaDB 10.11 (ใช้ Virtual Columns & Partitioning)
- **ORM:** TypeORM (Optimistic Locking)
- **Workflow Engine:** Custom DSL-based Engine (State Machine)
- **Queue:** BullMQ (Redis) สำหรับ Async Jobs & Notifications
- **Locking:** Redis (Redlock) + DB Pessimistic Fallback
- **Search:** Elasticsearch
- **Validation:** Zod / Class-validator / AJV (JSON Schema)
- **Framework:** NestJS (TypeScript, ESM)
- **Database:** MariaDB 10.11 (ใช้ Virtual Columns & Partitioning)
- **ORM:** TypeORM (Optimistic Locking)
- **Workflow Engine:** Custom DSL-based Engine (State Machine)
- **Queue:** BullMQ (Redis) สำหรับ Async Jobs & Notifications
- **Locking:** Redis (Redlock) + DB Pessimistic Fallback
- **Search:** Elasticsearch
- **Validation:** Zod / Class-validator / AJV (JSON Schema)
### **โครงสร้างโมดูล (Module Structure)**
@@ -64,23 +65,25 @@
**Goal:** เตรียมโครงสร้างพื้นฐานและข้อมูลหลักให้พร้อมสำหรับโมดูลอื่น
#### **[ ] T1.1 Master Data Module (Enhanced)**
- **Objective:** จัดการข้อมูลหลักทั้งหมดรวมถึงตารางใหม่ใน v1.5.1
- **Tasks:**
- [ ] Implement `OrganizationService` (CRUD)
- [ ] Implement `ProjectService` & `ContractService`
- [ ] Implement `TypeService` (Correspondence, RFA, Drawing)
- [ ] **[NEW]** Implement `DisciplineService` (CRUD for `disciplines` table)
- [ ] **[NEW]** Implement `CorrespondenceSubTypeService`
- [ ] **[NEW]** Implement `CodeService` (RFA Approve Codes, Status Codes)
- **Deliverables:** API สำหรับจัดการ Master Data ทั้งหมด
- **Objective:** จัดการข้อมูลหลักทั้งหมดรวมถึงตารางใหม่ใน v1.5.1
- **Tasks:**
- [ ] Implement `OrganizationService` (CRUD)
- [ ] Implement `ProjectService` & `ContractService`
- [ ] Implement `TypeService` (Correspondence, RFA, Drawing)
- [ ] **[NEW]** Implement `DisciplineService` (CRUD for `disciplines` table)
- [ ] **[NEW]** Implement `CorrespondenceSubTypeService`
- [ ] **[NEW]** Implement `CodeService` (RFA Approve Codes, Status Codes)
- **Deliverables:** API สำหรับจัดการ Master Data ทั้งหมด
#### **[ ] T1.2 User & Auth Module**
- **Objective:** ระบบผู้ใช้งานและสิทธิ์ (RBAC)
- **Tasks:**
- [ ] Implement `AuthService` (Login, Refresh Token)
- [ ] Implement `UserService` & `UserPreferenceService`
- [ ] Implement RBAC Guards (Global, Org, Project, Contract scopes)
- **Deliverables:** Secure Authentication & Authorization
- **Objective:** ระบบผู้ใช้งานและสิทธิ์ (RBAC)
- **Tasks:**
- [ ] Implement `AuthService` (Login, Refresh Token)
- [ ] Implement `UserService` & `UserPreferenceService`
- [ ] Implement RBAC Guards (Global, Org, Project, Contract scopes)
- **Deliverables:** Secure Authentication & Authorization
---
@@ -89,23 +92,25 @@
**Goal:** ระบบเลขที่เอกสารที่ถูกต้องแม่นยำและระบบไฟล์ที่ปลอดภัย
#### **[ ] T2.1 Document Numbering Module (Major Update)**
- **Objective:** ระบบสร้างเลขที่เอกสารแบบ 8-component key พร้อม Double-Lock
- **Tasks:**
- [ ] Update `DocumentNumberCounter` entity (8-column PK)
- [ ] Implement `DocumentNumberingService` with **Redlock**
- [ ] Implement **DB Optimistic Lock** fallback strategy
- [ ] Implement Token Parser (`{DISCIPLINE}`, `{SUB_TYPE}`, `{RFA_TYPE}`)
- [ ] Create `DocumentNumberAudit` & `DocumentNumberError` tables
- **Deliverables:** Race-condition free numbering system
- **Objective:** ระบบสร้างเลขที่เอกสารแบบ 8-component key พร้อม Double-Lock
- **Tasks:**
- [ ] Update `DocumentNumberCounter` entity (8-column PK)
- [ ] Implement `DocumentNumberingService` with **Redlock**
- [ ] Implement **DB Optimistic Lock** fallback strategy
- [ ] Implement Token Parser (`{DISCIPLINE}`, `{SUB_TYPE}`, `{RFA_TYPE}`)
- [ ] Create `DocumentNumberAudit` & `DocumentNumberError` tables
- **Deliverables:** Race-condition free numbering system
#### **[ ] T2.2 File Storage Service**
- **Objective:** Two-Phase Storage Strategy
- **Tasks:**
- [ ] Implement `Upload` (Phase 1: Temp storage)
- [ ] Implement `Commit` (Phase 2: Move to permanent)
- [ ] Integrate **ClamAV** for virus scanning
- [ ] Implement Cleanup Job for orphan files
- **Deliverables:** Secure file upload system
- **Objective:** Two-Phase Storage Strategy
- **Tasks:**
- [ ] Implement `Upload` (Phase 1: Temp storage)
- [ ] Implement `Commit` (Phase 2: Move to permanent)
- [ ] Integrate **ClamAV** for virus scanning
- [ ] Implement Cleanup Job for orphan files
- **Deliverables:** Secure file upload system
---
@@ -114,22 +119,24 @@
**Goal:** ระบบ Workflow กลางที่ยืดหยุ่นและ Configurable
#### **[ ] T3.1 Workflow Engine Core**
- **Objective:** สร้าง Engine สำหรับรัน Workflow ตาม DSL
- **Tasks:**
- [ ] Design DSL Schema (JSON)
- [ ] Implement `DslParserService` & Validator
- [ ] Implement `WorkflowEngineService` (State Machine)
- [ ] Implement `GuardExecutor` (Permission/Condition checks)
- [ ] Implement `EffectExecutor` (Actions after transition)
- **Deliverables:** Functional Workflow Engine
- **Objective:** สร้าง Engine สำหรับรัน Workflow ตาม DSL
- **Tasks:**
- [ ] Design DSL Schema (JSON)
- [ ] Implement `DslParserService` & Validator
- [ ] Implement `WorkflowEngineService` (State Machine)
- [ ] Implement `GuardExecutor` (Permission/Condition checks)
- [ ] Implement `EffectExecutor` (Actions after transition)
- **Deliverables:** Functional Workflow Engine
#### **[ ] T3.2 Workflow Integration**
- **Objective:** เชื่อมต่อ Engine เข้ากับ Business Modules
- **Tasks:**
- [ ] Create Standard Workflow Definitions (Correspondence, RFA)
- [ ] Implement `WorkflowInstance` creation logic
- [ ] Create API for Workflow Actions (Approve, Reject, Comment)
- **Deliverables:** Integrated Workflow System
- **Objective:** เชื่อมต่อ Engine เข้ากับ Business Modules
- **Tasks:**
- [ ] Create Standard Workflow Definitions (Correspondence, RFA)
- [ ] Implement `WorkflowInstance` creation logic
- [ ] Create API for Workflow Actions (Approve, Reject, Comment)
- **Deliverables:** Integrated Workflow System
---
@@ -138,42 +145,47 @@
**Goal:** ฟังก์ชันการทำงานหลักของระบบเอกสาร
#### **[ ] T4.1 Correspondence Module**
- **Objective:** จัดการหนังสือโต้ตอบ
- **Tasks:**
- [ ] Update Entity to support `discipline_id`
- [ ] Integrate with **Document Numbering**
- [ ] Integrate with **Workflow Engine**
- [ ] Implement CRUD & Revision handling
- **Objective:** จัดการหนังสือโต้ตอบ
- **Tasks:**
- [ ] Update Entity to support `discipline_id`
- [ ] Integrate with **Document Numbering**
- [ ] Integrate with **Workflow Engine**
- [ ] Implement CRUD & Revision handling
#### **[ ] T4.2 RFA Module**
- **Objective:** จัดการเอกสารขออนุมัติ
- **Tasks:**
- [ ] Update Entity to support `discipline_id`
- [ ] Implement RFA-specific workflow logic
- [ ] Implement RFA Item linking (Drawings)
- **Objective:** จัดการเอกสารขออนุมัติ
- **Tasks:**
- [ ] Update Entity to support `discipline_id`
- [ ] Implement RFA-specific workflow logic
- [ ] Implement RFA Item linking (Drawings)
#### **[ ] T4.3 Drawing Module**
- **Objective:** จัดการแบบก่อสร้าง (Shop Drawing, Contract Drawing)
- **Tasks:**
- [ ] Implement `ShopDrawingService` & `ContractDrawingService`
- [ ] Implement Revision Control for Drawings
- [ ] Implement Drawing Numbering Logic
- **Objective:** จัดการแบบก่อสร้าง (Shop Drawing, Contract Drawing)
- **Tasks:**
- [ ] Implement `ShopDrawingService` & `ContractDrawingService`
- [ ] Implement Revision Control for Drawings
- [ ] Implement Drawing Numbering Logic
#### **[ ] T4.4 Transmittal Module**
- **Objective:** จัดการใบนำส่งเอกสาร (Transmittal)
- **Tasks:**
- [ ] Implement `TransmittalService` (Create, View, PDF)
- [ ] Implement `TransmittalItem` linking (Correspondence, RFA, Drawing)
- [ ] Implement Transmittal Numbering (Type 901)
- [ ] Generate PDF Transmittal Letter
- **Objective:** จัดการใบนำส่งเอกสาร (Transmittal)
- **Tasks:**
- [ ] Implement `TransmittalService` (Create, View, PDF)
- [ ] Implement `TransmittalItem` linking (Correspondence, RFA, Drawing)
- [ ] Implement Transmittal Numbering (Type 901)
- [ ] Generate PDF Transmittal Letter
#### **[ ] T4.5 Circulation Module**
- **Objective:** จัดการใบเวียนภายใน (Circulation Sheet)
- **Tasks:**
- [ ] Implement `CirculationService` (Create, Assign, Complete)
- [ ] Implement `CirculationAssignee` tracking (Multiple users)
- [ ] Implement Circulation Numbering (Type 900)
- [ ] Integrate with Workflow for completion tracking
- **Objective:** จัดการใบเวียนภายใน (Circulation Sheet)
- **Tasks:**
- [ ] Implement `CirculationService` (Create, Assign, Complete)
- [ ] Implement `CirculationAssignee` tracking (Multiple users)
- [ ] Implement Circulation Numbering (Type 900)
- [ ] Integrate with Workflow for completion tracking
---
@@ -182,18 +194,20 @@
**Goal:** ระบบสนับสนุนและการตรวจสอบ
#### **[ ] T5.1 JSON Schema & Preferences**
- **Objective:** จัดการ Dynamic Data และ User Settings
- **Tasks:**
- [ ] Implement `JsonSchemaService` (Registry & Validation)
- [ ] Implement `UserPreferenceService`
- [ ] Implement Virtual Column management
- **Objective:** จัดการ Dynamic Data และ User Settings
- **Tasks:**
- [ ] Implement `JsonSchemaService` (Registry & Validation)
- [ ] Implement `UserPreferenceService`
- [ ] Implement Virtual Column management
#### **[ ] T5.2 Search & Logs**
- **Objective:** การค้นหาและตรวจสอบ
- **Tasks:**
- [ ] Implement **Elasticsearch** Sync
- [ ] Implement **Audit Log** with Partitioning
- [ ] Setup **Prometheus/Grafana** metrics
- **Objective:** การค้นหาและตรวจสอบ
- **Tasks:**
- [ ] Implement **Elasticsearch** Sync
- [ ] Implement **Audit Log** with Partitioning
- [ ] Setup **Prometheus/Grafana** metrics
---
@@ -206,7 +206,6 @@
### **Phase 0: Tasks**
- **[ ] F0.1 Project Setup & Tooling**
- [ ] Initialize Next.js 14+ project with TypeScript
- [ ] Configure pnpm workspace
- [ ] Setup ESLint, Prettier, and pre-commit hooks
@@ -217,7 +216,6 @@
- [ ] **Dependencies:** None
- **[ ] F0.2 Design System & UI Components**
- [ ] Setup color palette and design tokens
- [ ] Create responsive design breakpoints
- [ ] Implement core shadcn/ui components:
@@ -233,7 +231,6 @@
- [ ] **Dependencies:** F0.1
- **[ ] F0.3 API Client & Authentication**
- [ ] Setup Axios client with interceptors:
- [ ] Idempotency-Key header injection
- [ ] Authentication token management
@@ -282,7 +279,6 @@
### **Phase 1: Tasks**
- **[ ] F1.1 Main Layout & Navigation**
- [ ] Create App Shell layout:
- [ ] Navbar with user menu and notifications
- [ ] Collapsible sidebar with navigation
@@ -299,7 +295,6 @@
- [ ] **Dependencies:** F0.2, F0.3
- **[ ] F1.2 Authentication Pages**
- [ ] Create login page with form validation
- [ ] Implement forgot password flow
- [ ] Create registration page (admin-only)
@@ -310,7 +305,6 @@
- [ ] **Dependencies:** F0.3, F1.1
- **[ ] F1.3 Dashboard & Landing**
- [ ] Create public landing page for non-authenticated users
- [ ] Implement main dashboard with:
- [ ] KPI cards (document counts, pending tasks)
@@ -356,7 +350,6 @@
### **Phase 2: Tasks**
- **[ ] F2.1 User Profile & Settings**
- [ ] Create user profile page:
- [ ] Personal information display/edit
- [ ] Password change functionality
@@ -368,7 +361,6 @@
- [ ] **Dependencies:** F1.1, F0.4
- **[ ] F2.2 Admin Panel - User Management**
- [ ] Create user list with search and filters
- [ ] Implement user creation form
- [ ] Create user edit interface
@@ -379,7 +371,6 @@
- [ ] **Dependencies:** F1.1, F2.1
- **[ ] F2.3 Admin Panel - Role Management**
- [ ] Create role list and management interface
- [ ] Implement role creation and editing
- [ ] Create permission assignment interface
@@ -432,7 +423,6 @@
### **Phase 3: Tasks**
- **[ ] F3.1 Project Management UI**
- [ ] Create project list with search and filters
- [ ] Implement project creation and editing
- [ ] Create project detail view
@@ -443,7 +433,6 @@
- [ ] **Dependencies:** F1.1, F2.4
- **[ ] F3.2 Organization Management**
- [ ] Create organization list and management
- [ ] Implement organization creation and editing
- [ ] Create organization detail view
@@ -479,7 +468,6 @@
### **Phase 4: Tasks**
- **[ ] F4.1 Correspondence List & Search**
- [ ] Create correspondence list with advanced filtering:
- [ ] Filter by type, status, project, organization
- [ ] Search by title, document number, content
@@ -494,7 +482,6 @@
- [ ] **Dependencies:** F1.1, F3.1
- **[ ] F4.2 Correspondence Creation Form**
- [ ] Create dynamic form generator based on JSON schema
- [ ] Implement form with multiple sections:
- [ ] Basic information (type, title, recipients)
@@ -512,7 +499,6 @@
- [ ] **Dependencies:** F0.4, F4.1
- **[ ] F4.3 Correspondence Detail View**
- [ ] Create comprehensive detail page:
- [ ] Document header with metadata
- [ ] Content display based on type
@@ -544,7 +530,6 @@
### **Phase 4: Testing - Correspondence System**
- **[ ] F4.T1 Correspondence Test Suite**
- [ ] **Unit Tests:** Form validation, file upload components
- [ ] **Integration Tests:** Complete document lifecycle, file attachment flow
- [ ] **E2E Tests:** End-to-end correspondence creation and management
@@ -563,7 +548,6 @@
### **Phase 5: Tasks**
- **[ ] F5.1 Workflow Visualization Component**
- [ ] Create horizontal workflow progress visualization
- [ ] Implement step status indicators (pending, active, completed, skipped)
- [ ] Add due date and assignee information
@@ -574,7 +558,6 @@
- [ ] **Dependencies:** F4.3
- **[ ] F5.2 Routing Template Management**
- [ ] Create routing template list and editor
- [ ] Implement drag-and-drop step configuration
- [ ] Add step configuration (purpose, duration, assignee rules)
@@ -585,7 +568,6 @@
- [ ] **Dependencies:** F3.1, F4.2
- **[ ] F5.3 Workflow Step Actions**
- [ ] Create step action interface:
- [ ] Approve, reject, request changes
- [ ] Add comments and attachments
@@ -623,7 +605,6 @@
### **Phase 6: Tasks**
- **[ ] F6.1 Contract Drawings Management**
- [ ] Create contract drawing list with categorization
- [ ] Implement drawing upload and metadata management
- [ ] Create drawing preview and viewer
@@ -634,7 +615,6 @@
- [ ] **Dependencies:** F3.1, F4.4
- **[ ] F6.2 Shop Drawings Management**
- [ ] Create shop drawing list with revision tracking
- [ ] Implement shop drawing creation and revision system
- [ ] Create drawing comparison interface
@@ -645,7 +625,6 @@
- [ ] **Dependencies:** F6.1
- **[ ] F6.3 Drawing Revision System**
- [ ] Create revision history interface
- [ ] Implement revision comparison functionality
- [ ] Add revision notes and change tracking
@@ -681,7 +660,6 @@
### **Phase 7: Tasks**
- **[ ] F7.1 RFA List & Dashboard**
- [ ] Create RFA dashboard with status overview
- [ ] Implement advanced RFA filtering and search
- [ ] Create RFA calendar view for deadlines
@@ -692,7 +670,6 @@
- [ ] **Dependencies:** F4.1, F5.1
- **[ ] F7.2 RFA Creation with Dynamic Forms**
- [ ] Create RFA type-specific form generator
- [ ] Implement dynamic form fields based on RFA type:
- [ ] RFA_DWG: Shop drawing selection
@@ -712,7 +689,6 @@
- [ ] **Dependencies:** F4.2, F6.2
- **[ ] F7.3 RFA Workflow Integration**
- [ ] Integrate RFA with unified workflow engine
- [ ] Create RFA-specific workflow steps and actions
- [ ] Implement RFA approval interface
@@ -748,7 +724,6 @@
### **Phase 8: Tasks**
- **[ ] F8.1 Circulation Management**
- [ ] Create circulation list and management interface
- [ ] Implement circulation creation from correspondence
- [ ] Create circulation template management
@@ -759,7 +734,6 @@
- [ ] **Dependencies:** F4.1, F5.2
- **[ ] F8.2 Task Assignment Interface**
- [ ] Create task assignment interface with user selection
- [ ] Implement task priority and deadline setting
- [ ] Add task dependency management
@@ -795,7 +769,6 @@
### **Phase 9: Tasks**
- **[ ] F9.1 Advanced Search Interface**
- [ ] Create unified search interface across all document types
- [ ] Implement faceted search with multiple filters
- [ ] Add search result highlighting and relevance scoring
@@ -806,7 +779,6 @@
- [ ] **Dependencies:** F4.1, F7.1
- **[ ] F9.2 Notification System**
- [ ] Create notification center with real-time updates
- [ ] Implement notification preferences management
- [ ] Add notification grouping and digest views
@@ -817,7 +789,6 @@
- [ ] **Dependencies:** F1.3, F5.4
- **[ ] F9.3 Reporting & Analytics**
- [ ] Create reporting dashboard with customizable widgets
- [ ] Implement data visualization components (charts, graphs)
- [ ] Add report scheduling and export
@@ -853,7 +824,6 @@
### **Phase 10: Tasks**
- **[ ] F10.1 Comprehensive Testing**
- [ ] Idempotency Testing: เพิ่มการทดสอบเฉพาะสำหรับ Axios Interceptor เพื่อจำลองการส่ง Request POST/PUT/DELETE ที่มี Idempotency-Key ซ้ำไปยัง Mock API (MSW) เพื่อยืนยันว่า Client-side ไม่ส่ง Key ซ้ำในการทำงานปกติ และไม่เกิด Side Effect จากการ Replay Attack.
- [ ] Write unit tests for all components and utilities
- [ ] Create integration tests for critical user flows
@@ -865,7 +835,6 @@
- [ ] **Dependencies:** All previous phases
- **[ ] F10.2 Performance Optimization**
- [ ] Implement code splitting and lazy loading
- [ ] Optimize bundle size and asset delivery
- [ ] Add performance monitoring and metrics
@@ -876,7 +845,6 @@
- [ ] **Dependencies:** F10.1
- **[ ] F10.3 Security Hardening**
- [ ] Conduct security audit and penetration testing
- [ ] Implement Content Security Policy (CSP)
- [ ] Add security headers and protections
@@ -17,15 +17,15 @@
### **Technology Stack**
- **Framework:** Next.js 14+ (App Router, React 18, TypeScript)
- **Styling:** Tailwind CSS + Shadcn/UI
- **State Management:**
- **Server:** TanStack Query (React Query)
- **Client:** Zustand
- **Form:** React Hook Form + Zod
- **Workflow Visualization:** ReactFlow (สำหรับ Workflow Builder)
- **Editor:** Monaco Editor (สำหรับ DSL Editing)
- **Validation:** Zod + AJV (JSON Schema)
- **Framework:** Next.js 14+ (App Router, React 18, TypeScript)
- **Styling:** Tailwind CSS + Shadcn/UI
- **State Management:**
- **Server:** TanStack Query (React Query)
- **Client:** Zustand
- **Form:** React Hook Form + Zod
- **Workflow Visualization:** ReactFlow (สำหรับ Workflow Builder)
- **Editor:** Monaco Editor (สำหรับ DSL Editing)
- **Validation:** Zod + AJV (JSON Schema)
### **โครงสร้างโมดูล (Module Structure)**
@@ -61,17 +61,19 @@
**Goal:** เตรียมโครงสร้างโปรเจกต์และ Component พื้นฐาน
#### **[ ] F1.1 Project Setup & Design System**
- **Tasks:**
- [ ] Setup Next.js 14 + Tailwind + Shadcn/UI
- [ ] Configure Axios with **Idempotency Interceptor**
- [ ] Implement Base Layout (Sidebar, Navbar, Breadcrumbs)
- [ ] Setup **TanStack Query** & **Zustand**
- **Tasks:**
- [ ] Setup Next.js 14 + Tailwind + Shadcn/UI
- [ ] Configure Axios with **Idempotency Interceptor**
- [ ] Implement Base Layout (Sidebar, Navbar, Breadcrumbs)
- [ ] Setup **TanStack Query** & **Zustand**
#### **[ ] F1.2 Authentication UI**
- **Tasks:**
- [ ] Login Page with Form Validation
- [ ] Integrate NextAuth.js with Backend
- [ ] Implement RBAC Guard (Protect Routes based on Permissions)
- **Tasks:**
- [ ] Login Page with Form Validation
- [ ] Integrate NextAuth.js with Backend
- [ ] Implement RBAC Guard (Protect Routes based on Permissions)
---
@@ -80,27 +82,30 @@
**Goal:** ระบบจัดการผู้ใช้และข้อมูลหลัก (รองรับ v1.5.1 Requirements)
#### **[ ] F2.1 User & Role Management**
- **Tasks:**
- [ ] User List & CRUD (Admin only)
- [ ] Role Assignment UI
- [ ] Permission Matrix Viewer
- **Tasks:**
- [ ] User List & CRUD (Admin only)
- [ ] Role Assignment UI
- [ ] Permission Matrix Viewer
#### **[ ] F2.2 Enhanced Master Data UI**
- **Tasks:**
- [ ] **Organization Management:** CRUD + Logo Upload
- [ ] **Project & Contract Management:** CRUD + Relations
- [ ] **[NEW] Discipline Management:** CRUD for `disciplines`
- [ ] **[NEW] Sub-Type Management:** CRUD for `correspondence_sub_types`
- **Tasks:**
- [ ] **Organization Management:** CRUD + Logo Upload
- [ ] **Project & Contract Management:** CRUD + Relations
- [ ] **[NEW] Discipline Management:** CRUD for `disciplines`
- [ ] **[NEW] Sub-Type Management:** CRUD for `correspondence_sub_types`
#### **[ ] F2.3 System Configuration**
- **Tasks:**
- [ ] **[NEW] Document Numbering Config:**
- Template Editor (Monaco/Visual)
- Sequence Viewer
- [ ] **[NEW] Workflow Configuration:**
- Workflow List
- DSL Editor (Monaco)
- Visual Builder (ReactFlow)
- **Tasks:**
- [ ] **[NEW] Document Numbering Config:**
- Template Editor (Monaco/Visual)
- Sequence Viewer
- [ ] **[NEW] Workflow Configuration:**
- Workflow List
- DSL Editor (Monaco)
- Visual Builder (ReactFlow)
---
@@ -109,23 +114,25 @@
**Goal:** โมดูลหลัก Correspondence และ RFA
#### **[ ] F3.1 Correspondence Module**
- **Tasks:**
- [ ] List View with Advanced Filters
- [ ] **Create/Edit Form:**
- Add **Discipline Selector** (Dynamic based on Contract)
- Add **Sub-Type Selector** (Dynamic based on Type)
- File Upload (Two-Phase)
- [ ] Detail View with History & Comments
- **Tasks:**
- [ ] List View with Advanced Filters
- [ ] **Create/Edit Form:**
- Add **Discipline Selector** (Dynamic based on Contract)
- Add **Sub-Type Selector** (Dynamic based on Type)
- File Upload (Two-Phase)
- [ ] Detail View with History & Comments
#### **[ ] F3.2 RFA Module**
- **Tasks:**
- [ ] RFA List & Dashboard
- [ ] **Dynamic RFA Form:**
- Fields change based on RFA Type (DWG, MAT, MES)
- Item List Management
- [ ] **Approval Interface:**
- Approve/Reject/Comment Actions
- Workflow Status Visualization
- **Tasks:**
- [ ] RFA List & Dashboard
- [ ] **Dynamic RFA Form:**
- Fields change based on RFA Type (DWG, MAT, MES)
- Item List Management
- [ ] **Approval Interface:**
- Approve/Reject/Comment Actions
- Workflow Status Visualization
---
@@ -134,22 +141,25 @@
**Goal:** โมดูล Drawing, Transmittal และ Circulation
#### **[ ] F4.1 Drawing Module**
- **Tasks:**
- [ ] Shop Drawing & Contract Drawing Lists
- [ ] Revision Management UI
- [ ] Drawing Viewer (PDF/Image)
- **Tasks:**
- [ ] Shop Drawing & Contract Drawing Lists
- [ ] Revision Management UI
- [ ] Drawing Viewer (PDF/Image)
#### **[ ] F4.2 Transmittal Module**
- **Tasks:**
- [ ] Transmittal Creation Form (Select Documents to send)
- [ ] Transmittal Letter Preview (PDF Generation)
- [ ] Transmittal History
- **Tasks:**
- [ ] Transmittal Creation Form (Select Documents to send)
- [ ] Transmittal Letter Preview (PDF Generation)
- [ ] Transmittal History
#### **[ ] F4.3 Circulation Module**
- **Tasks:**
- [ ] Circulation Sheet Creation (Select Assignees)
- [ ] "My Tasks" Dashboard for Circulation
- [ ] Completion & Tracking UI
- **Tasks:**
- [ ] Circulation Sheet Creation (Select Assignees)
- [ ] "My Tasks" Dashboard for Circulation
- [ ] Completion & Tracking UI
---
@@ -158,15 +168,17 @@
**Goal:** การค้นหาและหน้า Dashboard
#### **[ ] F5.1 Advanced Search**
- **Tasks:**
- [ ] Unified Search Interface (Elasticsearch)
- [ ] Faceted Filters (Type, Date, Project, Status)
- **Tasks:**
- [ ] Unified Search Interface (Elasticsearch)
- [ ] Faceted Filters (Type, Date, Project, Status)
#### **[ ] F5.2 Dashboard & Monitoring**
- **Tasks:**
- [ ] Personal Dashboard (My Tasks, Pending Approvals)
- [ ] Project Dashboard (KPIs, Stats)
- [ ] **Admin Audit Logs Viewer**
- **Tasks:**
- [ ] Personal Dashboard (My Tasks, Pending Approvals)
- [ ] Project Dashboard (KPIs, Stats)
- [ ] **Admin Audit Logs Viewer**
---
@@ -2159,28 +2159,23 @@ WHERE user_id = ?
**Additional Performance Indexes**:
1. **Correspondence Tables**:
- `idx_correspondences_type_project` on (correspondence_type_id, project_id)
- `idx_corr_revisions_current_status` on (is_current, correspondence_status_id)
- `idx_corr_revisions_correspondence_current` on (correspondence_id, is_current)
- `idx_correspondences_project_type` on (project_id, correspondence_type_id)
2. **RFA Tables**:
- `idx_rfa_revisions_current_status` on (is_current, rfa_status_code_id)
- `idx_rfa_revisions_rfa_current` on (rfa_id, is_current)
3. **Circulation Tables**:
- `idx_circulation_routings_status_assigned` on (status, assigned_to)
- `idx_circulation_routings_circulation_status` on (circulation_id, status)
4. **Document Numbering**:
- `idx_doc_counter_composite` on (project_id, originator_organization_id, correspondence_type_id, current_year)
5. **Audit & Notifications**:
- `idx_audit_logs_reporting` on (created_at, entity_type, action)
- `idx_notifications_user_unread` on (user_id, is_read, created_at)
@@ -2208,12 +2203,10 @@ WHERE user_id = ?
### Unique Constraints
1. **Globally Unique**:
- usernames, emails
- shop_drawing.drawing_number
2. **Unique Within Scope**:
- (project_id, correspondence_number)
- (project_id, condwg_no)
- (correspondence_id, revision_number)
@@ -2232,13 +2225,11 @@ WHERE user_id = ?
### Business Rule Constraints
1. **Soft Delete Pattern**:
- deleted_at timestamp instead of hard delete
- Preserves audit trail and relationships
- Applied to: correspondences, rfas, shop_drawings, contract_drawings
2. **Current Revision Pattern**:
- is_current flag with UNIQUE constraint
- Ensures only one current revision per document
@@ -2423,13 +2414,11 @@ ANALYZE TABLE correspondences;
### Business Logic Validation
1. **Document Workflow**:
- Cannot edit submitted documents (unless Document Control)
- Cannot skip workflow steps (unless forced)
- Must provide approval comments
2. **User Management**:
- Cannot delete users with active assignments
- Cannot deactivate own account
- Must have valid organization for non-Global roles
@@ -2545,19 +2534,16 @@ ANALYZE TABLE correspondences;
### Integration Points
1. **Document Numbering**:
- Call DocumentNumberingService.generateNextNumber() (NestJS) which handles Redis locking and retry logic
- Format with template from document_number_formats
- Store in correspondences.correspondence_number
2. **File Upload**:
- Upload to QNAP /share/dms-data/
- Create attachment record
- Link via junction table
3. **Workflow Execution**:
- Check rfa_workflow_templates
- Create rfa_workflows records
- Update status as steps complete
@@ -123,16 +123,17 @@
**Purpose**: เก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา (Req 6B)
| Column Name | Data Type | Constraints | Description |
| :-------------- | :----------- | :----------- | :--------------------- |
| id | INT | PK, AI | Unique identifier |
| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา |
| Column Name | Data Type | Constraints | Description |
| :-------------- | :----------- | :----------- | :----------------------- |
| id | INT | PK, AI | Unique identifier |
| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา |
| discipline_code | VARCHAR(10) | NOT NULL | รหัสสาขา (เช่น GEN, STR) |
| code_name_th | VARCHAR(255) | NULL | ชื่อไทย |
| code_name_en | VARCHAR(255) | NULL | ชื่ออังกฤษ |
| is_active | TINYINT(1) | DEFAULT 1 | สถานะการใช้งาน |
| code_name_en | VARCHAR(255) | NULL | ชื่ออังกฤษ |
| is_active | TINYINT(1) | DEFAULT 1 | สถานะการใช้งาน |
**Indexes**:
- UNIQUE (contract_id, discipline_code)
---
@@ -334,13 +335,13 @@
**Purpose**: เก็บการตั้งค่าส่วนตัวของผู้ใช้ (Req 5.5, 6.8.3)
| Column Name | Data Type | Constraints | Description |
| :----------- | :---------- | :-------------- | :-------------- |
| user_id | INT | PK, FK | User ID |
| notify_email | BOOLEAN | DEFAULT TRUE | รับอีเมลแจ้งเตือน |
| notify_line | BOOLEAN | DEFAULT TRUE | รับไลน์แจ้งเตือน |
| Column Name | Data Type | Constraints | Description |
| :----------- | :---------- | :-------------- | :----------------- |
| user_id | INT | PK, FK | User ID |
| notify_email | BOOLEAN | DEFAULT TRUE | รับอีเมลแจ้งเตือน |
| notify_line | BOOLEAN | DEFAULT TRUE | รับไลน์แจ้งเตือน |
| digest_mode | BOOLEAN | DEFAULT FALSE | รับแจ้งเตือนแบบรวม |
| ui_theme | VARCHAR(20) | DEFAULT 'light' | UI Theme |
| ui_theme | VARCHAR(20) | DEFAULT 'light' | UI Theme |
---
@@ -375,13 +376,13 @@
**Purpose**: เก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส (Req 6B)
| Column Name | Data Type | Constraints | Description |
| :--------------------- | :----------- | :----------- | :------------------------ |
| id | INT | PK, AI | Unique identifier |
| Column Name | Data Type | Constraints | Description |
| :--------------------- | :----------- | :----------- | :--------------------------- |
| id | INT | PK, AI | Unique identifier |
| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา |
| correspondence_type_id | INT | FK, NOT NULL | ผูกกับประเภทเอกสารหลัก |
| sub_type_code | VARCHAR(20) | NOT NULL | รหัสย่อย (เช่น MAT, SHP) |
| sub_type_name | VARCHAR(255) | NULL | ชื่อประเภทหนังสือย่อย |
| sub_type_name | VARCHAR(255) | NULL | ชื่อประเภทหนังสือย่อย |
| sub_type_number | VARCHAR(10) | NULL | เลขรหัสสำหรับ Running Number |
---
@@ -395,7 +396,7 @@
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID |
| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) |
| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types |
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
| is_internal_communication | TINYINT(1) | DEFAULT 0 | Internal (1) or external (0) communication |
| project_id | INT | NOT NULL, FK | Reference to projects table |
| originator_id | INT | NULL, FK | Originating organization |
@@ -427,28 +428,28 @@
**Purpose**: Child table storing revision history of correspondences (1:N)
| Column Name | Data Type | Constraints | Description |
| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| correspondence_id | INT | NOT NULL, FK | Master correspondence ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag |
| correspondence_status_id | INT | NOT NULL, FK | Current status of this revision |
| title | VARCHAR(255) | NOT NULL | Document title |
| document_date | DATE | NULL | Document date |
| issued_date | DATETIME | NULL | Issue date |
| received_date | DATETIME | NULL | Received date |
| due_date | DATETIME | NULL | Due date for response |
| description | TEXT | NULL | Revision description |
| details | JSON | NULL | Type-specific details (e.g., RFI questions) |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp |
| created_by | INT | NULL, FK | User who created revision |
| updated_by | INT | NULL, FK | User who last updated |
| Column Name | Data Type | Constraints | Description |
| ------------------------ | ------------ | --------------------------------- | ------------------------------------------------------------ |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| correspondence_id | INT | NOT NULL, FK | Master correspondence ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag |
| correspondence_status_id | INT | NOT NULL, FK | Current status of this revision |
| title | VARCHAR(255) | NOT NULL | Document title |
| document_date | DATE | NULL | Document date |
| issued_date | DATETIME | NULL | Issue date |
| received_date | DATETIME | NULL | Received date |
| due_date | DATETIME | NULL | Due date for response |
| description | TEXT | NULL | Revision description |
| details | JSON | NULL | Type-specific details (e.g., RFI questions) |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp |
| created_by | INT | NULL, FK | User who created revision |
| updated_by | INT | NULL, FK | User who last updated |
| v_ref_project_id | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Project ID จาก JSON details เพื่อทำ Index |
| v_ref_type | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details |
| v_doc_subtype | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details |
| schema_version | INT | DEFAULT 1 | Version of the schema used with this details |
| v_ref_type | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details |
| v_doc_subtype | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details |
| schema_version | INT | DEFAULT 1 | Version of the schema used with this details |
**Indexes**:
@@ -649,7 +650,7 @@
| ----------------- | --------- | --------------------------- | --------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master RFA ID |
| rfa_type_id | INT | NOT NULL, FK | Reference to rfa_types |
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
| created_by | INT | NULL, FK | User who created the record |
| deleted_at | DATETIME | NULL | Soft delete timestamp |
@@ -674,28 +675,28 @@
**Purpose**: Child table storing revision history of RFAs (1:N)
| Column Name | Data Type | Constraints | Description |
| ------------------- | ------------ | --------------------------------- | ----------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| correspondence_id | INT | NOT NULL, FK | Link to correspondence (RFA as correspondence) |
| rfa_id | INT | NOT NULL, FK | Master RFA ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag |
| rfa_status_code_id | INT | NOT NULL, FK | Current RFA status |
| rfa_approve_code_id | INT | NULL, FK | Approval result code |
| title | VARCHAR(255) | NOT NULL | RFA title |
| document_date | DATE | NULL | Document date |
| issued_date | DATE | NULL | Issue date for approval |
| received_date | DATETIME | NULL | Received date |
| approved_date | DATE | NULL | Approval date |
| description | TEXT | NULL | Revision description |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp |
| created_by | INT | NULL, FK | User who created revision |
| updated_by | INT | NULL, FK | User who last updated |
| details | JSON | NULL | Type-specific details (e.g., RFI questions) |
| Column Name | Data Type | Constraints | Description |
| ------------------- | ------------ | --------------------------------- | --------------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| correspondence_id | INT | NOT NULL, FK | Link to correspondence (RFA as correspondence) |
| rfa_id | INT | NOT NULL, FK | Master RFA ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag |
| rfa_status_code_id | INT | NOT NULL, FK | Current RFA status |
| rfa_approve_code_id | INT | NULL, FK | Approval result code |
| title | VARCHAR(255) | NOT NULL | RFA title |
| document_date | DATE | NULL | Document date |
| issued_date | DATE | NULL | Issue date for approval |
| received_date | DATETIME | NULL | Received date |
| approved_date | DATE | NULL | Approval date |
| description | TEXT | NULL | Revision description |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp |
| created_by | INT | NULL, FK | User who created revision |
| updated_by | INT | NULL, FK | User who last updated |
| details | JSON | NULL | Type-specific details (e.g., RFI questions) |
| v_ref_drawing_count | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Drawing Count จาก JSON details เพื่อทำ Index |
| schema_version | INT | DEFAULT 1 | Version of the schema used with this details |
| schema_version | INT | DEFAULT 1 | Version of the schema used with this details |
**Indexes**:
@@ -752,20 +753,20 @@
**Purpose**: Transaction log table tracking actual RFA approval workflow execution
| Column Name | Data Type | Constraints | Description |
| --------------- | --------- | ----------------------------------- | ------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique workflow log ID |
| rfa_revision_id | INT | NOT NULL, FK | Reference to RFA revision |
| step_number | INT | NOT NULL | Current step number |
| organization_id | INT | NOT NULL, FK | Organization responsible |
| assigned_to | INT | NULL, FK | Assigned user ID |
| action_type | ENUM | NULL | Action type: REVIEW, APPROVE, ACKNOWLEDGE |
| status | ENUM | NULL | Status: PENDING, IN_PROGRESS, COMPLETED, REJECTED |
| comments | TEXT | NULL | Comments/remarks |
| completed_at | DATETIME | NULL | Completion timestamp |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
| state_context | JSON\* | NULL | เก็บข้อมูล Context ของ Workflow ณ ขณะนั้น (Snapshot) |
| Column Name | Data Type | Constraints | Description |
| --------------- | --------- | ----------------------------------- | ---------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique workflow log ID |
| rfa_revision_id | INT | NOT NULL, FK | Reference to RFA revision |
| step_number | INT | NOT NULL | Current step number |
| organization_id | INT | NOT NULL, FK | Organization responsible |
| assigned_to | INT | NULL, FK | Assigned user ID |
| action_type | ENUM | NULL | Action type: REVIEW, APPROVE, ACKNOWLEDGE |
| status | ENUM | NULL | Status: PENDING, IN_PROGRESS, COMPLETED, REJECTED |
| comments | TEXT | NULL | Comments/remarks |
| completed_at | DATETIME | NULL | Completion timestamp |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
| state_context | JSON\* | NULL | เก็บข้อมูล Context ของ Workflow ณ ขณะนั้น (Snapshot) |
**Indexes**:
@@ -1293,20 +1294,20 @@
**Purpose**: Central repository for all file attachments in the system
| Column Name | Data Type | Constraints | Description |
| ------------------- | ------------ | --------------------------- | -------------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID |
| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload |
| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename |
| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) |
| mime_type | VARCHAR(100) | NOT NULL | MIME type (application/pdf, image/jpeg, etc.) |
| file_size | INT | NOT NULL | File size in bytes |
| uploaded_by_user_id | INT | NOT NULL, FK | User who uploaded file |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Upload timestamp |
| is_temporary | BOOLEAN | DEFAULT TRUE | ระบุว่าเป็นไฟล์ชั่วคราว (ยังไม่ได้ Commit) |
| Column Name | Data Type | Constraints | Description |
| ------------------- | ------------ | --------------------------- | -------------------------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID |
| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload |
| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename |
| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) |
| mime_type | VARCHAR(100) | NOT NULL | MIME type (application/pdf, image/jpeg, etc.) |
| file_size | INT | NOT NULL | File size in bytes |
| uploaded_by_user_id | INT | NOT NULL, FK | User who uploaded file |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Upload timestamp |
| is_temporary | BOOLEAN | DEFAULT TRUE | ระบุว่าเป็นไฟล์ชั่วคราว (ยังไม่ได้ Commit) |
| temp_id\* | VARCHAR(100) | NULL | ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1 (อาจใช้ร่วมกับ id หรือแยกก็ได้) |
| expires_at | DATETIME | NULL | เวลาหมดอายุของไฟล์ Temp (เพื่อให้ Cron Job ลบออก) |
| checksum | VARCHAR(64) | NULL | SHA-256 Checksum สำหรับ Verify File Integrity [Req 3.9.3] |
| expires_at | DATETIME | NULL | เวลาหมดอายุของไฟล์ Temp (เพื่อให้ Cron Job ลบออก) |
| checksum | VARCHAR(64) | NULL | SHA-256 Checksum สำหรับ Verify File Integrity [Req 3.9.3] |
**Indexes**:
@@ -1482,17 +1483,17 @@
**Purpose**: Transaction table tracking running numbers (High Concurrency)
| Column Name | Data Type | Constraints | Description |
| -------------------------- | --------- | ------------- | -------------------------------------------- |
| project_id | INT | PK, NOT NULL | โครงการ |
| originator_organization_id | INT | PK, NOT NULL | องค์กรผู้ส่ง |
| recipient_organization_id | INT | PK, NOT NULL | [NEW] องค์กรผู้รับ (-1 = ทุกองค์กร) |
| correspondence_type_id | INT | PK, NOT NULL | ประเภทเอกสาร |
| Column Name | Data Type | Constraints | Description |
| -------------------------- | --------- | ------------- | ------------------------------------------------- |
| project_id | INT | PK, NOT NULL | โครงการ |
| originator_organization_id | INT | PK, NOT NULL | องค์กรผู้ส่ง |
| recipient_organization_id | INT | PK, NOT NULL | [NEW] องค์กรผู้รับ (-1 = ทุกองค์กร) |
| correspondence_type_id | INT | PK, NOT NULL | ประเภทเอกสาร |
| sub_type_id | INT | PK, DEFAULT 0 | [NEW] ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ) |
| rfa_type_id | INT | PK, DEFAULT 0 | [NEW] ประเภท RFA (0 = ไม่ใช่ RFA) |
| discipline_id | INT | PK, DEFAULT 0 | [NEW] สาขางาน (0 = ไม่ระบุ) |
| rfa_type_id | INT | PK, DEFAULT 0 | [NEW] ประเภท RFA (0 = ไม่ใช่ RFA) |
| discipline_id | INT | PK, DEFAULT 0 | [NEW] สาขางาน (0 = ไม่ระบุ) |
| current_year | INT | PK, NOT NULL | ปี ค.ศ. ของตัวนับ (auto-reset ทุกปี) |
| last_number | INT | DEFAULT 0 | เลขล่าสุดที่ถูกใช้งานไปแล้ว |
| last_number | INT | DEFAULT 0 | เลขล่าสุดที่ถูกใช้งานไปแล้ว |
| updated_at | TIMESTAMP | ON UPDATE | เวลาที่อัปเดตล่าสุด |
**Indexes**:
@@ -1511,15 +1512,15 @@
**Purpose**: Audit log for document number generation (Debugging & Tracking)
| Column Name | Data Type | Constraints | Description |
| :---------------- | :----------- | :---------- | :---------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| transaction_id | VARCHAR(36) | NOT NULL | UUID ของ Transaction การขอเลข |
| Column Name | Data Type | Constraints | Description |
| :---------------- | :----------- | :---------- | :---------------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| transaction_id | VARCHAR(36) | NOT NULL | UUID ของ Transaction การขอเลข |
| counter_key_json | JSON | NOT NULL | ค่า Key ที่ใช้ในการ Query (เก็บเป็น JSON) |
| generated_number | VARCHAR(100) | NOT NULL | เลขที่ได้ |
| requested_by | INT | FK | User ที่ขอเลข |
| requested_at | TIMESTAMP | DEFAULT NOW | เวลาที่ขอ |
| execution_time_ms | INT | NULL | เวลาที่ใช้ในการประมวลผล (ms) |
| generated_number | VARCHAR(100) | NOT NULL | เลขที่ได้ |
| requested_by | INT | FK | User ที่ขอเลข |
| requested_at | TIMESTAMP | DEFAULT NOW | เวลาที่ขอ |
| execution_time_ms | INT | NULL | เวลาที่ใช้ในการประมวลผล (ms) |
---
@@ -1527,14 +1528,14 @@
**Purpose**: Error log for failed document number generation
| Column Name | Data Type | Constraints | Description |
| :--------------- | :---------- | :---------- | :------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| transaction_id | VARCHAR(36) | NOT NULL | UUID ของ Transaction |
| Column Name | Data Type | Constraints | Description |
| :--------------- | :---------- | :---------- | :--------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| transaction_id | VARCHAR(36) | NOT NULL | UUID ของ Transaction |
| error_code | VARCHAR(50) | NOT NULL | รหัส Error (เช่น ERR_LOCK_TIMEOUT) |
| error_message | TEXT | NOT NULL | รายละเอียด Error |
| counter_key_json | JSON | NULL | ค่า Key ที่พยายามใช้ |
| occurred_at | TIMESTAMP | DEFAULT NOW | เวลาที่เกิด Error |
| error_message | TEXT | NOT NULL | รายละเอียด Error |
| counter_key_json | JSON | NULL | ค่า Key ที่พยายามใช้ |
| occurred_at | TIMESTAMP | DEFAULT NOW | เวลาที่เกิด Error |
---
@@ -1544,18 +1545,19 @@
**Purpose**: เก็บแม่แบบ (Template) ของ Workflow (Req 3.6)
| Column Name | Data Type | Constraints | Description |
| :------------ | :----------- | :----------- | :--------------------------------------------- |
| id | INT | PK, AI | Unique ID |
| workflow_code | VARCHAR(50) | UNIQUE | รหัส Workflow (เช่น WF-RFA-GENERIC) |
| workflow_name | VARCHAR(255) | NOT NULL | ชื่อ Workflow |
| description | TEXT | NULL | คำอธิบาย |
| module | VARCHAR(50) | NOT NULL | ใช้กับ Module ไหน (RFA, CORRESPONDENCE) |
| Column Name | Data Type | Constraints | Description |
| :------------ | :----------- | :----------- | :------------------------------------------------ |
| id | INT | PK, AI | Unique ID |
| workflow_code | VARCHAR(50) | UNIQUE | รหัส Workflow (เช่น WF-RFA-GENERIC) |
| workflow_name | VARCHAR(255) | NOT NULL | ชื่อ Workflow |
| description | TEXT | NULL | คำอธิบาย |
| module | VARCHAR(50) | NOT NULL | ใช้กับ Module ไหน (RFA, CORRESPONDENCE) |
| steps_config | JSON | NOT NULL | การตั้งค่า Step (Sequence, Approvers, Conditions) |
| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน |
| version | INT | DEFAULT 1 | เวอร์ชันของ Definition |
| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน |
| version | INT | DEFAULT 1 | เวอร์ชันของ Definition |
**Business Rules**:
- `steps_config` เก็บ Logic ของ Workflow ทั้งหมดในรูปแบบ JSON เพื่อความยืดหยุ่น
---
@@ -1564,16 +1566,16 @@
**Purpose**: เก็บสถานะของ Workflow ที่กำลังรันอยู่จริง (Runtime)
| Column Name | Data Type | Constraints | Description |
| :--------------------- | :----------- | :----------- | :----------------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| workflow_definition_id | INT | FK, NOT NULL | อ้างอิง Definition |
| Column Name | Data Type | Constraints | Description |
| :--------------------- | :----------- | :----------- | :------------------------------------------------ |
| id | BIGINT | PK, AI | Unique ID |
| workflow_definition_id | INT | FK, NOT NULL | อ้างอิง Definition |
| business_key | VARCHAR(100) | INDEX | ID ของเอกสารที่ผูกกับ Workflow นี้ (เช่น RFA-001) |
| current_step_name | VARCHAR(100) | NOT NULL | ชื่อ Step ปัจจุบัน |
| status | ENUM | NOT NULL | IN_PROGRESS, COMPLETED, TERMINATED |
| context_data | JSON | NULL | ข้อมูลประกอบการตัดสินใจ (Variables) |
| started_at | TIMESTAMP | DEFAULT NOW | เวลาที่เริ่ม |
| completed_at | TIMESTAMP | NULL | เวลาที่จบ |
| current_step_name | VARCHAR(100) | NOT NULL | ชื่อ Step ปัจจุบัน |
| status | ENUM | NOT NULL | IN_PROGRESS, COMPLETED, TERMINATED |
| context_data | JSON | NULL | ข้อมูลประกอบการตัดสินใจ (Variables) |
| started_at | TIMESTAMP | DEFAULT NOW | เวลาที่เริ่ม |
| completed_at | TIMESTAMP | NULL | เวลาที่จบ |
---
@@ -1581,15 +1583,15 @@
**Purpose**: เก็บประวัติการดำเนินการในแต่ละ Step (Audit Trail)
| Column Name | Data Type | Constraints | Description |
| :------------------- | :----------- | :----------- | :--------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| workflow_instance_id | BIGINT | FK, NOT NULL | อ้างอิง Instance |
| step_name | VARCHAR(100) | NOT NULL | ชื่อ Step |
| Column Name | Data Type | Constraints | Description |
| :------------------- | :----------- | :----------- | :---------------------------------- |
| id | BIGINT | PK, AI | Unique ID |
| workflow_instance_id | BIGINT | FK, NOT NULL | อ้างอิง Instance |
| step_name | VARCHAR(100) | NOT NULL | ชื่อ Step |
| action | VARCHAR(50) | NOT NULL | การกระทำ (APPROVE, REJECT, COMMENT) |
| actor_id | INT | FK, NULL | User ที่กระทำ |
| comments | TEXT | NULL | ความเห็นเพิ่มเติม |
| performed_at | TIMESTAMP | DEFAULT NOW | เวลาที่กระทำ |
| actor_id | INT | FK, NULL | User ที่กระทำ |
| comments | TEXT | NULL | ความเห็นเพิ่มเติม |
| performed_at | TIMESTAMP | DEFAULT NOW | เวลาที่กระทำ |
---
@@ -1602,9 +1604,9 @@
| Column Name | Data Type | Constraints | Description |
| :---------- | :---------- | :----------- | :------------------------------------- |
| id | INT | PK, AI | Unique ID |
| schema_code | VARCHAR(50) | UNIQUE | รหัส Schema (เช่น RFA_DETAILS_V1) |
| schema_code | VARCHAR(50) | UNIQUE | รหัส Schema (เช่น RFA_DETAILS_V1) |
| schema_body | JSON | NOT NULL | JSON Schema Draft 7/2020-12 definition |
| description | TEXT | NULL | คำอธิบาย |
| description | TEXT | NULL | คำอธิบาย |
| is_active | BOOLEAN | DEFAULT TRUE | สถานะ |
---
@@ -1636,6 +1638,7 @@
- INDEX (entity_id)
**Partitioning**:
- **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี เพื่อประสิทธิภาพในการเก็บข้อมูลระยะยาว
---
@@ -1663,6 +1666,7 @@
- INDEX (created_at)
**Partitioning**:
- **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี
---
@@ -1861,6 +1865,3 @@ LEFT JOIN rfa_approve_codes rac ON rr.rfa_approve_code_id = rac.id;
---
**End of Data Dictionary V1.5.1**
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,7 @@
## 1. Architecture Overview
### 1.1 Module Structure
```
backend/src/modules/document-numbering/
├── document-numbering.module.ts
@@ -53,6 +54,7 @@ backend/src/modules/document-numbering/
## 2. Core Entities
### 2.1 Numbering Configuration
```typescript
// entities/numbering-config.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
@@ -71,7 +73,7 @@ export class NumberingConfig {
@Column({
type: 'enum',
enum: ['GLOBAL', 'PROJECT', 'CONTRACT', 'YEARLY', 'MONTHLY'],
default: 'GLOBAL'
default: 'GLOBAL',
})
scope: string;
@@ -90,16 +92,17 @@ export class NumberingConfig {
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP'
onUpdate: 'CURRENT_TIMESTAMP',
})
updated_at: Date;
@OneToMany(() => NumberingSequence, sequence => sequence.config)
@OneToMany(() => NumberingSequence, (sequence) => sequence.config)
sequences: NumberingSequence[];
}
```
### 2.2 Sequence Counter
```typescript
// entities/numbering-sequence.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
@@ -134,6 +137,7 @@ export class NumberingSequence {
```
### 2.3 Audit Log
```typescript
// entities/numbering-audit-log.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@@ -184,6 +188,7 @@ export class NumberingAuditLog {
## 3. Core Services
### 3.1 Sequence Service
```typescript
// services/sequence.service.ts
import { Injectable, Logger } from '@nestjs/common';
@@ -206,16 +211,13 @@ export class SequenceService {
private dataSource: DataSource,
private redlock: Redlock,
private formatService: FormatService,
private formatService: FormatService
) {}
/**
* Get next sequence number with distributed locking
*/
async getNextSequence(
documentType: string,
scopeValue?: string,
): Promise<string> {
async getNextSequence(documentType: string, scopeValue?: string): Promise<string> {
// 1. Get configuration
const config = await this.getConfig(documentType);
@@ -238,11 +240,7 @@ export class SequenceService {
/**
* Get sequence with Redlock + Database pessimistic lock
*/
private async getSequenceWithRedlock(
config: NumberingConfig,
scopeValue: string,
lockKey: string,
): Promise<string> {
private async getSequenceWithRedlock(config: NumberingConfig, scopeValue: string, lockKey: string): Promise<string> {
// Acquire distributed lock
const lock = await this.redlock.acquire([lockKey], 5000, {
retryCount: 3,
@@ -266,11 +264,7 @@ export class SequenceService {
}
// Increment sequence
const nextValue = await this.incrementSequence(
manager,
sequence,
config,
);
const nextValue = await this.incrementSequence(manager, sequence, config);
// Format number
return this.formatService.formatNumber(config.format, nextValue, {
@@ -287,10 +281,7 @@ export class SequenceService {
/**
* Fallback: Database-only locking (no Redis)
*/
private async getSequenceWithDbLock(
config: NumberingConfig,
scopeValue: string,
): Promise<string> {
private async getSequenceWithDbLock(config: NumberingConfig, scopeValue: string): Promise<string> {
return await this.dataSource.transaction(async (manager) => {
let sequence = await manager.findOne(NumberingSequence, {
where: {
@@ -304,11 +295,7 @@ export class SequenceService {
sequence = await this.createSequence(manager, config, scopeValue);
}
const nextValue = await this.incrementSequence(
manager,
sequence,
config,
);
const nextValue = await this.incrementSequence(manager, sequence, config);
return this.formatService.formatNumber(config.format, nextValue, {
documentType: config.document_type,
@@ -323,7 +310,7 @@ export class SequenceService {
private async incrementSequence(
manager: EntityManager,
sequence: NumberingSequence,
config: NumberingConfig,
config: NumberingConfig
): Promise<number> {
let nextValue = sequence.current_value + 1;
@@ -335,9 +322,7 @@ export class SequenceService {
// Check max value
if (nextValue > config.max_value) {
throw new SequenceExhaustedError(
`Sequence exhausted for ${config.document_type}. Max: ${config.max_value}`,
);
throw new SequenceExhaustedError(`Sequence exhausted for ${config.document_type}. Max: ${config.max_value}`);
}
// Update sequence
@@ -351,11 +336,7 @@ export class SequenceService {
/**
* Check if number is cancelled
*/
private async isCancelledNumber(
manager: EntityManager,
config: NumberingConfig,
value: number,
): Promise<boolean> {
private async isCancelledNumber(manager: EntityManager, config: NumberingConfig, value: number): Promise<boolean> {
const count = await manager.count(NumberingAuditLog, {
where: {
document_type: config.document_type,
@@ -372,7 +353,7 @@ export class SequenceService {
private async createSequence(
manager: EntityManager,
config: NumberingConfig,
scopeValue: string,
scopeValue: string
): Promise<NumberingSequence> {
const sequence = manager.create(NumberingSequence, {
config_id: config.id,
@@ -395,8 +376,7 @@ export class SequenceService {
* Check if error is Redis unavailable
*/
private isRedisUnavailable(error: any): boolean {
return error.message?.includes('Redis') ||
error.message?.includes('ECONNREFUSED');
return error.message?.includes('Redis') || error.message?.includes('ECONNREFUSED');
}
/**
@@ -409,9 +389,7 @@ export class SequenceService {
});
if (!config) {
throw new ConfigNotFoundError(
`Numbering config not found for ${documentType}`,
);
throw new ConfigNotFoundError(`Numbering config not found for ${documentType}`);
}
return config;
@@ -422,6 +400,7 @@ export class SequenceService {
---
### 3.2 Reservation Service
```typescript
// services/reservation.service.ts
import { Injectable, Logger } from '@nestjs/common';
@@ -447,22 +426,15 @@ export class ReservationService {
constructor(
private redis: Redis,
private sequenceService: SequenceService,
private auditService: AuditService,
private auditService: AuditService
) {}
/**
* Reserve a document number
*/
async reserve(
documentType: string,
scopeValue?: string,
metadata?: Record<string, any>,
): Promise<Reservation> {
async reserve(documentType: string, scopeValue?: string, metadata?: Record<string, any>): Promise<Reservation> {
// 1. Generate next number
const documentNumber = await this.sequenceService.getNextSequence(
documentType,
scopeValue,
);
const documentNumber = await this.sequenceService.getNextSequence(documentType, scopeValue);
// 2. Generate reservation token
const token = uuidv4();
@@ -480,11 +452,7 @@ export class ReservationService {
metadata,
};
await this.redis.setex(
`reservation:${token}`,
this.TTL,
JSON.stringify(reservation),
);
await this.redis.setex(`reservation:${token}`, this.TTL, JSON.stringify(reservation));
// 5. Audit log
await this.auditService.log({
@@ -507,9 +475,7 @@ export class ReservationService {
const reservation = await this.getReservation(token);
if (!reservation) {
throw new ReservationExpiredError(
'Reservation not found or expired. Please reserve a new number.',
);
throw new ReservationExpiredError('Reservation not found or expired. Please reserve a new number.');
}
// 2. Save to database (via document creation)
@@ -584,6 +550,7 @@ export class ReservationService {
---
### 3.3 Manual Override Service
```typescript
// services/manual-override.service.ts
import { Injectable, Logger } from '@nestjs/common';
@@ -606,7 +573,7 @@ export class ManualOverrideService {
private dataSource: DataSource,
private formatService: FormatService,
private auditService: AuditService,
private auditService: AuditService
) {}
/**
@@ -617,7 +584,7 @@ export class ManualOverrideService {
manualNumber: string,
userId: number,
reason: string,
skipValidation = false,
skipValidation = false
): Promise<void> {
// 1. Get configuration
const config = await this.configRepo.findOne({
@@ -629,9 +596,7 @@ export class ManualOverrideService {
}
if (!config.allow_manual_override) {
throw new ManualOverrideNotAllowedError(
`Manual override not allowed for ${documentType}`,
);
throw new ManualOverrideNotAllowedError(`Manual override not allowed for ${documentType}`);
}
// 2. Validate
@@ -642,17 +607,11 @@ export class ManualOverrideService {
// 3. Check duplicate
const exists = await this.checkDuplicate(manualNumber);
if (exists) {
throw new DuplicateNumberError(
`Number ${manualNumber} already exists`,
);
throw new DuplicateNumberError(`Number ${manualNumber} already exists`);
}
// 4. Update sequence if higher
await this.updateSequenceIfHigher(
documentType,
manualNumber,
config,
);
await this.updateSequenceIfHigher(documentType, manualNumber, config);
// 5. Audit log
await this.auditService.log({
@@ -669,19 +628,11 @@ export class ManualOverrideService {
/**
* Validate manual number format
*/
private async validate(
manualNumber: string,
config: NumberingConfig,
): Promise<void> {
const isValid = this.formatService.matchesFormat(
manualNumber,
config.format,
);
private async validate(manualNumber: string, config: NumberingConfig): Promise<void> {
const isValid = this.formatService.matchesFormat(manualNumber, config.format);
if (!isValid) {
throw new InvalidFormatError(
`Number ${manualNumber} does not match format ${config.format}`,
);
throw new InvalidFormatError(`Number ${manualNumber} does not match format ${config.format}`);
}
}
@@ -701,7 +652,7 @@ export class ManualOverrideService {
SELECT document_number FROM drawings WHERE document_number = ?
) AS all_docs
`,
[number, number, number],
[number, number, number]
);
return count[0].count > 0;
@@ -713,13 +664,10 @@ export class ManualOverrideService {
private async updateSequenceIfHigher(
documentType: string,
manualNumber: string,
config: NumberingConfig,
config: NumberingConfig
): Promise<void> {
// Extract sequence value from manual number
const sequenceValue = this.formatService.extractSequence(
manualNumber,
config.format,
);
const sequenceValue = this.formatService.extractSequence(manualNumber, config.format);
if (!sequenceValue) {
this.logger.warn(`Could not extract sequence from ${manualNumber}`);
@@ -738,9 +686,7 @@ export class ManualOverrideService {
sequence.last_used_at = new Date();
await manager.save(sequence);
this.logger.log(
`Updated sequence for ${documentType} to ${sequenceValue}`,
);
this.logger.log(`Updated sequence for ${documentType} to ${sequenceValue}`);
}
});
}
@@ -752,12 +698,10 @@ export class ManualOverrideService {
## 4. Controllers
### 4.1 Main Numbering Controller
```typescript
// controllers/numbering.controller.ts
import {
Controller, Post, Body, UseGuards,
HttpCode, HttpStatus
} from '@nestjs/common';
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../auth/guards';
import { CurrentUser } from '../../common/decorators';
@@ -769,22 +713,13 @@ import { ReserveNumberDto, ConfirmReservationDto } from '../dto';
@Controller('document-numbering')
@UseGuards(JwtAuthGuard)
export class NumberingController {
constructor(
private reservationService: ReservationService,
) {}
constructor(private reservationService: ReservationService) {}
@Post('reserve')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Reserve a document number' })
async reserve(
@Body() dto: ReserveNumberDto,
@CurrentUser() user: any,
) {
const reservation = await this.reservationService.reserve(
dto.document_type,
dto.scope_value,
dto.metadata,
);
async reserve(@Body() dto: ReserveNumberDto, @CurrentUser() user: any) {
const reservation = await this.reservationService.reserve(dto.document_type, dto.scope_value, dto.metadata);
return {
success: true,
@@ -795,14 +730,8 @@ export class NumberingController {
@Post('confirm')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Confirm a reservation' })
async confirm(
@Body() dto: ConfirmReservationDto,
@CurrentUser() user: any,
) {
const documentNumber = await this.reservationService.confirm(
dto.token,
user.id,
);
async confirm(@Body() dto: ConfirmReservationDto, @CurrentUser() user: any) {
const documentNumber = await this.reservationService.confirm(dto.token, user.id);
return {
success: true,
@@ -813,10 +742,7 @@ export class NumberingController {
@Post('cancel')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Cancel a reservation' })
async cancel(
@Body() dto: ConfirmReservationDto,
@CurrentUser() user: any,
) {
async cancel(@Body() dto: ConfirmReservationDto, @CurrentUser() user: any) {
await this.reservationService.cancel(dto.token, user.id);
return {
@@ -832,41 +758,37 @@ export class NumberingController {
## 5. Integration with Document Creation
### 5.1 Correspondence Example
```typescript
// modules/correspondence/services/correspondence.service.ts
@Injectable()
export class CorrespondenceService {
constructor(
private reservationService: ReservationService,
private dataSource: DataSource,
private dataSource: DataSource
) {}
async create(dto: CreateCorrespondenceDto, userId: number) {
// Phase 1: Reserve number
const { token, document_number } = await this.reservationService.reserve(
'COR',
dto.project_id.toString(),
);
const { token, document_number } = await this.reservationService.reserve('COR', dto.project_id.toString());
try {
// Phase 2: Create document in transaction
const correspondence = await this.dataSource.transaction(
async (manager) => {
// Create correspondence
const corr = manager.create(Correspondence, {
document_number,
...dto,
created_by: userId,
});
const correspondence = await this.dataSource.transaction(async (manager) => {
// Create correspondence
const corr = manager.create(Correspondence, {
document_number,
...dto,
created_by: userId,
});
await manager.save(corr);
await manager.save(corr);
// Confirm reservation
await this.reservationService.confirm(token, userId);
// Confirm reservation
await this.reservationService.confirm(token, userId);
return corr;
},
);
return corr;
});
return correspondence;
} catch (error) {
@@ -883,6 +805,7 @@ export class CorrespondenceService {
## 6. Testing
### 6.1 Unit Tests
```typescript
// tests/unit/sequence.service.spec.ts
describe('SequenceService', () => {
@@ -927,14 +850,13 @@ describe('SequenceService', () => {
it('should throw on sequence exhausted', async () => {
await setSequence('COR', 999999); // Max value
await expect(
service.getNextSequence('COR')
).rejects.toThrow(SequenceExhaustedError);
await expect(service.getNextSequence('COR')).rejects.toThrow(SequenceExhaustedError);
});
});
```
### 6.2 Integration Tests
```typescript
// tests/integration/reservation.spec.ts
describe('Reservation Flow (Integration)', () => {
@@ -992,27 +914,26 @@ describe('Reservation Flow (Integration)', () => {
```
### 6.3 Load Tests
```typescript
// tests/load/concurrency.spec.ts
describe('Concurrency Test', () => {
it('should handle 1000 concurrent requests without duplicates', async () => {
const promises = Array.from({ length: 1000 }, (_, i) =>
request(app.getHttpServer())
.post('/document-numbering/reserve')
.send({ document_type: 'COR' })
request(app.getHttpServer()).post('/document-numbering/reserve').send({ document_type: 'COR' })
);
const results = await Promise.all(promises);
// Extract all document numbers
const numbers = results.map(r => r.body.data.document_number);
const numbers = results.map((r) => r.body.data.document_number);
// Check for duplicates
const uniqueNumbers = new Set(numbers);
expect(uniqueNumbers.size).toBe(1000);
// Verify sequential
const sequences = numbers.map(n => extractSeq(n)).sort((a, b) => a - b);
const sequences = numbers.map((n) => extractSeq(n)).sort((a, b) => a - b);
expect(sequences[0]).toBe(1);
expect(sequences[999]).toBe(1000);
});
@@ -1024,6 +945,7 @@ describe('Concurrency Test', () => {
## 7. Deployment Checklist
### 7.1 Pre-Deployment
- [ ] Run all tests (unit, integration, E2E)
- [ ] Load test (1000 req/s for 5 min)
- [ ] Setup Redis cluster (3 nodes)
@@ -1036,6 +958,7 @@ describe('Concurrency Test', () => {
- [ ] Document rollback procedure
### 7.2 Deployment Steps
1. Deploy Redis cluster to staging
2. Run migrations on staging database
3. Deploy backend service to staging
@@ -1047,6 +970,7 @@ describe('Concurrency Test', () => {
9. Gradual rollout (10% → 50% → 100%)
### 7.3 Post-Deployment
- [ ] Verify all metrics green
- [ ] Check error rates (<0.1%)
- [ ] Validate audit logs working
@@ -1060,6 +984,7 @@ describe('Concurrency Test', () => {
## 8. Monitoring & Observability
### 8.1 Prometheus Metrics
```typescript
// metrics/numbering.metrics.ts
import { Injectable } from '@nestjs/common';
@@ -1137,6 +1062,7 @@ export class NumberingMetrics {
```
### 8.2 Grafana Dashboard
```json
{
"dashboard": {
@@ -1181,6 +1107,7 @@ export class NumberingMetrics {
```
### 8.3 Alert Rules
```yaml
# alerts/numbering.yml
groups:
@@ -1194,8 +1121,8 @@ groups:
labels:
severity: critical
annotations:
summary: "Sequence {{ $labels.document_type }} >95% used"
description: "Current: {{ $value }}%. Extend max_value immediately."
summary: 'Sequence {{ $labels.document_type }} >95% used'
description: 'Current: {{ $value }}%. Extend max_value immediately.'
# Warning: Sequence >90% used
- alert: SequenceWarning
@@ -1204,8 +1131,8 @@ groups:
labels:
severity: warning
annotations:
summary: "Sequence {{ $labels.document_type }} >90% used"
description: "Current: {{ $value }}%. Plan to extend max_value."
summary: 'Sequence {{ $labels.document_type }} >90% used'
description: 'Current: {{ $value }}%. Plan to extend max_value.'
# Critical: High lock wait time
- alert: HighLockWaitTime
@@ -1214,8 +1141,8 @@ groups:
labels:
severity: warning
annotations:
summary: "Lock wait time >1s (p95)"
description: "p95: {{ $value }}s. Check Redis cluster health."
summary: 'Lock wait time >1s (p95)'
description: 'p95: {{ $value }}s. Check Redis cluster health.'
# Critical: Redis down
- alert: RedisUnavailable
@@ -1224,8 +1151,8 @@ groups:
labels:
severity: critical
annotations:
summary: "Redis cluster unavailable"
description: "Numbering system using DB-only fallback mode."
summary: 'Redis cluster unavailable'
description: 'Numbering system using DB-only fallback mode.'
# Warning: High error rate
- alert: HighErrorRate
@@ -1234,8 +1161,8 @@ groups:
labels:
severity: warning
annotations:
summary: "High error rate in numbering system"
description: "{{ $value }} errors/sec. Check logs."
summary: 'High error rate in numbering system'
description: '{{ $value }} errors/sec. Check logs.'
```
---
@@ -1245,9 +1172,11 @@ groups:
### 9.1 Common Issues
#### Issue 1: Duplicate Numbers Generated
**Symptoms**: Same document number appears twice
**Diagnosis**:
```sql
-- Find duplicates
SELECT document_number, COUNT(*) as count
@@ -1263,11 +1192,13 @@ HAVING count > 1;
```
**Root Causes**:
- Redis cluster failure during generation
- Database deadlock
- Bug in locking logic
**Resolution**:
1. Identify affected documents
2. Manually reassign one with new number
3. Update audit log
@@ -1277,9 +1208,11 @@ HAVING count > 1;
---
#### Issue 2: Sequence Exhausted
**Symptoms**: Error "Sequence exhausted for COR"
**Diagnosis**:
```sql
-- Check current vs max
SELECT
@@ -1293,6 +1226,7 @@ WHERE s.current_value >= c.max_value * 0.9;
```
**Resolution**:
```sql
-- Extend max_value
UPDATE document_numbering_configs
@@ -1312,9 +1246,11 @@ WHERE config_id = (
---
#### Issue 3: Lock Timeout
**Symptoms**: "Failed to acquire lock after 3 retries"
**Diagnosis**:
```bash
# Check Redis cluster health
redis-cli --cluster check localhost:7000
@@ -1325,12 +1261,14 @@ redis-cli GET "numbering:COR:project-1"
```
**Root Causes**:
- High concurrent load
- Redis node down
- Network latency
- Deadlock in database
**Resolution**:
1. Check Redis cluster health
2. Increase lock timeout (5s → 10s)
3. Add more Redis nodes
@@ -1340,9 +1278,11 @@ redis-cli GET "numbering:COR:project-1"
---
#### Issue 4: Reservation Expired
**Symptoms**: User gets "Reservation expired" error
**Diagnosis**:
```bash
# Check Redis TTL
redis-cli TTL "reservation:uuid-here"
@@ -1352,11 +1292,13 @@ redis-cli KEYS "reservation:*"
```
**Root Causes**:
- User took >5 minutes to complete form
- Network issue during confirmation
- Browser closed/refreshed
**Resolution**:
1. Reserve new number
2. Consider increasing TTL (5 min → 10 min)
3. Add progress auto-save
@@ -1398,6 +1340,7 @@ npm run cli numbering:check-duplicates
## 10. Performance Optimization
### 10.1 Database Indexes
```sql
-- Composite index for faster lookups
CREATE INDEX idx_sequence_lookup
@@ -1420,6 +1363,7 @@ ON drawings(document_number);
```
### 10.2 Connection Pooling
```typescript
// config/database.config.ts
export default {
@@ -1430,17 +1374,18 @@ export default {
// Connection pool settings
extra: {
connectionLimit: 20, // Max connections
queueLimit: 0, // Unlimited queue
connectionLimit: 20, // Max connections
queueLimit: 0, // Unlimited queue
waitForConnections: true,
acquireTimeout: 30000, // 30s timeout
idleTimeout: 10000, // 10s idle timeout
maxIdle: 5, // Max idle connections
acquireTimeout: 30000, // 30s timeout
idleTimeout: 10000, // 10s idle timeout
maxIdle: 5, // Max idle connections
},
};
```
### 10.3 Redis Optimization
```typescript
// config/redis.config.ts
export default {
@@ -1469,6 +1414,7 @@ export default {
```
### 10.4 Caching Strategy
```typescript
// Cache configuration
@Cacheable({
@@ -1499,6 +1445,7 @@ async updateConfig(documentType: string, data: any) {
## 11. Security Considerations
### 11.1 Access Control
```typescript
// guards/manual-override.guard.ts
@Injectable()
@@ -1525,6 +1472,7 @@ async manualOverride(@Body() dto: ManualOverrideDto) {
```
### 11.2 Rate Limiting
```typescript
// Apply rate limit to prevent abuse
@Throttle(100, 60) // 100 requests per minute
@@ -1535,6 +1483,7 @@ async reserve(@Body() dto: ReserveNumberDto) {
```
### 11.3 Audit Logging
```typescript
// decorators/audit-numbering.decorator.ts
export function AuditNumbering(operation: string) {
@@ -1575,6 +1524,7 @@ async manualOverride(dto: ManualOverrideDto, user: User) {
## 12. Migration Scripts
### 12.1 Import Legacy Documents
```typescript
// scripts/import-legacy-numbers.ts
import { DataSource } from 'typeorm';
@@ -1638,14 +1588,17 @@ async function updateSequenceCounters(dataSource: DataSource) {
const maxSeq = result[0].max_seq;
await dataSource.query(`
await dataSource.query(
`
UPDATE document_numbering_sequences
SET current_value = ?
WHERE config_id = (
SELECT id FROM document_numbering_configs
WHERE document_type = 'COR'
)
`, [maxSeq]);
`,
[maxSeq]
);
console.log(`Updated COR sequence to ${maxSeq}`);
}
@@ -1658,6 +1611,7 @@ importLegacyNumbers().catch(console.error);
## 13. CLI Tools
### 13.1 Status Command
```typescript
// cli/commands/numbering-status.command.ts
import { Command, CommandRunner } from 'nest-commander';
@@ -1691,7 +1645,7 @@ export class NumberingStatusCommand extends CommandRunner {
console.log(`Format: ${config.format}`);
console.log(`Current Value: ${sequence.current_value}`);
console.log(`Max Value: ${config.max_value}`);
console.log(`Utilization: ${(sequence.current_value / config.max_value * 100).toFixed(2)}%`);
console.log(`Utilization: ${((sequence.current_value / config.max_value) * 100).toFixed(2)}%`);
console.log(`Last Used: ${sequence.last_used_at}`);
console.log(`Manual Override: ${config.allow_manual_override ? 'Yes' : 'No'}`);
}
@@ -1705,9 +1659,9 @@ export class NumberingStatusCommand extends CommandRunner {
Type: c.document_type,
Current: c.sequence?.current_value || 0,
Max: c.max_value,
'Utilization (%)': ((c.sequence?.current_value || 0) / c.max_value * 100).toFixed(2),
'Utilization (%)': (((c.sequence?.current_value || 0) / c.max_value) * 100).toFixed(2),
'Last Used': c.sequence?.last_used_at || 'Never',
})),
}))
);
}
}
@@ -1718,6 +1672,7 @@ export class NumberingStatusCommand extends CommandRunner {
## 14. Best Practices Summary
### 14.1 DO's ✅
- ✅ Always use two-phase commit (reserve + confirm)
- ✅ Implement fallback to DB-only if Redis fails
- ✅ Log every operation to audit trail
@@ -1730,6 +1685,7 @@ export class NumberingStatusCommand extends CommandRunner {
- ✅ Implement exponential backoff on retry
### 14.2 DON'Ts ❌
- ❌ Never skip validation for manual override
- ❌ Never reuse cancelled numbers
- ❌ Never trust client-generated numbers
@@ -1746,6 +1702,7 @@ export class NumberingStatusCommand extends CommandRunner {
## 15. Appendix
### 15.1 Error Codes
```typescript
export enum NumberingErrorCode {
CONFIG_NOT_FOUND = 'NB001',
@@ -1760,6 +1717,7 @@ export enum NumberingErrorCode {
```
### 15.2 Environment Variables
```bash
# Redis Configuration
REDIS_HOST=localhost
@@ -1778,6 +1736,7 @@ GRAFANA_PORT=3000
```
### 15.3 Useful Queries
```sql
-- Find next available number
SELECT MAX(CAST(SUBSTRING_INDEX(document_number, '-', -1) AS UNSIGNED)) + 1
@@ -1,6 +1,7 @@
# Document Numbering Implementation Guide
---
title: 'Implementation Guide: Document Numbering System'
version: 1.6.1
status: implemented
@@ -171,45 +172,52 @@ src/modules/document-numbering/
### 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)
- 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).
- 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
- 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.
- 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.
- 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
- 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.
- Return generated number to caller.
### 2.3. TypeORM Entity
@@ -309,9 +317,11 @@ export class DocumentNumberingLockService {
}
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}`;
return (
`lock:docnum:${key.projectId}:${key.originatorOrgId}:` +
`${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` +
`${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`
);
}
}
```
@@ -333,7 +343,7 @@ export class CounterService {
constructor(
@InjectRepository(DocumentNumberCounter)
private counterRepo: Repository<DocumentNumberCounter>,
private dataSource: DataSource,
private dataSource: DataSource
) {}
async incrementCounter(counterKey: CounterKey): Promise<number> {
@@ -364,9 +374,7 @@ export class CounterService {
});
} catch (error) {
if (error instanceof OptimisticLockVersionMismatchError) {
this.logger.warn(
`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`,
);
this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`);
if (attempt === MAX_RETRIES - 1) {
throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
}
@@ -409,7 +417,7 @@ export class DocumentNumberingService {
constructor(
private lockService: DocumentNumberingLockService,
private counterService: CounterService,
private auditService: AuditService,
private auditService: AuditService
) {}
async generateDocumentNumber(dto: GenerateNumberDto): Promise<string> {
@@ -464,7 +472,7 @@ export class DocumentNumberingService {
return template
.replace('{SEQ:4}', seq.toString().padStart(4, '0'))
.replace('{YEAR:B.E.}', (key.year + 543).toString());
// ... more replacements
// ... more replacements
}
}
```
@@ -483,8 +491,16 @@ interface ValidationResult {
@Injectable()
export class TemplateValidator {
private readonly ALLOWED_TOKENS = [
'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE',
'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV',
'PROJECT',
'ORIGINATOR',
'RECIPIENT',
'CORR_TYPE',
'SUB_TYPE',
'RFA_TYPE',
'DISCIPLINE',
'SEQ',
'YEAR',
'REV',
];
validate(template: string, correspondenceType: string): ValidationResult {
@@ -574,10 +590,7 @@ export class CounterResetJob extends WorkerHost {
```typescript
// File: src/modules/document-numbering/document-numbering.controller.ts
import {
Controller, Get, Post, Patch,
Body, Param, Query, UseGuards, ParseIntPipe,
} from '@nestjs/common';
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';
@@ -613,10 +626,7 @@ export class DocumentNumberingController {
@Patch('counters/:id')
@RequirePermission('system.manage_settings')
async updateCounter(
@Param('id', ParseIntPipe) id: number,
@Body('sequence') sequence: number
) {
async updateCounter(@Param('id', ParseIntPipe) id: number, @Body('sequence') sequence: number) {
return this.numberingService.setCounterValue(id, sequence);
}
@@ -634,10 +644,7 @@ export class DocumentNumberingController {
```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 { 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';
@@ -686,30 +693,21 @@ export class DocumentNumberingAdminController {
@Post('manual-override')
@RequirePermission('system.manage_settings')
async manualOverride(@Body() dto: {
projectId: number;
correspondenceTypeId: number | null;
year: number;
newValue: number;
}) {
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;
}) {
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;
}) {
async cancelNumber(@Body() dto: { documentNumber: string; reason: string }) {
return this.service.cancelNumber(dto);
}
@@ -760,11 +758,7 @@ import { DocumentNumberingController } from './controllers/document-numbering.co
@Module({
imports: [
TypeOrmModule.forFeature([
DocumentNumberCounter,
DocumentNumberAudit,
DocumentNumberError,
]),
TypeOrmModule.forFeature([DocumentNumberCounter, DocumentNumberAudit, DocumentNumberError]),
BullModule.registerQueue({
name: 'document-numbering',
}),