# 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 schema - `document-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 ```sql 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 ```sql 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 ```sql 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 ```sql CREATE TABLE document_number_errors ( id BIGINT AUTO_INCREMENT PRIMARY KEY, error_type ENUM( 'LOCK_TIMEOUT', 'VERSION_CONFLICT', 'DB_ERROR', 'REDIS_ERROR', 'VALIDATION_ERROR', '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 ```sql 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 ```mermaid 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) ```mermaid 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 ```typescript // services/counter.service.ts @Injectable() export class CounterService { private readonly logger = new Logger(CounterService.name); constructor( @InjectRepository(DocumentNumberCounter) private counterRepo: Repository, private dataSource: DataSource, ) {} async incrementCounter(counterKey: CounterKey): Promise { 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 ```typescript // 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 { 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 { 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 ```typescript // 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, ): Promise { // 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 { 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 { 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 { 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 ```typescript // 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 ```typescript @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 ```mermaid 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 ```bash # Run unit tests pnpm test:watch -- --testPathPattern=document-numbering ``` ### 8.2 Integration Tests ```bash # Run integration tests pnpm test:e2e -- --testPathPattern=numbering ``` ### 8.3 Concurrency Test ```typescript // 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 ```bash # 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 - [Requirements](../01-requirements/01-03.11-document-numbering.md) - [Operations Guide](../04-operations/04-08-document-numbering-operations.md) - [ADR-018 Document Numbering](file:///d:/nap-dms.lcbp3/specs/05-decisions/adr-018-document-numbering.md) - [Backend Guidelines](03-02-backend-guidelines.md) --- **Document Version**: 2.0.0 **Created By**: Development Team **Last Updated**: 2025-12-17