Aller au contenu

PD-279 — Plan d'implémentation

1. Découpage en composants

# Composant Responsabilité Fichiers Agent
C1 Migration DDL Extension enum document_status + colonnes restituted_at, restitution_deadline + contrainte CHECK + down migration avec précondition src/database/migrations/1741300000000-PD279-AddRestitutedStatus.ts agent-developer
C2 DocumentStatus enum extension Ajout de RESTITUTED au cycle de vie TypeScript src/modules/documents/entities/document-secure.entity.ts agent-developer
C3 State machine extension Ajout des transitions SEALED→RESTITUTED et RESTITUTED→SEALED dans la matrice + export RESTITUTED const src/modules/destruction/services/document-state-machine.service.ts agent-developer
C4 RestitutionService Logique métier de restitution et retour : gardes, atomicité, attestation lifecycle_log, calcul SLA src/modules/documents/services/restitution.service.ts agent-developer
C5 RestitutionController Endpoints REST POST /documents/:id/restitute et POST /documents/:id/return-from-restitution src/modules/documents/controllers/restitution.controller.ts agent-developer
C6 RestitutionConfig Configuration SLA (durée max, seuil alerte, seuil escalade) avec Joi validation src/modules/documents/config/restitution.config.ts agent-developer
C7 RestitutionSlaScheduler Job planifié BullMQ pour alerte 80%, escalade 100%, événement RESTITUTION_OVERDUE src/modules/documents/services/restitution-sla.scheduler.ts agent-developer
C8 Destruction guard extension Ajout vérification RESTITUTED dans l'eligibility service et l'exécution de destruction src/modules/destruction/services/eligibility.service.ts, src/modules/destruction/services/destruction-execution.service.ts agent-developer
C9 Audit integration Ajout AuditActionType pour restitution/retour/refus + émission via AuditLogService src/modules/audit/types/audit-action.types.ts agent-developer
C10 JournalEventType extension Ajout types RESTITUTION, RETURN_FROM_RESTITUTION, RESTITUTION_OVERDUE dans le journal d'intégrité src/modules/integrity/enums/journal-event-type.enum.ts agent-developer
C11 Error codes DTOs d'erreur + constantes error_code pour les 409/423 de restitution src/modules/documents/dto/restitution-error.dto.ts agent-developer
C12 Tests unitaires + intégration Suite de tests couvrant TC-NOM-01 à TC-NOM-07, TC-ERR-01 à TC-ERR-10, TC-INV-09A/B, TC-NR-01 à TC-NR-04, TC-NEG-01 à TC-NEG-05 src/modules/documents/__tests__/, src/modules/destruction/__tests__/ agent-qa-unit-integration
C13 TLA+ model update Extension du modèle formel AnchorAssume_States avec l'état RESTITUTED ProbatioVault-doc/docs/normes/iso-14641/formal/ISO_14641.tla agent-developer

2. Flux techniques

2.1 Flux A — Restitution (POST /documents/:id/restitute)

