25 KiB
25 KiB
Document Numbering Implementation Guide (Combined)
title: 'Implementation Guide: Document Numbering System' version: 1.6.2 status: APPROVED owner: Development Team last_updated: 2025-12-17 related:
- specs/01-requirements/03.11-document-numbering.md
- specs/04-operations/document-numbering-operations.md
- specs/05-decisions/ADR-002-document-numbering-strategy.md
Overview
เอกสารนี้รวบรวม implementation details สำหรับระบบ Document Numbering โดยผนวกข้อมูลจาก:
document-numbering.md- Core implementation และ database schemadocument-numbering-add.md- Extended features (Reservation, Manual Override, Monitoring)
Technology Stack
| Component | Technology |
|---|---|
| Backend Framework | NestJS 10.x |
| ORM | TypeORM 0.3.x |
| Database | MariaDB 11.8 |
| Cache/Lock | Redis 7.x + Redlock |
| Message Queue | BullMQ |
| Monitoring | Prometheus + Grafana |
1. Module Structure
backend/src/modules/document-numbering/
├── document-numbering.module.ts
├── controllers/
│ ├── document-numbering.controller.ts # General endpoints
│ ├── document-numbering-admin.controller.ts # Admin endpoints
│ └── numbering-metrics.controller.ts # Metrics endpoints
├── services/
│ ├── document-numbering.service.ts # Main orchestration
│ ├── document-numbering-lock.service.ts # Redis Lock
│ ├── counter.service.ts # Sequence counter logic
│ ├── reservation.service.ts # Two-phase commit
│ ├── manual-override.service.ts # Manual number handling
│ ├── format.service.ts # Template formatting
│ ├── template.service.ts # Template CRUD
│ ├── audit.service.ts # Audit logging
│ ├── metrics.service.ts # Prometheus metrics
│ └── migration.service.ts # Legacy import
├── entities/
│ ├── document-number-counter.entity.ts
│ ├── document-number-format.entity.ts
│ ├── document-number-audit.entity.ts
│ ├── document-number-error.entity.ts
│ └── document-number-reservation.entity.ts
├── dto/
│ ├── generate-number.dto.ts
│ ├── preview-number.dto.ts
│ ├── reserve-number.dto.ts
│ ├── confirm-reservation.dto.ts
│ ├── manual-override.dto.ts
│ ├── void-document.dto.ts
│ └── bulk-import.dto.ts
├── validators/
│ └── template.validator.ts
├── guards/
│ └── manual-override.guard.ts
├── decorators/
│ └── audit-numbering.decorator.ts
├── jobs/
│ └── counter-reset.job.ts
└── tests/
├── unit/
├── integration/
└── e2e/
2. Database Schema
2.1 Format Template Table
CREATE TABLE document_number_formats (
id INT AUTO_INCREMENT PRIMARY KEY,
project_id INT NOT NULL,
correspondence_type_id INT NULL, -- NULL = default format for project
format_template VARCHAR(100) NOT NULL,
reset_sequence_yearly TINYINT(1) DEFAULT 1,
description VARCHAR(255),
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
UNIQUE KEY idx_unique_project_type (project_id, correspondence_type_id),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='Document Number Format Templates';
2.2 Counter Table
CREATE TABLE document_number_counters (
project_id INT NOT NULL,
correspondence_type_id INT NULL,
originator_organization_id INT NOT NULL,
recipient_organization_id INT NOT NULL DEFAULT 0, -- 0 = no recipient (RFA)
sub_type_id INT DEFAULT 0,
rfa_type_id INT DEFAULT 0,
discipline_id INT DEFAULT 0,
reset_scope VARCHAR(20) NOT NULL,
last_number INT DEFAULT 0 NOT NULL,,
version INT DEFAULT 0 NOT NULL,
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (
project_id,
originator_organization_id,
COALESCE(recipient_organization_id, 0),
correspondence_type_id,
sub_type_id,
rfa_type_id,
discipline_id,
reset_scope
),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE,
INDEX idx_counter_lookup (project_id, correspondence_type_id, reset_scope),
INDEX idx_counter_org (originator_organization_id, reset_scope),
INDEX idx_counter_updated (updated_at),
CONSTRAINT chk_last_number_positive CHECK (last_number >= 0),
CONSTRAINT chk_reset_scope_format CHECK (
reset_scope IN ('NONE') OR
reset_scope LIKE 'YEAR_%' OR
reset_scope LIKE 'MONTH_%' OR
reset_scope LIKE 'CONTRACT_%'
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Running Number Counters';
2.3 Audit Table
CREATE TABLE document_number_audit (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
document_id INT NULL COMMENT 'FK to documents (NULL initially)',
document_type VARCHAR(50),
document_number VARCHAR(100) NOT NULL,
operation ENUM('RESERVE', 'CONFIRM', 'CANCEL', 'MANUAL_OVERRIDE', 'VOID', 'GENERATE') NOT NULL,
status ENUM('RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID', 'MANUAL'),
counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)',
reservation_token VARCHAR(36) NULL,
originator_organization_id INT NULL,
recipient_organization_id INT NULL,
template_used VARCHAR(200) NOT NULL,
old_value TEXT NULL,
new_value TEXT NULL,
user_id INT NULL COMMENT 'FK to users (Allow NULL for system generation)',
ip_address VARCHAR(45),
user_agent TEXT,
is_success BOOLEAN DEFAULT TRUE,
retry_count INT DEFAULT 0,
lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds',
total_duration_ms INT COMMENT 'Total generation time',
fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE',
metadata JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_document_id (document_id),
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_operation (operation),
INDEX idx_document_number (document_number),
INDEX idx_created_at (created_at),
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail';
2.4 Error Log Table
CREATE TABLE document_number_errors (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
error_type ENUM(
'LOCK_TIMEOUT',
'VERSION_CONFLICT',
'DB_ERROR',
'REDIS_ERROR',
'VALIDATION_ERROR',
'SEQUENCE_EXHAUSTED',
'RESERVATION_EXPIRED',
'DUPLICATE_NUMBER'
) NOT NULL,
error_message TEXT,
stack_trace TEXT,
context_data JSON COMMENT 'Request context (user, project, etc.)',
user_id INT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP NULL,
INDEX idx_error_type (error_type),
INDEX idx_created_at (created_at),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB COMMENT='Document Numbering Error Log';
2.5 Reservation Table
CREATE TABLE document_number_reservations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
-- Reservation Details
token VARCHAR(36) NOT NULL UNIQUE COMMENT 'UUID v4',
document_number VARCHAR(100) NOT NULL UNIQUE,
status ENUM('RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID') NOT NULL DEFAULT 'RESERVED',
-- Linkage
document_id INT NULL COMMENT 'FK to documents (NULL until confirmed)',
-- Context (for debugging)
project_id INT NOT NULL,
correspondence_type_id INT NOT NULL,
originator_organization_id INT NOT NULL,
recipient_organization_id INT DEFAULT 0,
user_id INT NOT NULL,
-- Timestamps
reserved_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
expires_at DATETIME(6) NOT NULL,
confirmed_at DATETIME(6) NULL,
cancelled_at DATETIME(6) NULL,
-- Audit
ip_address VARCHAR(45),
user_agent TEXT,
metadata JSON NULL COMMENT 'Additional context',
-- Indexes
INDEX idx_token (token),
INDEX idx_status (status),
INDEX idx_status_expires (status, expires_at),
INDEX idx_document_id (document_id),
INDEX idx_user_id (user_id),
INDEX idx_reserved_at (reserved_at),
-- Foreign Keys
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE SET NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Document Number Reservations - Two-Phase Commit';
3. Core Services
3.1 Number Generation Process
sequenceDiagram
participant C as Client
participant S as NumberingService
participant L as LockService
participant CS as CounterService
participant DB as Database
participant R as Redis
C->>S: generateDocumentNumber(dto)
S->>L: acquireLock(counterKey)
L->>R: REDLOCK acquire
R-->>L: lock acquired
L-->>S: lock handle
S->>CS: incrementCounter(counterKey)
CS->>DB: BEGIN TRANSACTION
CS->>DB: SELECT FOR UPDATE
CS->>DB: UPDATE last_number
CS->>DB: COMMIT
DB-->>CS: newNumber
CS-->>S: sequence
S->>S: formatNumber(template, seq)
S->>L: releaseLock()
L->>R: REDLOCK release
S-->>C: documentNumber
3.2 Two-Phase Commit (Reserve/Confirm)
sequenceDiagram
participant C as Client
participant RS as ReservationService
participant SS as SequenceService
participant R as Redis
Note over C,R: Phase 1: Reserve
C->>RS: reserve(documentType)
RS->>SS: getNextSequence()
SS-->>RS: documentNumber
RS->>R: SETEX reservation:{token} (TTL: 5min)
RS-->>C: {token, documentNumber, expiresAt}
Note over C,R: Phase 2: Confirm
C->>RS: confirm(token)
RS->>R: GET reservation:{token}
R-->>RS: reservationData
RS->>R: DEL reservation:{token}
RS-->>C: documentNumber (confirmed)
3.3 Counter Service Implementation
// services/counter.service.ts
@Injectable()
export class CounterService {
private readonly logger = new Logger(CounterService.name);
constructor(
@InjectRepository(DocumentNumberCounter)
private counterRepo: Repository<DocumentNumberCounter>,
private dataSource: DataSource,
) {}
async incrementCounter(counterKey: CounterKey): Promise<number> {
const MAX_RETRIES = 2;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return await this.dataSource.transaction(async (manager) => {
// ใช้ Optimistic Locking
const counter = await manager.findOne(DocumentNumberCounter, {
where: this.buildWhereClause(counterKey),
});
if (!counter) {
const newCounter = manager.create(DocumentNumberCounter, {
...counterKey,
lastNumber: 1,
version: 0,
});
await manager.save(newCounter);
return 1;
}
counter.lastNumber += 1;
await manager.save(counter); // Auto-check version
return counter.lastNumber;
});
} catch (error) {
if (error instanceof OptimisticLockVersionMismatchError) {
this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`);
if (attempt === MAX_RETRIES - 1) {
throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
}
continue;
}
throw error;
}
}
}
}
3.4 Redis Lock Service
// services/document-numbering-lock.service.ts
@Injectable()
export class DocumentNumberingLockService {
private readonly logger = new Logger(DocumentNumberingLockService.name);
private redlock: Redlock;
constructor(@InjectRedis() private readonly redis: Redis) {
this.redlock = new Redlock([redis], {
driftFactor: 0.01,
retryCount: 5,
retryDelay: 100,
retryJitter: 50,
});
}
async acquireLock(counterKey: CounterKey): Promise<Redlock.Lock> {
const lockKey = this.buildLockKey(counterKey);
const ttl = 5000; // 5 seconds
try {
const lock = await this.redlock.acquire([lockKey], ttl);
this.logger.debug(`Acquired lock: ${lockKey}`);
return lock;
} catch (error) {
this.logger.error(`Failed to acquire lock: ${lockKey}`, error);
throw error;
}
}
async releaseLock(lock: Redlock.Lock): Promise<void> {
try {
await lock.release();
} catch (error) {
this.logger.warn('Failed to release lock (may have expired)', error);
}
}
private buildLockKey(key: CounterKey): string {
return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` +
`${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` +
`${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`;
}
}
3.5 Reservation Service
// services/reservation.service.ts
@Injectable()
export class ReservationService {
private readonly TTL = 300; // 5 minutes
constructor(
private redis: Redis,
private sequenceService: SequenceService,
private auditService: AuditService,
) {}
async reserve(
documentType: string,
scopeValue?: string,
metadata?: Record<string, any>,
): Promise<Reservation> {
// 1. Generate next number
const documentNumber = await this.sequenceService.getNextSequence(
documentType,
scopeValue,
);
// 2. Generate reservation token
const token = uuidv4();
const expiresAt = new Date(Date.now() + this.TTL * 1000);
// 3. Save to Redis
const reservation: Reservation = {
token,
document_number: documentNumber,
document_type: documentType,
scope_value: scopeValue,
expires_at: expiresAt,
metadata,
};
await this.redis.setex(
`reservation:${token}`,
this.TTL,
JSON.stringify(reservation),
);
// 4. Audit log
await this.auditService.log({
operation: 'RESERVE',
document_type: documentType,
document_number: documentNumber,
metadata: { token, scope_value: scopeValue },
});
return reservation;
}
async confirm(token: string, userId: number): Promise<string> {
const reservation = await this.getReservation(token);
if (!reservation) {
throw new ReservationExpiredError(
'Reservation not found or expired. Please reserve a new number.',
);
}
await this.redis.del(`reservation:${token}`);
await this.auditService.log({
operation: 'CONFIRM',
document_type: reservation.document_type,
document_number: reservation.document_number,
user_id: userId,
metadata: { token },
});
return reservation.document_number;
}
async cancel(token: string, userId: number): Promise<void> {
const reservation = await this.getReservation(token);
if (reservation) {
await this.redis.del(`reservation:${token}`);
await this.auditService.log({
operation: 'CANCEL',
document_type: reservation.document_type,
document_number: reservation.document_number,
user_id: userId,
metadata: { token },
});
}
}
@Cron('0 */5 * * * *') // Every 5 minutes
async cleanupExpired(): Promise<void> {
const keys = await this.redis.keys('reservation:*');
for (const key of keys) {
const ttl = await this.redis.ttl(key);
if (ttl <= 0) {
await this.redis.del(key);
}
}
}
}
4. Template System
4.1 Supported Tokens
| Token | Description | Example Output |
|---|---|---|
{PROJECT} |
Project Code | LCBP3 |
{ORIGINATOR} |
Originator Organization Code | คคง. |
{RECIPIENT} |
Recipient Organization Code | สคฉ.3 |
{CORR_TYPE} |
Correspondence Type Code | L |
{SUB_TYPE} |
Sub Type Code | TD |
{RFA_TYPE} |
RFA Type Code | RFA |
{DISCIPLINE} |
Discipline Code | CV |
{SEQ:n} |
Sequence Number (n digits) | 0001 |
{YEAR:CE} |
Year (Common Era) | 2025 |
{YEAR:BE} |
Year (Buddhist Era) | 2568 |
{REV} |
Revision Number | A |
4.2 Template Validation
// validators/template.validator.ts
@Injectable()
export class TemplateValidator {
private readonly ALLOWED_TOKENS = [
'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE',
'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV',
];
validate(template: string, correspondenceType: string): ValidationResult {
const tokens = this.extractTokens(template);
const errors: string[] = [];
// ตรวจสอบ Token ที่ไม่รู้จัก
for (const token of tokens) {
if (!this.ALLOWED_TOKENS.includes(token.name)) {
errors.push(`Unknown token: {${token.name}}`);
}
}
// กฎพิเศษสำหรับแต่ละประเภท
if (correspondenceType === 'RFA') {
if (!tokens.some((t) => t.name === 'PROJECT')) {
errors.push('RFA template ต้องมี {PROJECT}');
}
if (!tokens.some((t) => t.name === 'DISCIPLINE')) {
errors.push('RFA template ต้องมี {DISCIPLINE}');
}
}
if (correspondenceType === 'TRANSMITTAL') {
if (!tokens.some((t) => t.name === 'SUB_TYPE')) {
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
}
}
// ทุก template ต้องมี {SEQ}
if (!tokens.some((t) => t.name.startsWith('SEQ'))) {
errors.push('Template ต้องมี {SEQ:n}');
}
return { valid: errors.length === 0, errors };
}
}
5. API Endpoints
5.1 General Endpoints (/document-numbering)
| Endpoint | Method | Permission | Description |
|---|---|---|---|
/logs/audit |
GET | system.view_logs |
Get audit logs |
/logs/errors |
GET | system.view_logs |
Get error logs |
/sequences |
GET | correspondence.read |
Get counter sequences |
/counters/:id |
PATCH | system.manage_settings |
Update counter value |
/preview |
POST | correspondence.read |
Preview number without generating |
/reserve |
POST | correspondence.create |
Reserve a document number |
/confirm |
POST | correspondence.create |
Confirm a reservation |
/cancel |
POST | correspondence.create |
Cancel a reservation |
5.2 Admin Endpoints (/admin/document-numbering)
| Endpoint | Method | Permission | Description |
|---|---|---|---|
/templates |
GET | system.manage_settings |
Get all templates |
/templates |
POST | system.manage_settings |
Create/update template |
/templates/:id |
DELETE | system.manage_settings |
Delete template |
/metrics |
GET | system.view_logs |
Get metrics |
/manual-override |
POST | system.manage_settings |
Override counter value |
/void-and-replace |
POST | system.manage_settings |
Void and replace number |
/cancel |
POST | system.manage_settings |
Cancel a number |
/bulk-import |
POST | system.manage_settings |
Bulk import counters |
6. Monitoring & Observability
6.1 Prometheus Metrics
@Injectable()
export class NumberingMetrics {
// Counter: Total numbers generated
private readonly numbersGenerated = new Counter({
name: 'numbering_sequences_total',
help: 'Total document numbers generated',
labelNames: ['document_type'],
});
// Gauge: Sequence utilization (%)
private readonly sequenceUtilization = new Gauge({
name: 'numbering_sequence_utilization',
help: 'Sequence utilization percentage',
labelNames: ['document_type'],
});
// Histogram: Lock wait time
private readonly lockWaitTime = new Histogram({
name: 'numbering_lock_wait_seconds',
help: 'Time spent waiting for lock acquisition',
labelNames: ['document_type'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});
// Counter: Lock failures
private readonly lockFailures = new Counter({
name: 'numbering_lock_failures_total',
help: 'Total lock acquisition failures',
labelNames: ['document_type', 'reason'],
});
}
6.2 Alert Rules
| Alert | Condition | Severity | Action |
|---|---|---|---|
SequenceCritical |
Utilization > 95% | Critical | Extend max_value |
SequenceWarning |
Utilization > 90% | Warning | Plan extension |
HighLockWaitTime |
p95 > 1s | Warning | Check Redis health |
RedisUnavailable |
Redis cluster down | Critical | Switch to DB-only mode |
HighErrorRate |
> 10 errors/sec | Warning | Check logs |
7. Error Handling
7.1 Error Codes
| Code | Name | Description |
|---|---|---|
| NB001 | CONFIG_NOT_FOUND | Config not found for type |
| NB002 | SEQUENCE_EXHAUSTED | Sequence reached max value |
| NB003 | LOCK_TIMEOUT | Failed to acquire lock |
| NB004 | RESERVATION_EXPIRED | Reservation token expired |
| NB005 | DUPLICATE_NUMBER | Number already exists |
| NB006 | INVALID_FORMAT | Number format invalid |
| NB007 | MANUAL_OVERRIDE_NOT_ALLOWED | Manual override disabled |
| NB008 | REDIS_UNAVAILABLE | Redis connection failed |
7.2 Fallback Strategy
flowchart TD
A[Generate Number Request] --> B{Redis Available?}
B -->|Yes| C[Acquire Redlock]
B -->|No| D[Use DB-only Lock]
C --> E{Lock Acquired?}
E -->|Yes| F[Increment Counter]
E -->|No| G{Retry < 3?}
G -->|Yes| C
G -->|No| H[Fallback to DB Lock]
D --> F
H --> F
F --> I[Format Number]
I --> J[Return Number]
8. Testing
8.1 Unit Tests
# Run unit tests
pnpm test:watch -- --testPathPattern=document-numbering
8.2 Integration Tests
# Run integration tests
pnpm test:e2e -- --testPathPattern=numbering
8.3 Concurrency Test
// tests/load/concurrency.spec.ts
it('should handle 1000 concurrent requests without duplicates', async () => {
const promises = Array.from({ length: 1000 }, () =>
request(app.getHttpServer())
.post('/document-numbering/reserve')
.send({ document_type: 'COR' })
);
const results = await Promise.all(promises);
const numbers = results.map(r => r.body.data.document_number);
const uniqueNumbers = new Set(numbers);
expect(uniqueNumbers.size).toBe(1000);
});
9. Best Practices
9.1 DO's ✅
- ✅ Always use two-phase commit (reserve + confirm)
- ✅ Implement fallback to DB-only if Redis fails
- ✅ Log every operation to audit trail
- ✅ Monitor sequence utilization (alert at 90%)
- ✅ Test under concurrent load (1000+ req/s)
- ✅ Use pessimistic locking in database
- ✅ Set reasonable TTL for reservations (5 min)
- ✅ Validate manual override format
- ✅ Skip cancelled numbers (never reuse)
9.2 DON'Ts ❌
- ❌ Never skip validation for manual override
- ❌ Never reuse cancelled numbers
- ❌ Never trust client-generated numbers
- ❌ Never increase sequence without transaction
- ❌ Never deploy without load testing
- ❌ Never modify sequence table directly
- ❌ Never skip audit logging
10. Environment Variables
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_CLUSTER_NODES=redis-1:6379,redis-2:6379,redis-3:6379
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=lcbp3
DB_PASSWORD=
DB_DATABASE=lcbp3_db
DB_POOL_SIZE=20
# Numbering Configuration
NUMBERING_LOCK_TIMEOUT=5000 # 5 seconds
NUMBERING_RESERVATION_TTL=300 # 5 minutes
NUMBERING_RETRY_ATTEMPTS=3
NUMBERING_RETRY_DELAY=200 # milliseconds
# Monitoring
PROMETHEUS_PORT=9090
GRAFANA_PORT=3000
References
Document Version: 2.0.0 Created By: Development Team Last Updated: 2025-12-17