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 :
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_typemax 100 caractèresmodule_codemax 20 caractères- Plage de dates de requête :
date_fromdoit ê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
timestamppour les performances à grande échelle
Migrations Flyway¶
V002__create_audit_entry_partitioned.sql— Base tenant : tableaudit_entriesavec 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 Eventsdocs/standards/database-guidelines.md— §JSONB columns, §Time-partitioned tables, §Immutable tablesdocs/standards/api-guidelines.md— §Pagination, §Query parameter conventionsdocs/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