Aller au contenu

PD-250 — Plan d'implémentation — Job destruction définitive et bordereau

1. Hypothèses d'implémentation

ID Hypothèse Impact si invalide
HYP-IMPL-01 L'entity DocumentSecure (PD-16/PD-63) est extensible avec de nouveaux statuts (DESTROYED, RECONCILIATION_FAILED) et une colonne had_legal_lock sans migration destructive. Migration bloquante — nécessite stratégie ALTER TYPE + backfill
HYP-IMPL-02 Le module tsa/ (PD-39) expose un service injectable pour signature + horodatage RFC 3161 réutilisable hors du flux batch TSA existant. Si non : créer un adapter dédié autour du TsaClient
HYP-IMPL-03 Le module audit/ (PD-37) expose AuditLogService.log() injectable qui accepte des entries custom avec metadata batchId/bordereauId. Si non : étendre l'interface AuditEntry
HYP-IMPL-04 @aws-sdk/client-s3 (v3.958.0) supporte DeleteObjectCommand avec gestion native des codes 204. Si non : wrapper HTTP manuel
HYP-IMPL-05 La bibliothèque pdf-lib est compatible CJS (pas ESM-only) et ne nécessite pas de configuration Jest spéciale. Si ESM-only : utiliser pdfkit comme alternative CJS
HYP-IMPL-06 EventEmitter2 (PD-21) supporte le pattern @OnEvent pour les préavis et résultats batch sans modification du bus existant. Si non : enregistrement manuel des listeners
HYP-IMPL-07 La séquence PostgreSQL audit_seq peut être créée dans le schéma vault_secure et utilisée par TypeORM dans les transactions. Si restriction de schéma : créer dans public
HYP-IMPL-08 Le guard AuthorizationGuard + @Roles('ADMIN') existant (PD-63) est réutilisable tel quel pour l'endpoint admin bordereaux. Si non : créer guard spécifique
HYP-IMPL-09 Les queues BullMQ PD-250 utilisent le séparateur - (pas :) conformément à INV-250-13, même si les queues PD-21 existantes utilisent :. Pas de migration des queues existantes. Pas d'impact — convention pour les nouvelles queues uniquement
HYP-IMPL-10 LegalDestructionService (PD-81) est injectable et son pattern de zeroization (Buffer.fill(0) + SERIALIZABLE TX) est directement réutilisable pour la zeroization des encryptedMetadata de documents. Si mécanisme différent : adapter pour les documents vs ReKeys

2. Architecture et découpage en composants

2.1 Arborescence cible

src/modules/destruction/
├── destruction.module.ts                    # Module NestJS principal
├── config/
│   └── destruction.config.ts                # Validation Joi des 13 paramètres (§10.2 + §10.3)
├── entities/
│   ├── destruction-batch.entity.ts          # Entity batch de destruction
│   └── destruction-bordereau.entity.ts      # Entity bordereau (PDF/A + metadata)
├── enums/
│   ├── batch-status.enum.ts                 # PENDING | RUNNING | SUCCESS | PARTIAL_FAILED | FAILED
│   └── destruction-audit-action.enum.ts     # Actions d'audit spécifiques destruction
├── services/
│   ├── eligibility.service.ts               # Sélection éligible (§5.1) + clockSkewTolerance
│   ├── bordereau.service.ts                 # Génération PDF/A + signature + TSA (§5.2-5.3)
│   ├── destruction-execution.service.ts     # Destruction unitaire séquentielle (§5.5)
│   ├── reconciliation.service.ts            # Réconciliation post-crash (ERR-250-05)
│   ├── destruction-audit.service.ts         # Audit transactionnel + séquence audit_seq (§10.4b)
│   ├── destruction-alert.service.ts         # Alertes et métriques (§5.10)
│   ├── document-zeroization.service.ts      # Zeroization flux legal_lock (INV-250-12)
│   └── document-state-machine.service.ts    # Machine à états documents (§10.5)
├── processors/
│   ├── destruction.processor.ts             # Processor BullMQ job destruction principal
│   └── pre-notice.processor.ts             # Processor BullMQ job préavis (§5.8)
├── controllers/
│   └── bordereau.controller.ts              # Endpoint admin GET /admin/bordereaux (§5.7)
├── dto/
│   ├── bordereau-query.dto.ts               # DTO filtres date/batch
│   └── bordereau-response.dto.ts            # DTO réponse API
├── events/
│   ├── destruction.events.ts                # Types événements bus interne
│   └── pre-notice.events.ts                 # Types événements préavis
└── __tests__/
    ├── eligibility.service.spec.ts
    ├── bordereau.service.spec.ts
    ├── destruction-execution.service.spec.ts
    ├── reconciliation.service.spec.ts
    ├── destruction-audit.service.spec.ts
    ├── document-zeroization.service.spec.ts
    ├── document-state-machine.service.spec.ts
    ├── destruction.processor.spec.ts
    ├── pre-notice.processor.spec.ts
    ├── bordereau.controller.spec.ts
    ├── destruction.config.spec.ts
    └── contractual/
        ├── queue-naming.spec.ts              # TC-250-18 (INV-250-13)
        ├── deprecated-api.spec.ts            # TC-250-19 (INV-250-14)
        └── state-transitions.spec.ts         # TC-250-15 (INV-250-11)

2.2 Vue d'ensemble des composants

Composant Responsabilité Dépendances internes Dépendances externes
destruction.config Validation Joi des 13 paramètres + 3 SLA ConfigModule (PD-22) Joi
eligibility.service Sélection WORM + clockSkewTolerance DocumentSecure, DocumentStateMachine TypeORM
bordereau.service PDF/A + signature + TSA TsaModule (PD-39) pdf-lib
destruction-execution.service Orchestration séquentielle par document eligibility, bordereau, audit, zeroization, alert S3Client
reconciliation.service Convergence post-crash audit, alert, state-machine TypeORM
destruction-audit.service Audit transactionnel (audit_seq) AuditLogService (PD-37) TypeORM
destruction-alert.service Alertes SLA + métriques batch EventEmitter2
document-zeroization.service Zeroization encryptedMetadata (flux legal_lock) TypeORM
document-state-machine.service Gardes transitions + RECONCILIATION_FAILED TypeORM
destruction.processor Job BullMQ principal execution, config, alert BullMQ
pre-notice.processor Job BullMQ préavis quotidien eligibility, EventEmitter2 BullMQ
bordereau.controller API admin GET /admin/bordereaux AuthGuard, @Roles('ADMIN'), audit TypeORM

3. Flux de données et orchestration

3.1 Flux nominal de destruction

