# 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,
    recipient_organization_id,
    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 = 'NONE' OR
    reset_scope LIKE 'YEAR_%'
  )
) 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<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

```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<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

```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<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

```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
