Aller au contenu

Story CP-004 : Service Backend d'Audit Trail

Module : control-plane.
Slice : Slice 4 from architecture.
Brand context : Papillon HR Suite + ALTARYS ENTERPRISE HR Suite.
Priority : 4.
Depends on : CP-001.
Estimated complexity : M.


Objectif

Construire le service backend d'audit trail partagé et immuable, utilisé par TOUS les modules de la plateforme.
Chaque opération de création, mise à jour ou suppression doit produire une entrée d'audit avec des snapshots avant/après.
Cela satisfait l'AUDCIF Art. 24 (conservation 10 ans). Le service d'audit est un composant d'infrastructure partagé: il est consommé par chaque story suivante. L'interface de visualisation de l'audit est dans CP-016.


Périmètre Backend

Entités & Records

Entité AuditEntry (base tenant, partitionnée dans le temps) :

Champ Type Contraintes
id UUID v7 PK, généré côté serveur
tenant_id UUID NOT NULL
user_id UUID NOT NULL (FK vers User — qui a effectué l'action)
timestamp Instant NOT NULL, UTC
entity_type String NOT NULL (ex. "Employee", "LeaveRequest", "Tenant")
entity_id UUID NOT NULL
module_code String NOT NULL (ex. "CP", "ABSMGT", "PAYROL")
action AuditAction enum NOT NULL
before_value JSONB Nullable (null pour CREATE)
after_value JSONB Nullable (null pour DELETE)
metadata JSONB Nullable (contexte supplémentaire, ex. "raison de la suspension")

Enum AuditAction :

public enum AuditAction { CREATE, UPDATE, DELETE }

DTOs :

public record AuditEntryResponse(
    UUID id, UUID userId, String userFullName, Instant timestamp,
    String entityType, UUID entityId, String moduleCode,
    AuditAction action, JsonNode beforeValue, JsonNode afterValue,
    JsonNode metadata
) {}

public record AuditPageResponse(
    List<AuditEntryResponse> entries, int totalCount, int page, int pageSize
) {}

public record AuditQueryParams(
    @Nullable String entityType,
    @Nullable UUID entityId,
    @Nullable UUID userId,
    @Nullable AuditAction action,
    @Nullable String moduleCode,
    @Nullable Instant dateFrom,
    @Nullable Instant dateTo,
    int page,    // défaut 0
    int pageSize // défaut 50
) {}

Couche Repository

  • AuditRepository.java — Tenant-aware :
  • save(AuditEntry) → AuditEntry — INSERT uniquement (jamais de mise à jour)
  • findByFilters(UUID tenantId, AuditQueryParams) → Page<AuditEntry> — Requête paginée avec filtres optionnels sur entity_type, entity_id, user_id, action, module_code, plage de dates
  • Pas de méthode delete (immuable)
  • Pas de méthode update (immuable)

Couche Service

AuditEventListener.java (partagé — piloté par événements selon ADR-21) : - Aucune méthode .log() directe exposée aux autres modules. Au lieu de ça, les modules publient des événements domaine (ex. EmployeeCreatedEvent, TenantStatusChangedEvent), et le module Audit écoute. - @EventListener sur tous les événements domaine → convertit chaque événement en AuditEntry : - Injecte automatiquement tenant_id depuis TenantContext et user_id depuis SecurityContext - Mappe le nom de classe de l'événement → entity_type, les champs de l'événement → JSONB after_value - Génère un UUID v7 pour l'entrée - Sauvegarde de manière asynchrone (non-bloquant — n'alentit pas l'opération principale) - Selon ADR-21 : "Zéro couplage — les modules publient des événements, le module Audit écoute. Livraison garantie via la table EVENT_PUBLICATION. Aucun appel explicite au service d'audit."

AuditQueryService.java : - query(AuditQueryParams params) → AuditPageResponse — Requête tenant-scopée pour l'UI (consommée par CP-016)

Notes d'implémentation : - Les écritures d'audit sont fire-and-forget (asynchrones). Si une écriture d'audit échoue, elle NE DOIT PAS faire échouer la transaction principale. Logger l'échec dans les logs applicatifs pour la supervision ops. - Couverture = couverture des événements : chaque mutation de données DOIT publier un événement domaine, que le listener d'audit capturera automatiquement.

Endpoints API

Aucun endpoint REST dans cette story. L'endpoint de requête (GET /api/v1/audit) est dans CP-016.

Règles de validation

  • Les entrées d'audit sont immuables : aucun endpoint PUT, PATCH ou DELETE n'existe
  • entity_type max 100 caractères
  • module_code max 20 caractères
  • Plage de dates de requête : date_from doit être antérieure à date_to
  • Taille de page : 1–100 (défaut 50)

Considérations multi-tenant

  • Les entrées d'audit sont dans la base tenant (tenant-scoped)
  • L'API de requête scope automatiquement par TenantContext.current().tenantId()
  • L'admin plateforme (CP-012) peut interroger sur plusieurs tenants — cet endpoint est dans la story Console Admin
  • Partitionnement temporel : partitions mensuelles sur la colonne timestamp pour les performances à grande échelle

Migrations Flyway

  • V002__create_audit_entry_partitioned.sql — Base tenant : table audit_entries avec partitionnement temporel (mensuel). Créer les partitions initiales pour le mois courant et les 3 prochains mois. Inclure un job planifié ou un déclencheur pour créer les partitions futures.

Périmètre Frontend

Aucun — cette story est uniquement backend. Toute l'UI d'audit (AuditTrailPage, AuditEntryCard, AuditDetailSheet, AuditFilterBar) est dans CP-016.


Critères d'acceptation

AC-026: Audit entry created on data mutation
Given I create an employee via POST /api/v1/employees
When the employee is persisted
Then an audit entry is created with action=CREATE, entityType="Employee", afterValue=employee JSON, beforeValue=null

AC-027: Audit entry captures before/after on update
Given employee "Moussa Koné" exists with department="Production"
When I update the department to "Commercial"
Then an audit entry is created with action=UPDATE, beforeValue containing department="Production", afterValue containing department="Commercial"

AC-028: Audit entries are immutable
Given an audit entry exists
When any attempt is made to update or delete it (via API or direct DB)
Then the operation is rejected (no DELETE or UPDATE API exists; DB constraint prevents DELETE)

AC-030: Audit is tenant-scoped
Given tenant A has 20 audit entries and tenant B has 10
When tenant A queries GET /api/v1/audit
Then only tenant A's 20 entries are returned (zero from tenant B)

ACs déplacés : AC-029, AC-031, AC-032, AC-033 → CP-016.


Règles OHADA & Réglementaires

  • AUDCIF Art. 24 : Tous les enregistrements financiers et comptables doivent être conservés pendant au minimum 10 ans. Les entrées d'audit satisfont cette exigence pour la traçabilité de TOUTES les mutations de données.
  • AUDCIF Art. 22 : Les entrées d'audit doivent être immuables — aucune modification, aucune suppression. Le schéma DB l'applique (pas de déclencheur DELETE, pas de permission UPDATE sur la table). Remarque : la conformité complète AUDCIF Art. 22 (chaînes de hash, verrouillage de période) est pour le module Comptabilité, pas pour l'audit trail général.
  • Loi 2013-450 : L'audit trail fournit la responsabilité requise pour le traitement des données personnelles. Chaque accès et modification de données personnelles (dossiers employés) est tracé.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Entities, §Async processing, §Application Events
  • docs/standards/database-guidelines.md — §JSONB columns, §Time-partitioned tables, §Immutable tables
  • docs/standards/api-guidelines.md — §Pagination, §Query parameter conventions
  • docs/standards/react-typescript-guidelines.md — §List/table components, §Filter pattern, §Bottom sheet detail

Exigences de test

Tests unitaires

  • AuditService.log() : création correcte de l'entrée avec sérialisation avant/après
  • AuditService.log() : gestion du before null (CREATE) et du after null (DELETE)
  • AuditService.log() : injection de tenant_id et user_id depuis le contexte

Tests d'intégration

  • Flux complet : créer une entité → l'entrée d'audit existe en DB avec les données correctes
  • Flux complet : mettre à jour une entité → l'entrée d'audit a le bon avant/après
  • Isolation tenant : la requête depuis le tenant B retourne zéro entrées du tenant A
  • Immuabilité : tentative de DELETE sur la table audit_entries → échec
  • Async : l'échec d'une écriture d'audit ne fait pas échouer la transaction principale

Ce que le QA validera

  • Les entrées d'audit sont créées pour chaque mutation de données (vérification via DB ou API)
  • Isolation tenant : aucune fuite d'audit inter-tenant

Hors périmètre

  • UI d'audit (AuditTrailPage, filtres, vue détail, contrôle d'accès) — CP-016
  • Endpoint de requête d'audit avec filtres (GET /api/v1/audit avec paramètres de filtre) — CP-016
  • Export CSV de l'audit trail — V2
  • Requête d'audit cross-tenant (pour l'admin plateforme) — CP-012
  • Chaînes de hash AUDCIF Art. 22 — Module Comptabilité
  • Automatisation de la gestion des partitions (création des nouvelles partitions mensuelles) — DevOps/cron, pas code applicatif dans cette story. Créer les partitions pour 6 mois à l'avance dans la migration.
  • Audit trail pour les actions de la console admin — CP-012

Définition de Done

  • [ ] AuditService injectable et produisant des entrées correctes pour CREATE, UPDATE, DELETE
  • [ ] Entité AuditEntry persistée en base tenant avec isolation tenant correcte
  • [ ] La migration Flyway crée la table partitionnée
  • [ ] Tous les critères d'acceptation passent manuellement
  • [ ] Tests unitaires écrits et passants
  • [ ] Tests d'intégration écrits et passants
  • [ ] Isolation multi-tenant vérifiée (aucune fuite de données inter-tenant)
  • [ ] Pas de types Java bruts — tous les génériques explicites
  • [ ] L'échec d'une écriture d'audit async ne fait pas échouer la transaction principale