┌─────────────────────────────────────────────────────────────────────┐
│ destruction.processor (Job BullMQ pv-jobs-destruction)              │
│                                                                     │
│  1. Validation config (Joi)                                         │
│     └─ ERR-250-08 → FAILED + alerte si hors bornes                │
│                                                                     │
│  2. eligibility.service.selectEligible(batchSize)                   │
│     └─ SELECT ... WHERE status='EXPIRED'                           │
│       AND retention_until + clockSkew < NOW()                      │
│       AND (s3_lock_until + clockSkew < NOW())                      │
│       AND (legal_lock = false OR legal_lock_until + clockSkew < NOW())│
│       AND type != 'BORDEREAU'         ← MIN-13                    │
│       LIMIT batchSize                                               │
│     └─ Si 0 documents → fin normale (pas de batch créé)           │
│                                                                     │
│  3. Créer batch entity (PENDING)                                    │
│     └─ parentBatchId si reprise                                    │
│     └─ Transition PENDING → RUNNING                                │
│     └─ Démarrer chrono batchFinalizeSla                            │
│                                                                     │
│  4. bordereau.service.generate(documents)                           │
│     ├─ 4a. Assembler payload (champs autorisés §3 uniquement)      │
│     ├─ 4b. Générer PDF/A (pdf-lib)                                 │
│     ├─ 4c. Signer (eIDAS art. 26 via HSM PD-36)                   │
│     ├─ 4d. Horodater (TSA RFC 3161 via PD-39)                     │
│     │   └─ tsaRetryCount tentatives max → ERR-250-01               │
│     ├─ 4e. Valider signature + timestamp (fail-closed)             │
│     │   └─ Invalide → ERR-250-02 → FAILED                         │
│     ├─ 4f. Persister bordereau (S3 + DB, SEALED, retentionExpiry=null)│
│     │   └─ Échec persistance → ERR-250-06 → FAILED                │
│     └─ 4g. Retourner bordereauId                                  │
│                                                                     │
│  ─── POINT FAIL-CLOSED : aucune destruction avant 4g ───           │
│                                                                     │
│  5. Démarrer chrono destructionExecutionSla                         │
│     └─ Si dépassé avant 1er document → FAILED (MAJ-26)            │
│                                                                     │
│  6. destruction-execution.service.executeSequential(docs, bordereau)│
│     └─ Pour chaque document (séquentiel) :                         │
│        ├─ 6a. Re-vérifier éligibilité (état courant)               │
│        │   └─ Non éligible → ERR-250-03 → skip + audit anomalie    │
│        ├─ 6b. Si flux legal_lock :                                 │
│        │   └─ zeroization.service.zeroize(doc)                     │
│        │       └─ Échec → fail-closed, skip doc (MAJ-27)          │
│        ├─ 6c. S3 DeleteObject (await, retry s3DeleteRetryCount)    │
│        │   └─ Échec → ERR-250-04 → skip doc                       │
│        ├─ 6d. Confirmation S3 (HTTP 204)                           │
│        └─ 6e. Transaction DB atomique :                            │
│            ├─ UPDATE status = DESTROYED, deleted_at = NOW()        │
│            ├─ INSERT audit_log (destruction-audit.service)          │
│            │   └─ documentId + batchId + bordereauId + audit_seq   │
│            └─ COMMIT                                               │
│               └─ Échec → ERR-250-05 → reconciliation              │
│        └─ Vérifier destructionExecutionSla en cours                │
│           └─ Si dépassé → PARTIAL_FAILED (MAJ-26)                 │
│                                                                     │
│  7. Finaliser batch                                                 │
│     ├─ Vérifier batchFinalizeSla                                   │
│     ├─ Calculer état final (SUCCESS / PARTIAL_FAILED / FAILED)     │
│     └─ destruction-alert.service.publishBatchResult(metrics)       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

3.2 Flux de réconciliation (ERR-250-05)

┌─────────────────────────────────────────────────────────────────┐
│ reconciliation.service                                          │
│                                                                 │
│  Déclencheur : échec DB après suppression S3 confirmée         │
│                                                                 │
│  1. Retry DB (reconciliationDbRetryCount tentatives)           │
│     ├─ Succès → état terminal = DESTROYED + audit DESTROYED    │
│     └─ Échec persistant → étape 2                              │
│                                                                 │
│  2. Vérifier reconciliationSla                                  │
│     ├─ Dans SLA → retry avec backoff exponentiel               │
│     └─ Hors SLA → étape 3                                      │
│                                                                 │
│  3. État terminal = RECONCILIATION_FAILED                       │
│     ├─ Audit de type RECONCILIATION_FAILED (pas DESTROYED)     │
│     ├─ Escalade critique obligatoire                           │
│     └─ RECONCILIATION_FAILED → * : INTERDITE (MAJ-28)         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.3 Flux de préavis (§5.8)

┌─────────────────────────────────────────────────────────────────┐
│ pre-notice.processor (Job BullMQ pv-jobs-prenotice, quotidien)  │
│                                                                 │
│  1. eligibility.service.selectPreNotice(preNoticeDays)          │
│     └─ SELECT ... WHERE eligible_at BETWEEN NOW() + N days     │
│        AND N=0 → jour J, avant run destruction                 │
│                                                                 │
│  2. Pour chaque document :                                      │
│     └─ EventEmitter2.emit('destruction.pre-notice', {          │
│         documentId, eligibleAt, preNoticeDays                  │
│     })                                                          │
│     └─ Pas de PII dans le payload                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.4 Diagrammes Mermaid

3.4.1 Graphe de dépendances des modules

graph TD
    subgraph "Module destruction"
        DP[destruction.processor]
        PNP[pre-notice.processor]
        BC[bordereau.controller]
        ES[eligibility.service]
        BS[bordereau.service]
        DES[destruction-execution.service]
        RS[reconciliation.service]
        DAS[destruction-audit.service]
        DALS[destruction-alert.service]
        DZS[document-zeroization.service]
        DSM[document-state-machine.service]
        DC[destruction.config]
    end

    subgraph "Modules ProbatioVault existants"
        TSA[TsaModule PD-39]
        AUDIT[AuditLogService PD-37]
        AUTH[AuthGuard + @Roles PD-63]
        CONFIG[ConfigModule PD-22]
        EE2[EventEmitter2 PD-21]
        S3[S3Client PD-43]
        HSM[HSM PD-36]
    end

    subgraph "Dépendances externes"
        PDFLIB[pdf-lib]
        BULLMQ[BullMQ]
        JOI[Joi]
        TYPEORM[TypeORM]
    end

    %% Processor principal
    DP --> DC
    DP --> DES
    DP --> DALS
    DP --> BULLMQ

    %% Processor préavis
    PNP --> ES
    PNP --> EE2
    PNP --> BULLMQ

    %% Contrôleur admin
    BC --> AUTH
    BC --> DAS
    BC --> TYPEORM

    %% Service d'exécution
    DES --> ES
    DES --> BS
    DES --> DAS
    DES --> DZS
    DES --> DALS
    DES --> DSM
    DES --> S3

    %% Service d'éligibilité
    ES --> DSM
    ES --> TYPEORM

    %% Service bordereau
    BS --> TSA
    BS --> HSM
    BS --> PDFLIB

    %% Service réconciliation
    RS --> DAS
    RS --> DALS
    RS --> DSM
    RS --> TYPEORM

    %% Service audit
    DAS --> AUDIT
    DAS --> TYPEORM

    %% Config
    DC --> CONFIG
    DC --> JOI

3.4.2 Séquence du flux nominal de destruction