Client → RestitutionController.restitute(id)
  ├─ 1. ParseUUIDPipe (validation format) → 400 si invalide
  └─ RestitutionService.restitute(documentId, actorId)
       ├─ 2. Lecture document via DataSource (SELECT ... FOR UPDATE)
       │     → 404 si inexistant
       ├─ 3. Vérification ownership (document.userId === actorId)
       │     → 403 si non owner
       ├─ 4. Vérification état courant
       │     ├─ Si déjà RESTITUTED → 200 idempotent (retour body actuel, pas d'attestation)
       │     └─ Si != SEALED → 409 INVALID_SOURCE_STATE
       ├─ 5. Vérification legal_lock
       │     → 423 DOCUMENT_UNDER_LEGAL_LOCK si true
       ├─ 6. Vérification geo_copy_count >= 2
       │     → 409 INSUFFICIENT_GEO_COPIES si < 2
       ├─ 7. Calcul SLA
       │     restituted_at = NOW() UTC
       │     restitution_deadline = restituted_at + restitution_max_duration
       ├─ 8. Transaction ACID (QueryRunner)
       │     ├─ UPDATE documents SET status=RESTITUTED, restituted_at, restitution_deadline
       │     └─ INSERT lifecycle_log (type=RESTITUTION, actor_id, security_level, timestamp_utc)
       ├─ 9. Post-commit : AuditLogService.logAsync(document.restitute)
       └─ 10. HTTP 200 + body document mis à jour

2.2 Flux B — Retour de restitution (POST /documents/:id/return-from-restitution)

Client → RestitutionController.returnFromRestitution(id)
  ├─ 1. ParseUUIDPipe → 400 si invalide
  └─ RestitutionService.returnFromRestitution(documentId, actorId)
       ├─ 2. Lecture document (SELECT ... FOR UPDATE)
       │     → 404 si inexistant
       ├─ 3. Vérification ownership → 403
       ├─ 4. Vérification état courant
       │     ├─ Si déjà SEALED → 200 idempotent
       │     └─ Si != RESTITUTED → 409 INVALID_SOURCE_STATE
       ├─ 5. Transaction ACID
       │     ├─ UPDATE documents SET status=SEALED
       │     └─ INSERT lifecycle_log (type=RETURN_FROM_RESTITUTION, actor_id, security_level, timestamp_utc)
       ├─ 6. Post-commit : AuditLogService.logAsync(document.return_from_restitution)
       └─ 7. HTTP 200 + body document

2.3 Flux C — Contrôle destruction cross-module

Module Destruction (routes existantes)
  ├─ EligibilityService.reVerifyEligibility()
  │     Ajouter : if status === 'RESTITUTED' → return { eligible: false, reason: RESTITUTED }
  ├─ DestructionExecutionService (boucle document)
  │     Ajouter : vérification status RESTITUTED avant destroy unitaire
  └─ DocumentsController.delete()
       Ajouter : vérification status RESTITUTED → 409 DOCUMENT_RESTITUTED_DESTRUCTION_FORBIDDEN

2.4 Flux D — SLA Scheduler (async)

RestitutionSlaScheduler (BullMQ cron job)
  ├─ 1. SELECT documents WHERE status=RESTITUTED AND restitution_deadline IS NOT NULL
  ├─ 2. Pour chaque document :
  │     ├─ elapsed_ratio = (NOW - restituted_at) / (restitution_deadline - restituted_at)
  │     │
  │     ├─ Si elapsed_ratio >= 0.80 et alerte non émise
  │     │     → IntegrityJournalService.appendEntry(OPS_ALERT_SENT, {type: 'RESTITUTION_SLA_80'})
  │     │     → AuditLogService.logAsync(document.restitution_sla_alert)
  │     │
  │     ├─ Si elapsed_ratio >= 1.00 et escalade non émise
  │     │     → IntegrityJournalService.appendEntry(SLA_EXCEEDED, {type: 'RESTITUTION_SLA_100'})
  │     │     → AuditLogService.logAsync(document.restitution_sla_escalation)
  │     │
  │     └─ Si NOW > restitution_deadline et RESTITUTION_OVERDUE non émis
  │           → IntegrityJournalService.appendEntry(SLA_EXCEEDED, {type: 'RESTITUTION_OVERDUE'})
  │           → AuditLogService.logAsync(document.restitution_overdue)
  └─ 3. Idempotence : payload_digest inclut document_id + seuil → pas de doublon

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-279-01-state-model RESTITUTED dans DocumentStatus persistant Extension enum TypeScript + migration ALTER TYPE PostgreSQL C1, C2 SELECT enum_range(NULL::vault_secure.document_status) contient RESTITUTED Migration ALTER TYPE ADD VALUE ne peut pas être rollbackée dans une transaction PG — exécuter hors transaction
INV-279-02-restitute-guards SEALED→RESTITUTED si owner, legal_lock=false, geo_copy_count>=2, état SEALED Gardes séquentielles dans RestitutionService.restitute() avec ordre normatif §6.1 C4 Codes HTTP retournés selon la première garde violée (403, 409, 423) Race condition si deux appels concurrents — mitigé par SELECT FOR UPDATE
INV-279-03-return-guards RESTITUTED→SEALED si owner et état RESTITUTED ; pas de check geo_copy_count Gardes dans RestitutionService.returnFromRestitution() C4 HTTP 200 sans vérification geo_copy_count Aucun risque spécifique
INV-279-04-traceability Attestation lifecycle_log complète pour chaque transition INSERT INTO vault_integrity.integrity_journal_entries dans la même transaction via IntegrityJournalService.appendEntry() C4, C10 Entrée avec event_type, actor_id, security_level, timestamp Si HSM indisponible pour signature journal → fallback signature différée existant (PD-251)
INV-279-05-sla restituted_at + restitution_deadline ; alerte 80%, escalade 100%, RESTITUTION_OVERDUE Calcul au moment de la transition + RestitutionSlaScheduler pour les événements temporels C4, C7 Colonnes DB non-null après restitution ; 3 événements journal distincts Dérive horloge — mitigé par UTC + NTP existant (PD-251)
INV-279-06-no-destruction-while-restituted Rejet destruction HTTP 409 Vérification status !== 'RESTITUTED' dans EligibilityService.reVerifyEligibility() + soft-delete guard C8 HTTP 409 + DOCUMENT_RESTITUTED_DESTRUCTION_FORBIDDEN Chemin de contournement si un nouveau endpoint destruction est ajouté sans garde — mitigé par tests contractuels
INV-279-07-transition-matrix Transitions explicitement autorisées/interdites Extension de ALLOWED_TRANSITIONS dans DocumentStateMachineService C3 InvalidStateTransitionError levée pour toute transition non listée Aucun risque — même pattern que PD-250
INV-279-08-ddl-reversible Migration up/down réversible Migration TypeORM avec up() et down() ; précondition SELECT COUNT(*) WHERE status='RESTITUTED' dans down() C1 npm run typeorm migration:run puis migration:revert sans erreur ALTER TYPE DROP VALUE n'existe pas en PG < 16 — utiliser stratégie rename+recreate
INV-279-09-atomicity Transition + attestation atomiques ; async post-commit QueryRunner unique pour UPDATE + INSERT lifecycle_log ; audit post-commit C4 Crash pré-commit → aucun artefact persisté ; crash post-commit → état DB valide + rattrapage async Si QueryRunner abandonné sans rollback — garanti par NestJS TypeORM lifecycle
INV-279-10-cross-module-guard Module destruction vérifie status avant destruction Vérification dans EligibilityService.reVerifyEligibility() + DestructionExecutionService + DocumentsController.delete() C8 HTTP 409 sur 3 routes destruction Couplage cross-module — mitigé par import direct de l'enum DocumentStatus
INV-279-11-destruction-checkpoint-uniqueness Vérification AVANT inclusion batch ET AVANT exécution Double vérification : (1) selectEligible() filtre status=EXPIRED excluant RESTITUTED, (2) reVerifyEligibility() vérifie unitairement avant destroy C8 Document RESTITUTED jamais inclus ni détruit Race condition fenêtre entre inclusion et exécution — mitigé par SELECT FOR UPDATE dans reVerifyEligibility()
INV-279-12-expired-terminal EXPIRED : aucune transition sortante dans ce périmètre (hors destruction PD-250) ALLOWED_TRANSITIONS pour EXPIRED ne contient pas RESTITUTED C3 InvalidStateTransitionError si tentative EXPIRED→RESTITUTED Aucun risque — matrice explicite

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-279-01 Extension enum TS + migration PG ALTER TYPE ADD VALUE C1, C2 DocumentStatus.RESTITUTED compilable + persisté en DB
CA-279-02 Migration up() ajoute enum + colonnes ; down() vérifie précondition puis retire C1 CI migration aller/retour sans erreur PG < 16 : ALTER TYPE DROP VALUE impossible → stratégie alternative
CA-279-03 RestitutionService.restitute() avec gardes séquentielles + transaction C4, C5 HTTP 200 + status=RESTITUTED en DB
CA-279-04 Gardes ordonnées §6.1 dans RestitutionService.restitute() C4, C11 409 INVALID_SOURCE_STATE, 423 DOCUMENT_UNDER_LEGAL_LOCK, 409 INSUFFICIENT_GEO_COPIES selon garde violée
CA-279-05 RestitutionService.returnFromRestitution() avec gardes ownership + état C4, C5 HTTP 200 + status=SEALED en DB
CA-279-06 Garde état dans returnFromRestitution() C4, C11 HTTP 409 INVALID_SOURCE_STATE
CA-279-07 IntegrityJournalService.appendEntry() dans la transaction C4, C10 Entrée integrity_journal_entries avec tous champs (timestamp, actor_id, type, security_level)
CA-279-08 Vérification dans EligibilityService (inclusion) + DestructionExecutionService (exécution) + DocumentsController.delete() C8 HTTP 409 + audit de refus sur les 3 routes
CA-279-09 Calcul dans restitute() : deadline = restituted_at + config.restitutionMaxDuration C4, C6 Écart 0 seconde entre valeurs DB
CA-279-10 RestitutionSlaScheduler avec 3 seuils + idempotence C7 3 événements journal distincts (80%, 100%, OVERDUE)
CA-279-11 Mise à jour modèle TLA+ avec RESTITUTED dans States C13 tlc ou apalache retourne PASS sur AnchorAssume_States
CA-279-12 Garde ownership dans restitute() et returnFromRestitution() C4 HTTP 403 pour non-owner sur les deux endpoints
CA-279-13 Détection d'état déjà atteint avant transaction : si RESTITUTED → 200 sans attestation ; si SEALED → 200 sans attestation C4 Rejeu sans duplication d'attestation

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau de test visé
TC-NOM-01 INV-279-01, INV-279-02, INV-279-05, CA-279-01/03/09 RestitutionService.restitute() + transaction + calcul SLA HTTP 200, status DB = RESTITUTED, deadline exacte, lifecycle_log créé Unit + Integration
TC-NOM-02 INV-279-03, CA-279-05 RestitutionService.returnFromRestitution() + transaction HTTP 200, status DB = SEALED, lifecycle_log RETURN_FROM_RESTITUTION Unit + Integration
TC-NOM-03 INV-279-05, CA-279-10 RestitutionSlaScheduler + IntegrityJournalService Alerte 80%, escalade 100%, OVERDUE — 3 événements distincts Unit (horloge simulée)
TC-NOM-04 INV-279-04, CA-279-07 IntegrityJournalService.appendEntry() pour RESTITUTION + RETURN 2 entrées lifecycle_log avec timestamp UTC, actor_id, type, security_level Integration
TC-NOM-05 INV-279-06, INV-279-10, CA-279-08 EligibilityService.reVerifyEligibility() + DocumentsController.delete() HTTP 409 + audit de refus Unit + Integration
TC-NOM-06 INV-279-08, CA-279-02 Migration up/down via TypeORM Commandes migration:run/revert sans erreur Integration (CI)
TC-NOM-07 CA-279-11 Modèle TLA+ mis à jour AnchorAssume_States PASS Formel (TLC)
TC-ERR-01 §6 (400) ParseUUIDPipe validation HTTP 400, aucune mutation DB Unit
TC-ERR-02 §6 (404) Requête DB retourne null HTTP 404, aucune attestation Unit
TC-ERR-03 INV-279-02, CA-279-12 Garde ownership dans restitute() HTTP 403, status inchangé Unit
TC-ERR-04 INV-279-03, CA-279-12 Garde ownership dans returnFromRestitution() HTTP 403, status inchangé Unit
TC-ERR-05 INV-279-02, INV-279-07, CA-279-04 Garde état != SEALED HTTP 409 INVALID_SOURCE_STATE Unit
TC-ERR-06 INV-279-02, CA-279-04 Garde geo_copy_count < 2 HTTP 409 INSUFFICIENT_GEO_COPIES Unit
TC-ERR-07 INV-279-02, CA-279-04 Garde legal_lock = true HTTP 423 DOCUMENT_UNDER_LEGAL_LOCK Unit
TC-ERR-08 INV-279-03, INV-279-07, CA-279-06 Garde état != RESTITUTED pour retour HTTP 409 INVALID_SOURCE_STATE Unit
TC-ERR-09 INV-279-05 Joi validation dans RestitutionConfig Rejet config au démarrage si hors bornes [1,30] Unit
TC-ERR-10 INV-279-09 Erreur forcée dans transaction (mock QueryRunner) HTTP 500, rollback total, aucune attestation Unit
TC-INV-09A INV-279-09 Transaction interrompue avant commit Ni status ni lifecycle_log persistés Unit
TC-INV-09B INV-279-09 Scheduler SLA redémarré après interruption Événements manquants émis, doublons évités Unit
TC-NR-01 Non-régression Lecture status PENDING/SEALED/EXPIRED Aucune erreur désérialisation Integration
TC-NR-02 Non-régression Flux SEALED→EXPIRED inchangé Comportement identique baseline Integration
TC-NR-03 Non-régression Triggers immutabilité existants Écriture restitution possible Integration
TC-NR-04 Non-régression Destruction pour statuts non-RESTITUTED Politique inchangée Integration
TC-NEG-01 Adversarial Deux appels concurrents restitute 1 seul succès, SELECT FOR UPDATE sérialise Integration
TC-NEG-02 Adversarial Rejeu return après retour effectué Idempotent : 200 sans attestation Unit
TC-NEG-03 Adversarial UUID casse mixte Traitement normal Unit
TC-NEG-04 Adversarial security_level invalide Rejet transactionnel Unit
TC-NEG-05 Adversarial Crash post-commit État DB cohérent, rattrapage async Integration

6. Gestion des erreurs

Code HTTP error_code Condition Composant Observable
400 INVALID_DOCUMENT_ID UUID non conforme v4 C5 (ParseUUIDPipe) Body JSON avec message validation
401 Token JWT absent/expiré Guard Auth existant
403 NOT_DOCUMENT_OWNER document.userId !== actorId C4 Body JSON avec error_code
404 DOCUMENT_NOT_FOUND Document inexistant C4 Body JSON
409 INVALID_SOURCE_STATE État incompatible (non-SEALED pour restitute, non-RESTITUTED pour return) C4, C11 Body JSON avec current_state
409 INSUFFICIENT_GEO_COPIES geo_copy_count < 2 lors restitution C4, C11 Body JSON avec geo_copy_count actuel
409 DOCUMENT_RESTITUTED_DESTRUCTION_FORBIDDEN Destruction d'un document RESTITUTED C8, C11 Body JSON + audit refus
423 DOCUMENT_UNDER_LEGAL_LOCK legal_lock=true lors restitution C4, C11 Body JSON
422 INVALID_SLA_CONFIG Bornes SLA hors [1j, 30j] au chargement config C6 Joi.ValidationError au démarrage
500 Échec transactionnel non-métier C4 Rollback total, log error

Stratégie de propagation : Les erreurs métier sont des exceptions NestJS (ConflictException, ForbiddenException, NotFoundException, HttpException(423)) avec payload structuré { error_code, message, details? }. Un ExceptionFilter spécifique normalise le format de réponse.

7. Impacts sécurité

Risque Mitigation Composant
Élévation de privilèges : non-owner pourrait restituer Garde ownership obligatoire avec SELECT FOR UPDATE vérifiant user_id C4
Contournement destruction : document RESTITUTED détruit Triple vérification cross-module (inclusion batch, exécution batch, soft-delete) C8
Race condition : appels concurrents restitute/return SELECT ... FOR UPDATE sur la ligne document dans le QueryRunner C4
Injection SQL : paramètres document_id ParseUUIDPipe NestJS (validation UUID v4 format) + requêtes paramétrées TypeORM C5
Déni de service SLA : configuration altérée Joi validation stricte au démarrage avec rejet complet si hors bornes C6
Audit trail incomplet : transition sans attestation Atomicité ACID — attestation lifecycle_log dans la même transaction que la MAJ status C4
Fuite information : error_code révèle l'état Les codes d'erreur sont génériques et ne révèlent pas de données sensibles (pas de PII) C11
Journalisation : toutes les transitions et refus sont audités AuditLogService.logAsync() post-commit pour chaque opération (succès et refus) C9

8. Hypothèses techniques

ID Hypothèse Validation Impact si faux
HT-279-01 document_id est UUID v4 (contrat API existant, validé par ParseUUIDPipe) Vérifié : ParseUUIDPipe utilisé dans tous les controllers documents Adapter regex + tests
HT-279-02 geo_copy_count est un champ existant accessible sur l'entité DocumentSecure NON VÉRIFIÉ : ce champ n'existe pas actuellement dans l'entité. Sera implémenté comme colonne ou résolu via service externe Si absent : ajouter colonne dans la migration C1 ou créer un service de résolution
HT-279-03 security_level est disponible dans le contexte documentaire au moment de la transition PARTIELLEMENT VÉRIFIÉ : ce concept existe dans le module d'intégrité PD-251 mais pas directement sur l'entité document Si absent : utiliser une valeur par défaut ou récupérer via service
HT-279-04 Le module destruction PD-250 est déployé et fonctionnel Vérifié : code et migrations présents dans le backend
HT-279-05 PostgreSQL >= 14 (pour les features enum utilisées) À vérifier : la stratégie de down() migration dépend de la version PG Si PG < 14 : stratégie rename+recreate enum
HT-279-06 IntegrityJournalService accepte de nouveaux JournalEventType sans modification de signature Vérifié : appendEntry(archiveId, eventType, payload) prend un string enum
HT-279-07 Les jobs planifiés SLA sont idempotents via payload_digest dans le journal d'intégrité Vérifié : IntegrityJournalEntry.payloadDigest permet la déduplication
HT-279-08 EXPIRED est déjà terminal dans la matrice de transitions pour le contexte restitution Vérifié partiellement : EXPIRED→DESTROYED et EXPIRED→RECONCILIATION_FAILED existent dans PD-250 ; PD-279 ne modifie pas ces transitions

Résolution HT-279-02 (geo_copy_count)

Le champ geo_copy_count n'existe pas dans l'entité DocumentSecure. Deux approches :

  1. Ajout d'une colonne geo_copy_count integer NOT NULL DEFAULT 0 dans la migration PD-279 — simple mais couple le concept de réplication géo au document.
  2. Service de résolution qui compte les copies géo via un service externe (OVH Object Storage multi-région) — plus propre architecturalement mais ajoute une dépendance réseau dans le flux de garde.

Décision : Approche 1 (colonne) car la spécification traite geo_copy_count comme un attribut du document. Un trigger ou un job de synchronisation met à jour cette valeur en amont (hors périmètre PD-279). La migration PD-279 ajoute la colonne avec DEFAULT 0, ce qui signifie que les gardes de restitution refuseront par défaut (geo_copy_count < 2) tant que le mécanisme de comptage amont n'a pas initialisé la valeur — comportement fail-safe conforme à l'esprit ISO 14641.

Résolution HT-279-03 (security_level)

Le champ security_level sera passé en paramètre à la méthode restitute()/returnFromRestitution() depuis le contexte d'authentification (JWT claims ou profil utilisateur). Si non disponible, utiliser la valeur par défaut du document (niveau de sécurité du coffre-fort associé). Le service récupère cette information depuis le contexte existant @CurrentUser() decorator.

9. Points de vigilance (risques, dette, pièges)

# Point Risque Mitigation
V1 ALTER TYPE ADD VALUE non-transactionnelle PostgreSQL ne permet pas ALTER TYPE ... ADD VALUE dans une transaction. La migration doit exécuter cette commande hors transaction. Pattern PD-250 : séparer l'ajout de valeur enum dans un queryRunner.query() direct, pas dans un bloc BEGIN/COMMIT
V2 Down migration enum ALTER TYPE ... DROP VALUE n'existe pas en PG < 16. Stratégie : (1) vérifier précondition COUNT(*)=0 WHERE status='RESTITUTED', (2) recréer le type enum avec les anciennes valeurs, (3) ALTER TABLE ALTER COLUMN pour utiliser le nouveau type
V3 geo_copy_count inexistant Le champ n'existe pas dans l'entité actuelle. Sa sémantique (qui l'initialise, qui le met à jour) est hors périmètre PD-279. Ajouter la colonne en migration PD-279 avec DEFAULT 0 (fail-safe). Documenter le STUB de mise à jour comme // STUB: PD-XXX — mécanisme de comptage copies géo
V4 Couplage module destruction PD-279 modifie des fichiers du module destruction (eligibility, execution). Limiter les modifications au strict minimum (ajout d'un if de vérification status). Tests de non-régression obligatoires sur la destruction
V5 BullMQ queue pour SLA scheduler Nouvelle queue pv-jobs-restitution-sla à enregistrer dans le module NestJS. Suivre le pattern PD-250 : constante de nom de queue, enregistrement dans BullModule.forRoot()
V6 Idempotence des endpoints REST Le rejeu d'un POST restitute sur un document déjà RESTITUTED ne doit pas créer de nouvelle attestation. Vérifier l'état AVANT la transaction. Si déjà dans l'état cible → retour immédiat sans mutation
V7 Spec review PD-279 — legal_lock HTTP 423 vs 409 La spec v2 utilise HTTP 423 (Locked) pour legal_lock=true, pas 409. Le code existant (PD-63) utilise déjà 423 pour legal_lock. Respecter la spec v2 : utiliser HttpException(423) avec DOCUMENT_UNDER_LEGAL_LOCK

10. Hors périmètre

  • État DISPOSED : STUB planifié en PD-280, transition RESTITUTED→DISPOSED interdite dans PD-279.
  • NF Z42-013 DIP : Couvert par PD-278.
  • Restitution multi-destinataire : Non spécifié, non implémenté.
  • Frontend/mobile : Aucune hypothèse UI dans cette story.
  • Mécanisme de comptage geo_copy_count : Seule la colonne et la garde sont implémentées. Le mécanisme de mise à jour du compteur est hors périmètre (STUB).
  • Notification utilisateur : Les événements SLA sont des alertes opérationnelles, pas des notifications end-user.

11. Mécanismes cross-module

Élément Détail
Routes de l'autre module à protéger POST /destruction/batches (inclusion), POST /destruction/batches/:id/execute (exécution), DELETE /documents/:id (soft-delete)
Controller et méthode (inclusion) EligibilityService.selectEligible() — filtre déjà status=EXPIRED, exclut implicitement RESTITUTED
Controller et méthode (exécution) EligibilityService.reVerifyEligibility() — ajout check status === 'RESTITUTED'
Controller et méthode (soft-delete) DocumentsController.delete() ou service associé — ajout check status === 'RESTITUTED'
Effet du guard HTTP 409 DOCUMENT_RESTITUTED_DESTRUCTION_FORBIDDEN + audit de refus
Mécanisme de jointure cross-schéma vault_secure.documents.status lu directement via DataSource — même schéma, pas de FK cross-schéma
Scope d'enregistrement Vérification au niveau service du module destruction (pas de guard NestJS global)
Exceptions d'accès Aucune : même rôle ADMIN ne peut détruire un RESTITUTED

12. Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire Tous les composants C1-C11 : RestitutionService, RestitutionSlaScheduler, RestitutionConfig, state machine, guards, DTOs, error handling
Intégration Interactions RestitutionService ↔ IntegrityJournalService ↔ AuditLogService ; cross-module destruction ↔ restitution ; migration up/down
E2E Flux HTTP complet restitute/return via supertest
Formel Vérification TLA+ AnchorAssume_States
Performance Hors scope : volume de restitutions prévu faible (< 10/jour), pas de benchmark requis Volume insuffisant pour justifier un test de charge dédié

Tous les niveaux de test sont couverts sauf performance (justification : volumétrie faible). Aucune exclusion de composant.

13. Ordonnancement des composants

Phase 1 — Fondations (C1, C2, C3, C6, C9, C10, C11)
  Migration + enum extension + state machine + config + types audit/journal + error DTOs

Phase 2 — Core métier (C4, C5)
  RestitutionService + RestitutionController

Phase 3 — Cross-module (C8)
  Extension destruction guard

Phase 4 — SLA async (C7)
  RestitutionSlaScheduler

Phase 5 — Vérification formelle (C13)
  TLA+ model update

Phase 6 — Tests (C12)
  Suite complète unitaire + intégration + E2E + formel