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 :
- 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 clockSkewToleranceappliqué comme marge soustractive (§5.1)-
Filtre
entity_type != 'BORDEREAU'pour MIN-13 (exclusion des bordereaux de la sélection) -
Méthode
selectPreNotice(preNoticeDays: number): Promise<DocumentSecure[]>: - Sélectionne les documents dont la date d'éligibilité est dans J+N jours
- Même conditions de base mais sur une fenêtre future
-
N=0 : jour même de l'éligibilité
-
Méthode
isStillEligible(documentId: string): Promise<boolean>: - Re-vérification unitaire avant destruction (ERR-250-03)
- Utilise
FOR UPDATEpour é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 :
- Méthode
generate(batchId, documents): Promise<BordereauResult>: - Assembler le payload avec uniquement les champs autorisés (§3) :
batchId, horodatage run,documentIdtechnique, type documentaire, hash probatoire, date scellement, date expiration, date destruction prévue - PAS de données personnelles directes (INV-250-07) : nom, prénom, email, adresse, téléphone, NIR exclus
- Générer PDF/A avec
pdf-lib(PDFDocument.create(), setProducer, setCreationDate, embedFont) -
Format tabulaire dans le PDF : une ligne par document
-
Signature électronique :
- Appel au service HSM (PD-36) pour signature qualifiée eIDAS art. 26
- Timeout contrôlé par
signatureTimeout -
Vérification post-signature obligatoire (fail-closed)
-
Horodatage RFC 3161 :
- Appel au module TSA (PD-39)
- Retry contrôlé :
tsaRetryCounttentatives max - Timeout par tentative :
tsaTimeout -
Échec persistant → ERR-250-01 → batch FAILED
-
Persistance :
- Sauvegarder le bordereau comme document probatoire dans S3 (avec Object Lock COMPLIANCE, rétention maximale)
- Créer entity
destruction_bordereauen DB avecretentionExpiry = null(INV-250-06) - Statut
SEALEDpermanent (jamais éligible à la destruction automatique) - É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 :
- Méthode
executeSequential(documents, batchId, bordereauId): Promise<ExecutionResult>: - Boucle
for...ofsur les documents (séquentiel, pas dePromise.all— résolution MAJ-25) -
Compteurs :
destroyed,skipped,failed -
Pour chaque document (pattern
awaitséquentiel) : - Re-vérification éligibilité :
eligibility.isStillEligible(doc.id)— si non éligible → ERR-250-03, skip avec audit anomalie - Branchement flux legal_lock (INV-250-12) : si
doc.hadLegalLock === true→ appelzeroization.zeroize(doc)— échec → fail-closed, skip doc (MAJ-27) - S3 DeleteObject :
await s3Client.send(new DeleteObjectCommand({...}))— retry ≤s3DeleteRetryCount— timeouts3DeleteTimeout— confirmation HTTP 204 — échec → ERR-250-04, skip -
Transaction DB atomique (§10.4b) :
— Échec transaction → ERR-250-05 → déclencher réconciliation -
Gestion SLA
destructionExecutionSla(résolution MAJ-26) : - Chrono démarré après validation bordereau
- Si dépassé AVANT traitement du 1er document : batch
FAILED(car 0 destruction) - 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 :
- Méthode
reconcile(documentId, batchId, bordereauId): Promise<ReconciliationResult>: - Retry DB :
reconciliationDbRetryCounttentatives avec backoff exponentiel - Si réussi → état terminal =
DESTROYED+ auditDESTROYED -
Si échec persistant → vérifier
reconciliationSla -
Vérification SLA :
- Chrono démarré au moment du premier échec DB
- Si dans SLA → retry
-
Si hors SLA → état terminal =
RECONCILIATION_FAILED -
État
RECONCILIATION_FAILED: - Audit de type
RECONCILIATION_FAILED(pasDESTROYED) — complétude INV-250-05 - Escalade critique obligatoire via
destruction-alert.service 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 :
- Séquence PostgreSQL
audit_seq: - Créée dans la migration (TASK-13) :
CREATE SEQUENCE vault_secure.audit_destruction_seq - Utilisée comme clé d'ordonnancement monotone (pas l'horloge système)
-
Chaque audit log reçoit
nextval('vault_secure.audit_destruction_seq') -
Méthode
logDestruction(queryRunner, params): Promise<void>: - Exécutée dans la même transaction que la finalisation DESTROYED (§10.4b)
- Paramètres :
documentId,batchId,bordereauId,eventType(DESTROYED ou RECONCILIATION_FAILED) -
Appel à
AuditLogServiceexistant (PD-37) avec metadata étendue -
Actions d'audit spécifiques (extension de
AuditActionType) : DOCUMENT_DESTROY: destruction unitaire réussieDOCUMENT_DESTROY_BATCH: événement batch-levelDOCUMENT_DESTROY_BORDEREAU: création bordereauDOCUMENT_DESTROY_RECONCILIATION_FAILED: échec réconciliationDOCUMENT_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 :
- Méthode
publishBatchResult(metrics): void(§5.10) : - Émet sur EventEmitter2 :
destruction.batch_result - Payload : compteurs agrégés (documents traités/détruits/en erreur, durée), pas d'identifiants individuels
-
Restriction d'accès (MIN-12) : l'événement est tagué
ADMIN|SYSTEM— les listeners doivent vérifier leur rôle -
Alertes SLA (INV-250-16) :
batchFinalizeSladépassé → alerte critique + batchFAILEDdestructionExecutionSladépassé → alerte + batchPARTIAL_FAILEDouFAILEDselon MAJ-26-
reconciliationSladépassé → escalade critique +RECONCILIATION_FAILED -
Alerte fail-closed (ERR-250-07) :
- Si chaîne d'audit indisponible → arrêt du job + alerte immédiate
- É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 :
- Transitions autorisées (§10.5) :
PENDING → SEALED(existant)SEALED → EXPIRED(existant)EXPIRED → DESTROYED(PD-250)-
EXPIRED → RECONCILIATION_FAILED(PD-250, ERR-250-05) -
Transitions INTERDITES (INV-250-11 + MAJ-28) :
SEALED → PENDING: INTERDITEEXPIRED → SEALED: INTERDITEDESTROYED → EXPIRED: INTERDITEDESTROYED → SEALED: INTERDITE-
RECONCILIATION_FAILED → *: INTERDITE (toute transition depuis cet état) -
Méthode
validateTransition(currentStatus, newStatus): void: - Vérifie dans la matrice des transitions autorisées
-
Si transition interdite → throw avec code d'erreur explicite + audit
-
Méthode
applyTransition(queryRunner, documentId, newStatus): Promise<void>: - Appelle
validateTransitionpuis 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 :
- Méthode
zeroize(documentId: string, queryRunner: QueryRunner): Promise<ZeroizationResult>: - Pattern PD-81 adapté : transaction SERIALIZABLE
- Charger
encryptedMetadata(Buffer) Buffer.fill(0)pour écraser en mémoireUPDATE documents SET encrypted_metadata = NULL WHERE id = :id-
Log de zeroization avec taille zeroized
-
Fail-closed (MAJ-27) :
- Si zeroization échoue → le document n'est PAS supprimé de S3
- Audit d'anomalie spécifique
-
Le document reste dans son état courant (pas de transition DESTROYED)
-
Détection flux
legal_lock: - Colonne
had_legal_lock(boolean, migration TASK-13) truesilegal_locka ététrueà un moment quelconque- 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 :
- Endpoint :
GET /admin/bordereaux - Guard :
@UseGuards(AuthorizationGuard)+@Roles('ADMIN') - Query params :
dateFrom?,dateTo?,batchId? -
Pagination standard (offset/limit)
-
Contrôle d'accès (INV-250-15) :
- Requête sans rôle ADMIN → HTTP 403 + audit
DOCUMENT_DESTROY_ACCESS_DENIED -
Aucun contenu bordereau dans la réponse 403
-
DTO réponse :
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 :
- Job BullMQ sur queue
pv-jobs-prenotice(sans:, INV-250-13) : - Scheduling : quotidien (configurable via
destructionJobInterval) -
Exécuté AVANT le job de destruction (ordonnancement cron)
-
Logique :
- Appel
eligibility.selectPreNotice(preNoticeDays) - Pour chaque document : émettre
EventEmitter2.emit('destruction.pre-notice', payload) -
Payload :
{ documentId, eligibleAt, preNoticeDays }— pas de PII -
Cas N=0 :
- Préavis émis le jour J, avant le run de destruction
- 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 :
- Job BullMQ sur queue
pv-jobs-destruction(sans:, INV-250-13) : - Scheduling : configurable via
destructionJobInterval -
getJobSchedulers/removeJobScheduleruniquement (INV-250-14) -
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) } -
parentBatchId (MIN-11) :
- Si le batch est une reprise (documents non-DESTROYED d'un batch précédent) :
parentBatchIdobligatoire - 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 :
-
Extension de l'enum
document_status: -
Colonne
had_legal_lock: -
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() ); -
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() ); -
Séquence
audit_destruction_seq: -
Index :
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 :
- TC-250-18 — Queue naming :
- Assert :
DESTRUCTION_QUEUE_NAMEne contient pas: - Assert :
PRE_NOTICE_QUEUE_NAMEne contient pas: -
Regex :
/:/→ test doit passer (absence) -
TC-250-19 — API dépréciées :
- Scan statique :
grep -r 'getRepeatableJobs\|removeRepeatableByKey' src/modules/destruction/ - Assert : 0 occurrences
-
Vérifier présence de
getJobSchedulers/removeJobScheduler -
TC-250-15 étendu — Transitions interdites (5 transitions, incluant MAJ-28) :
DESTROYED → EXPIRED: rejectEXPIRED → SEALED: rejectSEALED → PENDING: rejectDESTROYED → SEALED: rejectRECONCILIATION_FAILED → DESTROYED: reject (MAJ-28)-
Chaque rejet vérifie : code d'erreur explicite + état inchangé + audit
-
TC-250-17b — Fail-closed zeroization (MAJ-27) :
- Mock zeroization.zeroize → throw Error
-
Assert : document non supprimé de S3 + état inchangé + audit anomalie
-
parentBatchId (MIN-11) :
- Test batch de reprise :
parentBatchIdnon null -
Test batch initial :
parentBatchIdnull -
Stratégie d'échantillonnage (MIN-15) :
- 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)