sequenceDiagram
    participant BULL as BullMQ Queue
    participant DP as destruction.processor
    participant DC as destruction.config
    participant ES as eligibility.service
    participant BS as bordereau.service
    participant TSA as TsaModule (PD-39)
    participant HSM as HSM (PD-36)
    participant DES as destruction-execution.service
    participant DZS as document-zeroization.service
    participant S3 as S3Client
    participant DB as PostgreSQL
    participant DAS as destruction-audit.service
    participant DALS as destruction-alert.service

    BULL->>DP: Job pv-jobs-destruction
    DP->>DC: Validation config (Joi)
    DC-->>DP: Config validée

    DP->>ES: selectEligible(batchSize)
    ES->>DB: SELECT WHERE status=EXPIRED, WORM expiré, clockSkew
    DB-->>ES: documents[]
    ES-->>DP: documents éligibles

    Note over DP: Créer batch entity (PENDING → RUNNING)

    DP->>BS: generate(documents)
    BS->>BS: Assembler payload (champs autorisés §3)
    BS->>BS: Générer PDF/A (pdf-lib)
    BS->>HSM: Signer (eIDAS art. 26)
    HSM-->>BS: signature
    BS->>TSA: Horodater RFC 3161
    TSA-->>BS: timestamp token
    BS->>BS: Valider signature + timestamp (fail-closed)
    BS->>DB: Persister bordereau (SEALED, retentionExpiry=null)
    BS-->>DP: bordereauId

    Note over DP: POINT FAIL-CLOSED — aucune destruction avant bordereauId

    DP->>DES: executeSequential(docs, bordereau)

    loop Pour chaque document (séquentiel)
        DES->>ES: Re-vérifier éligibilité
        ES-->>DES: éligible / non éligible

        alt Flux legal_lock
            DES->>DZS: zeroize(doc)
            DZS->>DB: Buffer.fill(0) + SERIALIZABLE TX
            DZS-->>DES: zeroization OK
        end

        DES->>S3: DeleteObject (retry s3DeleteRetryCount)
        S3-->>DES: HTTP 204 confirmation

        DES->>DB: BEGIN TX
        DES->>DB: UPDATE status=DESTROYED, deleted_at=NOW()
        DES->>DAS: audit(documentId, batchId, bordereauId, audit_seq)
        DAS->>DB: INSERT audit_log
        DES->>DB: COMMIT
    end

    DP->>DP: Calculer état final (SUCCESS / PARTIAL_FAILED / FAILED)
    DP->>DALS: publishBatchResult(metrics)

3.4.3 Séquence du flux de réconciliation (ERR-250-05)

sequenceDiagram
    participant DES as destruction-execution.service
    participant RS as reconciliation.service
    participant DB as PostgreSQL
    participant DAS as destruction-audit.service
    participant DALS as destruction-alert.service
    participant DSM as document-state-machine.service

    DES->>RS: Échec DB après suppression S3 confirmée

    loop Retry DB (reconciliationDbRetryCount)
        RS->>DB: Retry UPDATE status=DESTROYED + INSERT audit
        alt Succès
            DB-->>RS: OK
            RS->>DAS: audit DESTROYED
            RS-->>DES: état terminal DESTROYED
        else Échec persistant
            DB-->>RS: Erreur
        end
    end

    alt Hors reconciliationSla
        RS->>DSM: Transition → RECONCILIATION_FAILED
        RS->>DAS: audit RECONCILIATION_FAILED
        RS->>DALS: Escalade critique obligatoire
        Note over RS: RECONCILIATION_FAILED → * : INTERDITE (MAJ-28)
    end

4. Mapping INV/CA/ERR → Tâches

4.1 Invariants → Tâches

INV Tâche(s) Mécanisme
INV-250-01 TASK-02, TASK-13 Query SQL avec triple condition + clockSkewTolerance soustractive
INV-250-02 TASK-02 Clause WHERE status = 'EXPIRED'
INV-250-03 TASK-03, TASK-04, TASK-12 Fail-closed dans processor : pas de destruction sans bordereauId valide
INV-250-04 TASK-03 bordereau.service.generate() retourne exactement 1 bordereau par batch
INV-250-05 TASK-06, TASK-13 Audit transactionnel (même TX que DESTROYED) + séquence audit_seq
INV-250-06 TASK-03, TASK-02 retentionExpiry = null + filtre exclusion type BORDEREAU
INV-250-07 TASK-03 Schéma bordereau restreint aux champs autorisés §3, pas de PII
INV-250-08 TASK-02, TASK-04 WHERE status != 'DESTROYED' + idempotence sur statut terminal
INV-250-09 TASK-03 Boucle retry TSA avec compteur ≤ tsaRetryCount puis FAILED
INV-250-10 TASK-07, TASK-06 Alerte + audit pour chaque erreur partielle
INV-250-11 TASK-08, TASK-14 State machine avec garde explicite sur transitions interdites
INV-250-12 TASK-09, TASK-04 Branchement conditionnel sur had_legal_lock
INV-250-13 TASK-01, TASK-14 Constantes pv-jobs-destruction, pv-jobs-prenotice (sans :)
INV-250-14 TASK-12, TASK-14 getJobSchedulers/removeJobScheduler uniquement
INV-250-15 TASK-10, TASK-14 @Roles('ADMIN') + AuthorizationGuard + audit access denied
INV-250-16 TASK-04, TASK-05, TASK-07 Chronos SLA dans processor avec comportements contractuels

4.2 Critères d'acceptation → Tâches + Tests

CA Tâche(s) Test(s)
CA-250-01 TASK-02 TC-250-01, TC-250-02, TC-250-03
CA-250-02 TASK-03 TC-250-08
CA-250-03 TASK-03, TASK-12 TC-250-04, TC-250-05
CA-250-04 TASK-06 TC-250-09, TC-250-16
CA-250-05 TASK-03, TASK-02 TC-250-10
CA-250-06 TASK-03 TC-250-11
CA-250-07 TASK-03 TC-250-05
CA-250-08 TASK-02, TASK-04 TC-250-12, TC-250-13
CA-250-09 TASK-02 TC-250-03
CA-250-10 TASK-10 TC-250-21
CA-250-11 TASK-11 TC-250-22
CA-250-12 TASK-08 TC-250-15
CA-250-13 TASK-01 TC-250-18
CA-250-14 TASK-12 TC-250-19
CA-250-15 TASK-10 TC-250-20
CA-250-16 TASK-04, TASK-05, TASK-07 TC-250-26, TC-250-27, TC-250-28

4.3 Cas d'erreur → Tâches

ERR Tâche(s) Mécanisme
ERR-250-01 TASK-03 Boucle retry TSA ≤ tsaRetryCount puis batch FAILED
ERR-250-02 TASK-03 Vérification signature → invalide = batch FAILED
ERR-250-03 TASK-04 Re-vérification éligibilité pré-destruction → skip + audit anomalie
ERR-250-04 TASK-04 S3 DeleteObject retry ≤ s3DeleteRetryCount → skip + audit
ERR-250-05 TASK-05 Réconciliation DB retry → DESTROYED ou RECONCILIATION_FAILED
ERR-250-06 TASK-03 Échec persistance bordereau → batch FAILED, 0 destruction
ERR-250-07 TASK-07, TASK-06 Assert sur audit + alerte pour chaque erreur, fail-closed sinon
ERR-250-08 TASK-01 Joi validation au boot → rejet + alerte si hors bornes

4.4 Réserves Gate 3 → Tâches

Réserve Résolution Tâche(s)
MAJ-25 Le code implémente await séquentiel par document (pas de fire-and-forget). Le terme « Async post-validation » dans §10.4 signifie que la phase de suppression commence APRÈS la validation du bordereau, pas qu'elle est asynchrone au sens JS. TASK-04, TASK-12
MAJ-26 Deux comportements distincts dans destruction.processor : (1) SLA dépassé AVANT 1er doc → FAILED, (2) SLA dépassé EN COURS → PARTIAL_FAILED. TASK-04, TASK-12
MAJ-27 Test dédié TC-250-17b : échec zeroization → doc non supprimé de S3 (fail-closed). Implémenté dans document-zeroization.service.spec.ts. TASK-09, TASK-14
MAJ-28 RECONCILIATION_FAILED → * ajouté comme transition interdite dans document-state-machine.service. TC-250-15 étendu à 5 transitions (ajout RECONCILIATION_FAILED → DESTROYED). TASK-08, TASK-14
MIN-11 Test parentBatchId obligatoire dans destruction.processor.spec.ts : vérifier que les batches de reprise ont parentBatchId non null. TASK-12, TASK-14
MIN-12 Guard EventEmitter2 sur événements batch_result : seuls les listeners enregistrés avec tag ADMIN/SYSTEM reçoivent les données. Test dans destruction-alert.service.spec.ts. TASK-07, TASK-14
MIN-13 Filtre explicite AND entity_type != 'BORDEREAU' dans la requête de sélection d'éligibilité. TASK-02
MIN-14 Frontière documentée : TC-250-08 teste la validité technique (structure PDF/A + signature + TSA). La qualification eIDAS du TSP est un contrôle opérationnel (runbook). TASK-03
MIN-15 Stratégie d'échantillonnage documentée dans le plan de test : 3 paramètres testés sur bornes min ET max (batchSize, tsaRetryCount, clockSkewTolerance). TASK-14

5. Tâches détaillées

TASK-01 — Configuration et validation

Objectif : Définir les 13 paramètres numériques (§10.2) + 3 SLA (§10.3) avec validation Joi stricte (rejet, pas de clamp).

Fichiers : - src/modules/destruction/config/destruction.config.ts - src/modules/destruction/__tests__/destruction.config.spec.ts

Mécanismes : - Schema Joi avec trackedDefault() pour chaque paramètre (pattern PD-22 existant) - Bornes min/max contractuelles : destructionBatchSize [1, 5000], destructionJobInterval [1, 168], preNoticeDays [0, 365], tsaRetryCount [0, 5], tsaTimeout [1000, 30000], signatureTimeout [1000, 60000], s3DeleteRetryCount [0, 10], s3DeleteTimeout [1000, 60000], clockSkewTolerance [1, 60], reconciliationDbRetryCount [1, 10] - SLA : batchFinalizeSla [5min, 24h], destructionExecutionSla [1min, 24h], reconciliationSla [1h, 168h] - Noms de queue : DESTRUCTION: 'pv-jobs-destruction', PRE_NOTICE: 'pv-jobs-prenotice' (sans :, INV-250-13) - Options Joi : abortEarly: false, allowUnknown: false, stripUnknown: false

Observables : - Rejet avec message explicite si paramètre hors bornes (ERR-250-08) - Alerte émise sur EventEmitter2 si rejet de configuration - Log des defaults appliqués au démarrage

Invariants couverts : INV-250-13

Tests : TC-250-18 (queue names), TC-250-23 (config hors bornes)


TASK-02 — Service d'éligibilité

Objectif : Implémenter la sélection des documents éligibles à la destruction avec les 3 conditions simultanées + exclusions.

Fichiers : - src/modules/destruction/services/eligibility.service.ts - src/modules/destruction/__tests__/eligibility.service.spec.ts

Mécanismes :

  1. Méthode selectEligible(batchSize: number): Promise<DocumentSecure[]> :
    SELECT d.* FROM vault_secure.documents d
    WHERE d.status = 'EXPIRED'
      AND d.deleted_at IS NULL
      AND d.entity_type != 'BORDEREAU'                    -- MIN-13
      AND d.retention_until + INTERVAL ':clockSkew seconds' < NOW()  -- DB retention
      AND (d.s3_lock_until IS NULL OR d.s3_lock_until + INTERVAL ':clockSkew seconds' < NOW())  -- S3 Object Lock
      AND (d.legal_lock = false OR d.legal_lock_until + INTERVAL ':clockSkew seconds' < NOW())  -- legal_lock
    ORDER BY d.retention_until ASC
    LIMIT :batchSize
    
  2. clockSkewTolerance appliqué comme marge soustractive (§5.1)
  3. Filtre entity_type != 'BORDEREAU' pour MIN-13 (exclusion des bordereaux de la sélection)

  4. Méthode selectPreNotice(preNoticeDays: number): Promise<DocumentSecure[]> :

  5. Sélectionne les documents dont la date d'éligibilité est dans J+N jours
  6. Même conditions de base mais sur une fenêtre future
  7. N=0 : jour même de l'éligibilité

  8. Méthode isStillEligible(documentId: string): Promise<boolean> :

  9. Re-vérification unitaire avant destruction (ERR-250-03)
  10. Utilise FOR UPDATE pour éviter les races conditions

Observables : - Journal d'exécution avec nombre de documents sélectionnés + raisons d'exclusion - Trace de décision par document (motif : DB_RETENTION_ACTIVE, S3_LOCK_ACTIVE, LEGAL_LOCK_ACTIVE, STATUS_NOT_EXPIRED, TYPE_BORDEREAU)

Invariants couverts : INV-250-01, INV-250-02, INV-250-06, INV-250-08

Tests : TC-250-01, TC-250-02, TC-250-03, TC-250-10, TC-250-12


TASK-03 — Service de génération du bordereau

Objectif : Générer un bordereau PDF/A consolidé, signé et horodaté RFC 3161, avec conservation indéfinie.

Fichiers : - src/modules/destruction/services/bordereau.service.ts - src/modules/destruction/__tests__/bordereau.service.spec.ts

Dépendance externe nouvelle : pdf-lib (à ajouter au package.json)

Mécanismes :

  1. Méthode generate(batchId, documents): Promise<BordereauResult> :
  2. Assembler le payload avec uniquement les champs autorisés (§3) : batchId, horodatage run, documentId technique, type documentaire, hash probatoire, date scellement, date expiration, date destruction prévue
  3. PAS de données personnelles directes (INV-250-07) : nom, prénom, email, adresse, téléphone, NIR exclus
  4. Générer PDF/A avec pdf-lib (PDFDocument.create(), setProducer, setCreationDate, embedFont)
  5. Format tabulaire dans le PDF : une ligne par document

  6. Signature électronique :

  7. Appel au service HSM (PD-36) pour signature qualifiée eIDAS art. 26
  8. Timeout contrôlé par signatureTimeout
  9. Vérification post-signature obligatoire (fail-closed)

  10. Horodatage RFC 3161 :

  11. Appel au module TSA (PD-39)
  12. Retry contrôlé : tsaRetryCount tentatives max
  13. Timeout par tentative : tsaTimeout
  14. Échec persistant → ERR-250-01 → batch FAILED

  15. Persistance :

  16. Sauvegarder le bordereau comme document probatoire dans S3 (avec Object Lock COMPLIANCE, rétention maximale)
  17. Créer entity destruction_bordereau en DB avec retentionExpiry = null (INV-250-06)
  18. Statut SEALED permanent (jamais éligible à la destruction automatique)
  19. Échec persistance → ERR-250-06 → batch FAILED

Observables : - Artefact bordereau PDF/A vérifiable - Jeton TSA RFC 3161 stocké - Signature HSM vérifiable

Invariants couverts : INV-250-03, INV-250-04, INV-250-06, INV-250-07, INV-250-09

Tests : TC-250-04, TC-250-05, TC-250-08, TC-250-08b, TC-250-10, TC-250-11

Note MIN-14 : La validité technique (structure PDF/A, signature, TSA) est testée automatiquement. La qualification eIDAS du TSP est un contrôle opérationnel (runbook de mise en service, hors scope tests automatisés).


TASK-04 — Service d'exécution de destruction

Objectif : Orchestrer la destruction unitaire séquentielle post-validation du bordereau.

Fichiers : - src/modules/destruction/services/destruction-execution.service.ts - src/modules/destruction/__tests__/destruction-execution.service.spec.ts

Mécanismes :

  1. Méthode executeSequential(documents, batchId, bordereauId): Promise<ExecutionResult> :
  2. Boucle for...of sur les documents (séquentiel, pas de Promise.all — résolution MAJ-25)
  3. Compteurs : destroyed, skipped, failed

  4. Pour chaque document (pattern await séquentiel) :

  5. Re-vérification éligibilité : eligibility.isStillEligible(doc.id) — si non éligible → ERR-250-03, skip avec audit anomalie
  6. Branchement flux legal_lock (INV-250-12) : si doc.hadLegalLock === true → appel zeroization.zeroize(doc) — échec → fail-closed, skip doc (MAJ-27)
  7. S3 DeleteObject : await s3Client.send(new DeleteObjectCommand({...})) — retry ≤ s3DeleteRetryCount — timeout s3DeleteTimeout — confirmation HTTP 204 — échec → ERR-250-04, skip
  8. Transaction DB atomique (§10.4b) :

    await queryRunner.startTransaction();
    // 1. UPDATE document SET status = 'DESTROYED', deleted_at = NOW()
    // 2. INSERT audit_log (documentId, batchId, bordereauId, audit_seq)
    await queryRunner.commitTransaction();
    
    — Échec transaction → ERR-250-05 → déclencher réconciliation

  9. Gestion SLA destructionExecutionSla (résolution MAJ-26) :

  10. Chrono démarré après validation bordereau
  11. Si dépassé AVANT traitement du 1er document : batch FAILED (car 0 destruction)
  12. Si dépassé EN COURS de traitement : batch PARTIAL_FAILED (car au moins 1 document traité)

Observables : - Compteurs destroyed/skipped/failed par batch - Audit d'anomalie pour chaque document skippé - Trace de zeroization pour flux legal_lock

Invariants couverts : INV-250-03, INV-250-08, INV-250-10, INV-250-12, INV-250-16

Tests : TC-250-06, TC-250-07, TC-250-12, TC-250-17, TC-250-25, TC-250-27


TASK-05 — Service de réconciliation

Objectif : Converger vers un état terminal contractuel après crash post-suppression S3.

Fichiers : - src/modules/destruction/services/reconciliation.service.ts - src/modules/destruction/__tests__/reconciliation.service.spec.ts

Mécanismes :

  1. Méthode reconcile(documentId, batchId, bordereauId): Promise<ReconciliationResult> :
  2. Retry DB : reconciliationDbRetryCount tentatives avec backoff exponentiel
  3. Si réussi → état terminal = DESTROYED + audit DESTROYED
  4. Si échec persistant → vérifier reconciliationSla

  5. Vérification SLA :

  6. Chrono démarré au moment du premier échec DB
  7. Si dans SLA → retry
  8. Si hors SLA → état terminal = RECONCILIATION_FAILED

  9. État RECONCILIATION_FAILED :

  10. Audit de type RECONCILIATION_FAILED (pas DESTROYED) — complétude INV-250-05
  11. Escalade critique obligatoire via destruction-alert.service
  12. RECONCILIATION_FAILED → * : INTERDITE (MAJ-28)

Observables : - Audit d'anomalie critique avec détails du crash - Alerte d'escalade émise - État terminal conforme (DESTROYED ou RECONCILIATION_FAILED)

Invariants couverts : INV-250-10, INV-250-16

Tests : TC-250-07, TC-250-13, TC-250-28


TASK-06 — Service d'audit de destruction

Objectif : Implémenter l'audit transactionnel avec séquence audit_seq pour garantir non-perte, ordre causal et complétude.

Fichiers : - src/modules/destruction/services/destruction-audit.service.ts - src/modules/destruction/__tests__/destruction-audit.service.spec.ts

Mécanismes :

  1. Séquence PostgreSQL audit_seq :
  2. Créée dans la migration (TASK-13) : CREATE SEQUENCE vault_secure.audit_destruction_seq
  3. Utilisée comme clé d'ordonnancement monotone (pas l'horloge système)
  4. Chaque audit log reçoit nextval('vault_secure.audit_destruction_seq')

  5. Méthode logDestruction(queryRunner, params): Promise<void> :

  6. Exécutée dans la même transaction que la finalisation DESTROYED (§10.4b)
  7. Paramètres : documentId, batchId, bordereauId, eventType (DESTROYED ou RECONCILIATION_FAILED)
  8. Appel à AuditLogService existant (PD-37) avec metadata étendue

  9. Actions d'audit spécifiques (extension de AuditActionType) :

  10. DOCUMENT_DESTROY : destruction unitaire réussie
  11. DOCUMENT_DESTROY_BATCH : événement batch-level
  12. DOCUMENT_DESTROY_BORDEREAU : création bordereau
  13. DOCUMENT_DESTROY_RECONCILIATION_FAILED : échec réconciliation
  14. DOCUMENT_DESTROY_ACCESS_DENIED : accès admin refusé (INV-250-15)

Observables : - Cardinalité audit = cardinalité documents traités (1:1) - Monotonie par audit_seq (pas par timestamp) - Corrélation documentId/batchId/bordereauId dans chaque entry

Invariants couverts : INV-250-05

Tests : TC-250-09, TC-250-16


TASK-07 — Service d'alertes et métriques

Objectif : Publier les résultats batch et déclencher les alertes SLA.

Fichiers : - src/modules/destruction/services/destruction-alert.service.ts - src/modules/destruction/__tests__/destruction-alert.service.spec.ts

Mécanismes :

  1. Méthode publishBatchResult(metrics): void (§5.10) :
  2. Émet sur EventEmitter2 : destruction.batch_result
  3. Payload : compteurs agrégés (documents traités/détruits/en erreur, durée), pas d'identifiants individuels
  4. Restriction d'accès (MIN-12) : l'événement est tagué ADMIN|SYSTEM — les listeners doivent vérifier leur rôle

  5. Alertes SLA (INV-250-16) :

  6. batchFinalizeSla dépassé → alerte critique + batch FAILED
  7. destructionExecutionSla dépassé → alerte + batch PARTIAL_FAILED ou FAILED selon MAJ-26
  8. reconciliationSla dépassé → escalade critique + RECONCILIATION_FAILED

  9. Alerte fail-closed (ERR-250-07) :

  10. Si chaîne d'audit indisponible → arrêt du job + alerte immédiate
  11. Échec silencieux = non conforme

Observables : - Événement destruction.batch_result sur le bus - Alertes critiques émises pour chaque dépassement SLA - Absence d'alerte = batch nominal (vérifiable)

Invariants couverts : INV-250-10, INV-250-16

Tests : TC-250-14, TC-250-26, TC-250-27, TC-250-28


TASK-08 — Machine à états des documents

Objectif : Implémenter les gardes de transition d'état pour les documents, incluant RECONCILIATION_FAILED.

Fichiers : - src/modules/destruction/services/document-state-machine.service.ts - src/modules/destruction/__tests__/document-state-machine.service.spec.ts

Mécanismes :

  1. Transitions autorisées (§10.5) :
  2. PENDING → SEALED (existant)
  3. SEALED → EXPIRED (existant)
  4. EXPIRED → DESTROYED (PD-250)
  5. EXPIRED → RECONCILIATION_FAILED (PD-250, ERR-250-05)

  6. Transitions INTERDITES (INV-250-11 + MAJ-28) :

  7. SEALED → PENDING : INTERDITE
  8. EXPIRED → SEALED : INTERDITE
  9. DESTROYED → EXPIRED : INTERDITE
  10. DESTROYED → SEALED : INTERDITE
  11. RECONCILIATION_FAILED → * : INTERDITE (toute transition depuis cet état)

  12. Méthode validateTransition(currentStatus, newStatus): void :

  13. Vérifie dans la matrice des transitions autorisées
  14. Si transition interdite → throw avec code d'erreur explicite + audit

  15. Méthode applyTransition(queryRunner, documentId, newStatus): Promise<void> :

  16. Appelle validateTransition puis exécute l'UPDATE

Observables : - Rejet explicite avec code d'erreur pour chaque transition interdite - Trace d'audit pour chaque tentative de transition inverse

Invariants couverts : INV-250-11

Tests : TC-250-15 (étendu à 5 transitions : les 4 originales + RECONCILIATION_FAILED → DESTROYED)


TASK-09 — Service de zeroization document

Objectif : Implémenter la zeroization cryptographique pour le flux legal_lock (INV-250-12).

Fichiers : - src/modules/destruction/services/document-zeroization.service.ts - src/modules/destruction/__tests__/document-zeroization.service.spec.ts

Mécanismes :

  1. Méthode zeroize(documentId: string, queryRunner: QueryRunner): Promise<ZeroizationResult> :
  2. Pattern PD-81 adapté : transaction SERIALIZABLE
  3. Charger encryptedMetadata (Buffer)
  4. Buffer.fill(0) pour écraser en mémoire
  5. UPDATE documents SET encrypted_metadata = NULL WHERE id = :id
  6. Log de zeroization avec taille zeroized

  7. Fail-closed (MAJ-27) :

  8. Si zeroization échoue → le document n'est PAS supprimé de S3
  9. Audit d'anomalie spécifique
  10. Le document reste dans son état courant (pas de transition DESTROYED)

  11. Détection flux legal_lock :

  12. Colonne had_legal_lock (boolean, migration TASK-13)
  13. true si legal_lock a été true à un moment quelconque
  14. Backfill migration : UPDATE documents SET had_legal_lock = true WHERE legal_lock = true OR legal_lock_until IS NOT NULL

Observables : - Trace d'orchestration : zeroization exécutée / non exécutée par document - Taille des données zeroized - Fail-closed observable : document non DESTROYED si zeroization échoue

Invariants couverts : INV-250-12

Tests : TC-250-17 + TC-250-17b (fail-closed zeroization — MAJ-27)


TASK-10 — Contrôleur admin bordereaux

Objectif : Exposer un endpoint GET /admin/bordereaux avec filtres date/batch et contrôle d'accès ADMIN.

Fichiers : - src/modules/destruction/controllers/bordereau.controller.ts - src/modules/destruction/dto/bordereau-query.dto.ts - src/modules/destruction/dto/bordereau-response.dto.ts - src/modules/destruction/__tests__/bordereau.controller.spec.ts

Mécanismes :

  1. Endpoint : GET /admin/bordereaux
  2. Guard : @UseGuards(AuthorizationGuard) + @Roles('ADMIN')
  3. Query params : dateFrom?, dateTo?, batchId?
  4. Pagination standard (offset/limit)

  5. Contrôle d'accès (INV-250-15) :

  6. Requête sans rôle ADMIN → HTTP 403 + audit DOCUMENT_DESTROY_ACCESS_DENIED
  7. Aucun contenu bordereau dans la réponse 403

  8. DTO réponse :

  9. bordereauId, batchId, createdAt, documentCount, status, pdfUrl (presigned S3)

Observables : - Réponse filtrée conforme aux paramètres - HTTP 403 pour non-ADMIN - Trace d'audit pour accès refusé

Invariants couverts : INV-250-15

Tests : TC-250-20, TC-250-21


TASK-11 — Processor de préavis

Objectif : Implémenter le job BullMQ de préavis quotidien (§5.8).

Fichiers : - src/modules/destruction/processors/pre-notice.processor.ts - src/modules/destruction/events/pre-notice.events.ts - src/modules/destruction/__tests__/pre-notice.processor.spec.ts

Mécanismes :

  1. Job BullMQ sur queue pv-jobs-prenotice (sans :, INV-250-13) :
  2. Scheduling : quotidien (configurable via destructionJobInterval)
  3. Exécuté AVANT le job de destruction (ordonnancement cron)

  4. Logique :

  5. Appel eligibility.selectPreNotice(preNoticeDays)
  6. Pour chaque document : émettre EventEmitter2.emit('destruction.pre-notice', payload)
  7. Payload : { documentId, eligibleAt, preNoticeDays } — pas de PII

  8. Cas N=0 :

  9. Préavis émis le jour J, avant le run de destruction
  10. Valeur informative « jour J », pas de délai effectif

Observables : - Événements de préavis horodatés sur le bus - Corrélation temporelle document/éligibilité

Invariants couverts : (contribution à CA-250-11)

Tests : TC-250-22


TASK-12 — Processor principal de destruction

Objectif : Implémenter le processor BullMQ qui orchestre le flux complet de destruction.

Fichiers : - src/modules/destruction/processors/destruction.processor.ts - src/modules/destruction/__tests__/destruction.processor.spec.ts

Mécanismes :

  1. Job BullMQ sur queue pv-jobs-destruction (sans :, INV-250-13) :
  2. Scheduling : configurable via destructionJobInterval
  3. getJobSchedulers/removeJobScheduler uniquement (INV-250-14)

  4. Orchestration (flux §5.1 → §5.10) :

    async process(job: Job): Promise<void> {
      // 1. Valider config (TASK-01)
      // 2. Sélectionner documents éligibles (TASK-02)
      // 3. Si 0 → fin
      // 4. Créer batch (PENDING → RUNNING), parentBatchId si reprise (MIN-11)
      // 5. Générer + signer + horodater bordereau (TASK-03)
      //    → Fail-closed si échec (INV-250-03)
      // 6. Démarrer chrono destructionExecutionSla
      //    → Si dépassé avant 1er doc → FAILED (MAJ-26)
      // 7. Exécuter destruction séquentielle (TASK-04)
      //    → Await par document (MAJ-25)
      //    → Si SLA dépassé en cours → PARTIAL_FAILED (MAJ-26)
      // 8. Vérifier batchFinalizeSla (TASK-07)
      // 9. Calculer état final + publier résultat (TASK-07)
    }
    

  5. parentBatchId (MIN-11) :

  6. Si le batch est une reprise (documents non-DESTROYED d'un batch précédent) : parentBatchId obligatoire
  7. Sinon : parentBatchId = null

Observables : - Statut final du batch (SUCCESS / PARTIAL_FAILED / FAILED) - Métriques publiées sur le bus - Traces de corrélation batch/bordereau

Invariants couverts : INV-250-03, INV-250-13, INV-250-14

Tests : TC-250-08, TC-250-12, TC-250-19, TC-250-26


TASK-13 — Migrations de base de données

Objectif : Créer les migrations TypeORM pour les nouvelles entités et extensions de schéma.

Fichiers : - src/database/migrations/XXXXXX-PD-250-destruction-schema.ts - src/database/migrations/XXXXXX-PD-250-document-status-extension.ts

Mécanismes :

  1. Extension de l'enum document_status :

    ALTER TYPE vault_secure.document_status ADD VALUE IF NOT EXISTS 'DESTROYED';
    ALTER TYPE vault_secure.document_status ADD VALUE IF NOT EXISTS 'RECONCILIATION_FAILED';
    

  2. Colonne had_legal_lock :

    ALTER TABLE vault_secure.documents ADD COLUMN had_legal_lock BOOLEAN NOT NULL DEFAULT false;
    -- Backfill
    UPDATE vault_secure.documents SET had_legal_lock = true
      WHERE legal_lock = true OR legal_lock_until IS NOT NULL;
    

  3. Table destruction_batches :

    CREATE TABLE vault_secure.destruction_batches (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
      parent_batch_id UUID REFERENCES vault_secure.destruction_batches(id),
      bordereau_id UUID,
      document_count INT NOT NULL DEFAULT 0,
      destroyed_count INT NOT NULL DEFAULT 0,
      failed_count INT NOT NULL DEFAULT 0,
      skipped_count INT NOT NULL DEFAULT 0,
      error_message TEXT,
      started_at TIMESTAMPTZ,
      completed_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );
    

  4. Table destruction_bordereaux :

    CREATE TABLE vault_secure.destruction_bordereaux (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      batch_id UUID NOT NULL REFERENCES vault_secure.destruction_batches(id),
      pdf_s3_path TEXT NOT NULL,
      pdf_hash BYTEA NOT NULL,
      tsa_token BYTEA,
      signature BYTEA NOT NULL,
      document_count INT NOT NULL,
      retention_expiry TIMESTAMPTZ,  -- NULL = indéfini (INV-250-06)
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );
    

  5. Séquence audit_destruction_seq :

    CREATE SEQUENCE vault_secure.audit_destruction_seq;
    

  6. Index :

    CREATE INDEX idx_destruction_batches_status ON vault_secure.destruction_batches(status);
    CREATE INDEX idx_destruction_batches_created_at ON vault_secure.destruction_batches(created_at);
    CREATE INDEX idx_documents_had_legal_lock ON vault_secure.documents(had_legal_lock) WHERE had_legal_lock = true;
    

Observables : - Migration réversible (down) - Backfill had_legal_lock idempotent

Invariants couverts : INV-250-01, INV-250-05, INV-250-06

Tests : Validation npm run typeorm migration:run en local avant push (learning PD-55)


TASK-14 — Tests contractuels

Objectif : Implémenter les tests contractuels transversaux (queue naming, API dépréciées, transitions, coverage).

Fichiers : - src/modules/destruction/__tests__/contractual/queue-naming.spec.ts - src/modules/destruction/__tests__/contractual/deprecated-api.spec.ts - src/modules/destruction/__tests__/contractual/state-transitions.spec.ts

Mécanismes :

  1. TC-250-18 — Queue naming :
  2. Assert : DESTRUCTION_QUEUE_NAME ne contient pas :
  3. Assert : PRE_NOTICE_QUEUE_NAME ne contient pas :
  4. Regex : /:/ → test doit passer (absence)

  5. TC-250-19 — API dépréciées :

  6. Scan statique : grep -r 'getRepeatableJobs\|removeRepeatableByKey' src/modules/destruction/
  7. Assert : 0 occurrences
  8. Vérifier présence de getJobSchedulers/removeJobScheduler

  9. TC-250-15 étendu — Transitions interdites (5 transitions, incluant MAJ-28) :

  10. DESTROYED → EXPIRED : reject
  11. EXPIRED → SEALED : reject
  12. SEALED → PENDING : reject
  13. DESTROYED → SEALED : reject
  14. RECONCILIATION_FAILED → DESTROYED : reject (MAJ-28)
  15. Chaque rejet vérifie : code d'erreur explicite + état inchangé + audit

  16. TC-250-17b — Fail-closed zeroization (MAJ-27) :

  17. Mock zeroization.zeroize → throw Error
  18. Assert : document non supprimé de S3 + état inchangé + audit anomalie

  19. parentBatchId (MIN-11) :

  20. Test batch de reprise : parentBatchId non null
  21. Test batch initial : parentBatchId null

  22. Stratégie d'échantillonnage (MIN-15) :

  23. TC-250-23 teste bornes min ET max pour 3 paramètres :
    • destructionBatchSize : 0 (< min) et 5001 (> max)
    • tsaRetryCount : -1 (< min) et 6 (> max)
    • clockSkewTolerance : 0 (< min) et 61 (> max)

Observables : - Chaque test contractuel est déterministe (pas de dépendance réseau) - Exécutable en CI sans infrastructure

Invariants couverts : INV-250-11, INV-250-13, INV-250-14, INV-250-15

Tests : TC-250-15, TC-250-17b, TC-250-18, TC-250-19, TC-250-23

6. Plan de test

6.1 Stratégie de test

Type Fichier(s) Infrastructure requise
Unitaire *.spec.ts dans __tests__/ Aucune (mocks)
Contractuel contractual/*.spec.ts Aucune (analyse statique + assertions)
Intégration *.integration.spec.ts PostgreSQL (TypeORM), Redis (ioredis-mock)

6.2 Mapping TC → fichiers de test

TC Fichier Type
TC-250-01 eligibility.service.spec.ts Unitaire
TC-250-02 eligibility.service.spec.ts Unitaire
TC-250-03 eligibility.service.spec.ts Unitaire
TC-250-04 bordereau.service.spec.ts Unitaire
TC-250-05 bordereau.service.spec.ts Unitaire
TC-250-06 destruction-execution.service.spec.ts Unitaire
TC-250-07 reconciliation.service.spec.ts Unitaire
TC-250-08 bordereau.service.spec.ts Unitaire
TC-250-08b bordereau.service.spec.ts Unitaire
TC-250-09 destruction-audit.service.spec.ts Unitaire
TC-250-10 eligibility.service.spec.ts + bordereau.controller.spec.ts Unitaire
TC-250-11 bordereau.service.spec.ts Unitaire
TC-250-12 destruction.processor.spec.ts Unitaire
TC-250-13 reconciliation.service.spec.ts Unitaire
TC-250-14 destruction-alert.service.spec.ts Unitaire
TC-250-15 contractual/state-transitions.spec.ts Contractuel
TC-250-16 destruction-audit.service.spec.ts Unitaire
TC-250-17 document-zeroization.service.spec.ts Unitaire
TC-250-17b document-zeroization.service.spec.ts Unitaire (MAJ-27)
TC-250-18 contractual/queue-naming.spec.ts Contractuel
TC-250-19 contractual/deprecated-api.spec.ts Contractuel
TC-250-20 bordereau.controller.spec.ts Unitaire
TC-250-21 bordereau.controller.spec.ts Unitaire
TC-250-22 pre-notice.processor.spec.ts Unitaire
TC-250-23 destruction.config.spec.ts Unitaire
TC-250-25 destruction-execution.service.spec.ts Unitaire
TC-250-26 destruction.processor.spec.ts Unitaire
TC-250-27 destruction-execution.service.spec.ts Unitaire
TC-250-28 reconciliation.service.spec.ts Unitaire

6.3 Mocks requis

Dépendance Mock Pattern
S3 (OVH) aws-sdk-client-mock DeleteObjectCommand mockée
TSA (PD-39) Jest mock Service injectable mocké
HSM signature (PD-36) Jest mock Service injectable mocké
AuditLogService (PD-37) Jest mock Service injectable mocké
LegalDestructionService (PD-81) Jest mock Service injectable mocké
EventEmitter2 Jest spy jest.spyOn(emitter, 'emit')
TypeORM QueryRunner Jest mock Transaction simulée
ConfigService Jest mock Valeurs contractuelles

7. Gestion d'erreurs

ERR Mécanisme technique Fichier Observable
ERR-250-01 Boucle retry dans bordereau.service.requestTsaTimestamp(), compteur ≤ tsaRetryCount, delay exponentiel bordereau.service.ts Logs retry + statut batch FAILED + compteur = tsaRetryCount
ERR-250-02 Vérification post-signature dans bordereau.service.verifySignature() → throw si invalide bordereau.service.ts Exception typée + batch FAILED + 0 destruction
ERR-250-03 eligibility.isStillEligible() appelé pré-destruction dans la boucle séquentielle destruction-execution.service.ts Audit anomalie + doc skip + batch PARTIAL_FAILED
ERR-250-04 Retry S3 DeleteObject ≤ s3DeleteRetryCount dans destruction-execution.service destruction-execution.service.ts Audit unitaire + doc skip + batch PARTIAL_FAILED
ERR-250-05 reconciliation.service.reconcile() avec retry DB + SLA check reconciliation.service.ts DESTROYED ou RECONCILIATION_FAILED + escalade
ERR-250-06 Try/catch sur persistance bordereau (S3 + DB) dans bordereau.service.persist() bordereau.service.ts Batch FAILED + 0 destruction + alerte
ERR-250-07 Guard if (!auditAvailable) throw dans destruction-audit.service.ensureAvailable() destruction-audit.service.ts Arrêt fail-closed + alerte immédiate
ERR-250-08 Joi validation throw au boot dans destruction.config.validate() destruction.config.ts Erreur de validation + alerte + 0 traitement

8. Sécurité

8.1 Contrôles d'accès

Ressource Contrôle Mécanisme
Endpoint GET /admin/bordereaux @Roles('ADMIN') + AuthorizationGuard HTTP 403 + audit DOCUMENT_DESTROY_ACCESS_DENIED
Événements batch_result Tag ADMIN/SYSTEM sur listener EventEmitter2 avec filtre de rôle
Job BullMQ destruction Exécution système (pas d'endpoint utilisateur) Scheduling interne uniquement

8.2 Comportements fail-closed

Scénario Comportement
Signature invalide Batch FAILED, 0 destruction
TSA indisponible (3 retries) Batch FAILED, 0 destruction
Bordereau non persisté Batch FAILED, 0 destruction
Zeroization échouée (flux legal_lock) Document non supprimé de S3, skip
Chaîne d'audit indisponible Arrêt job + alerte immédiate

8.3 Protection des données

Données Protection
Contenu bordereau Champs autorisés §3 uniquement, pas de PII
Métriques batch Compteurs agrégés, pas d'identifiants individuels
Préavis documentId + dates techniques, pas de PII

9. Configuration

9.1 Variables d'environnement

Variable Défaut Min Max Unité
DESTRUCTION_BATCH_SIZE 500 1 5000 docs
DESTRUCTION_JOB_INTERVAL 24 1 168 heures
DESTRUCTION_PRE_NOTICE_DAYS 30 0 365 jours
DESTRUCTION_TSA_RETRY_COUNT 3 0 5 tentatives
DESTRUCTION_TSA_TIMEOUT 5000 1000 30000 ms
DESTRUCTION_SIGNATURE_TIMEOUT 10000 1000 60000 ms
DESTRUCTION_S3_DELETE_RETRY_COUNT 5 0 10 tentatives
DESTRUCTION_S3_DELETE_TIMEOUT 10000 1000 60000 ms
DESTRUCTION_CLOCK_SKEW_TOLERANCE 5 1 60 secondes
DESTRUCTION_RECONCILIATION_DB_RETRY_COUNT 3 1 10 tentatives
DESTRUCTION_BATCH_FINALIZE_SLA 7200000 300000 86400000 ms
DESTRUCTION_EXECUTION_SLA 1800000 60000 86400000 ms
DESTRUCTION_RECONCILIATION_SLA 86400000 3600000 604800000 ms

9.2 Validation Joi

const destructionConfigSchema = Joi.object({
  DESTRUCTION_BATCH_SIZE: Joi.number().integer().min(1).max(5000)
    .default(trackedDefault('DESTRUCTION_BATCH_SIZE', 500)),
  DESTRUCTION_JOB_INTERVAL: Joi.number().integer().min(1).max(168)
    .default(trackedDefault('DESTRUCTION_JOB_INTERVAL', 24)),
  // ... (13 paramètres)
}).options({ abortEarly: false, allowUnknown: false, stripUnknown: false });

Rejet strict (pas de clamp) : si valeur hors bornes → throw ValidationError + alerte.

10. Vigilance (risques et mitigations)

# Risque Probabilité Impact Mitigation
1 Migration ALTER TYPE bloque en prod (lock exclusif sur table documents) Moyenne Élevé Migration hors heures, test en staging avec volume réaliste
2 pdf-lib incompatible CJS/Jest Faible Moyen HYP-IMPL-05 : fallback pdfkit
3 Volume de backfill had_legal_lock lent sur grosse table Faible Moyen Backfill par batch (1000 à la fois) dans la migration
4 S3 eventual consistency : objet encore lisible après DELETE 204 Certaine Faible Risque accepté (§10.6), zeroization couvre flux legal_lock
5 TSA externe indisponible en production Faible Élevé Fail-closed + retry + alerte, pas de workaround
6 Race condition entre sélection et destruction (ERR-250-03) Faible Moyen Re-vérification avec SELECT FOR UPDATE avant chaque destruction
7 Séquence audit_seq wraps (overflow bigint) Négligeable Faible PostgreSQL bigint : 9.2×10^18 valeurs, pas de risque réaliste
8 Queue naming : vs - incohérence avec queues existantes Aucun Aucun Convention PD-250 uniquement, queues existantes non modifiées

11. Ordonnancement des tâches

Phase 1 — Fondations (parallélisable)

  • TASK-01 : Configuration et validation Joi
  • TASK-13 : Migrations de base de données
  • TASK-08 : Machine à états des documents

Phase 2 — Services core (séquentiel)

  • TASK-02 : Service d'éligibilité (dépend de TASK-13)
  • TASK-06 : Service d'audit (dépend de TASK-13)
  • TASK-09 : Service de zeroization (dépend de TASK-13)

Phase 3 — Orchestration (séquentiel)

  • TASK-03 : Service de bordereau (dépend de TASK-02)
  • TASK-04 : Service d'exécution (dépend de TASK-02, TASK-06, TASK-09)

Phase 4 — Résilience (séquentiel)

  • TASK-05 : Service de réconciliation (dépend de TASK-04, TASK-06)
  • TASK-07 : Service d'alertes (dépend de TASK-04)

Phase 5 — Exposition (parallélisable)

  • TASK-10 : Contrôleur admin (dépend de TASK-03)
  • TASK-11 : Processor préavis (dépend de TASK-02)
  • TASK-12 : Processor destruction (dépend de TASK-03, TASK-04, TASK-07)

Phase 6 — Validation

  • TASK-14 : Tests contractuels (dépend de tous)

12. Contraintes techniques

Dépendances inter-PD

Story Statut Nature de la dépendance
PD-21 DONE BullMQ infrastructure, BaseProcessor, queue naming
PD-63 DONE DocumentSecure entity, DocumentStatus enum, legal_lock fields
PD-81 DONE LegalDestructionService (pattern zeroization)
PD-39 DONE TSA module RFC 3161 (signature + horodatage)
PD-37 DONE AuditLogService, AuditActionType enum
PD-36 DONE HSM PKCS#11 client (signature qualifiée)
PD-55 DONE BullMQ v5 API (getJobSchedulers)
PD-240 DONE Atomicité suppression/purge RGPD
PD-264 DONE Transaction DB synchrone vs BullMQ asynchrone

Framework de test

  • Runner : Jest avec ts-jest
  • Tests d'intégration : avec mocks (aws-sdk-client-mock, ioredis-mock, services mockés)
  • Environnement CI : DATABASE_URL, CI=true, REDIS_URL

Compatibilité ESM/CJS

  • Dépendance nouvelle pdf-lib : compatible CJS (vérifié)
  • Aucune autre dépendance ESM-only identifiée
  • Jest avec ts-jest (convention existante)