Story AB-003 : Employee Event Listeners + Cache Sync¶
Module : absence-management Slice : VS-AB-003 from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 3 Depends on : AB-002 Can develop concurrently with : AB-004 Merge order : Après AB-002 Estimated complexity : S PRD User Stories : N/A - Technical Story
Objectif¶
Mettre en place les event listeners Spring Modulith qui s'abonnent aux événements EmployeeCreated, EmployeeUpdated
et ManagerChanged du Control Plane, en mettant en cache les données employé localement dans la table CachedEmployeeData.
Cela élimine le couplage à la compilation avec le module organisation et fournit des lookups à latence zéro pour la validation des demandes de congé et le routage des approbations.
Périmètre Backend¶
Entités & Records¶
CachedEmployeeData — défini dans AB-002, utilisé ici.
Événements consommés (publiés par le module Control Plane) :
// These records are defined in the Control Plane module's public API
public record EmployeeCreated(
UUID tenantId, UUID employeeId, String matricule,
String firstName, String lastName, String category,
LocalDate hireDate, UUID orgUnitId, UUID managerId
) {}
public record EmployeeUpdated(
UUID tenantId, UUID employeeId,
String firstName, String lastName, String category,
LocalDate hireDate, UUID orgUnitId, String status
) {}
public record ManagerChanged(
UUID tenantId, UUID employeeId,
UUID oldManagerId, UUID newManagerId,
LocalDate effectiveDate
) {}
Couche Repository¶
CachedEmployeeDataRepository— défini dans AB-002 :findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData>-
Méthode supplémentaire nécessaire :
existsByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> boolean -
LeaveEmployeeProfileRepository— défini dans AB-002 : -
findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<LeaveEmployeeProfile> -
LeaveRequestRepository— défini dans AB-002 : - Méthode supplémentaire nécessaire :
findByTenantIdAndApproverIdAndStatus(UUID tenantId, UUID approverId, String status) -> List<LeaveRequest>
Couche Service¶
EmployeeEventListener.java — @ApplicationModuleListener dans le package com.altarys.papillon.modules.absence.listener :
@ApplicationModuleListener void on(EmployeeCreated event):- Vérification d'idempotence : si
CachedEmployeeDataexiste déjà pour (tenantId, employeeId), logger un warning et retourner - Insérer une nouvelle ligne
CachedEmployeeDataavec tous les champs de l'événement,synced_at = Instant.now() - Créer un nouveau
LeaveEmployeeProfileavec :annual_balance_ouvrables = BigDecimal.ZEROexceptional_days_used_this_period = BigDecimal.ZEROsick_days_current_rolling_12m = BigDecimal.ZEROnumber_of_children_under_21 = 0reference_period_start/reference_period_end= limites de l'année civile en cours (1er jan au 31 déc)
-
Log :
"Employee cached and profile created: {} for tenant {}", employeeId, tenantId -
@ApplicationModuleListener void on(EmployeeUpdated event): - Trouver le
CachedEmployeeDataexistant par (tenantId, employeeId) — si non trouvé, logger une erreur et retourner - Mettre à jour les champs :
firstName,lastName,category,hireDate,orgUnitId,employeeStatus - Définir
synced_at = Instant.now() - Sauvegarder l'entité mise à jour
-
Log :
"Employee cache updated: {} for tenant {}", employeeId, tenantId -
@ApplicationModuleListener void on(ManagerChanged event): - Mettre à jour
CachedEmployeeData.current_manager_idavecnewManagerIdpour (tenantId, employeeId) - Trouver toutes les
LeaveRequestavectenant_id = tenantId AND approver_id = oldManagerId AND status = 'PENDING_APPROVAL'pour l'employé concerné - Mettre à jour
approver_idavecnewManagerIdsur chaque demande en attente - Définir
synced_at = Instant.now()sur l'entrée de cache - Log :
"Manager changed for employee {}: {} -> {}. Updated {} pending requests", employeeId, oldManagerId, newManagerId, count
Endpoints API¶
N/A — Pas d'endpoints dans ce slice. C'est un service interne piloté par événements.
Règles de validation¶
- Le
tenantIdde l'événement ne doit pas être null - Le
employeeIdde l'événement ne doit pas être null ManagerChanged.newManagerIdne doit pas être null (old peut être null pour la première affectation de manager)- Tous les champs BigDecimal du profil initialisés à
BigDecimal.ZERO, jamais0.0ounull
Considérations multi-tenant¶
- Tous les événements portent un
tenantId— chaque opération en base de données inclut letenantIddans la requête - Les event listeners traitent dans le contexte tenant de l'événement (pas besoin de TenantContext ScopedValue — le tenant_id vient directement du payload de l'événement)
- La communication inter-modules utilise les Application Events Spring Modulith — jamais d'appels de méthodes directs au module Control Plane
Périmètre Frontend¶
N/A — Story d'infrastructure backend.
Pages & Routes¶
N/A
Composants¶
N/A
États UI (TOUS REQUIS)¶
N/A — Backend uniquement.
Comportement responsive¶
N/A
Interactions¶
N/A
Critères d'acceptation¶
AC-007: Employee created event triggers cache insert
Given an EmployeeCreated is published by the Control Plane
When the Absence module listener receives it
Then a CachedEmployeeData row is inserted AND a LeaveEmployeeProfile is created with annual_balance_ouvrables = 0
AC-008: Employee updated event updates cache
Given a CachedEmployeeData row exists for an employee
When an EmployeeUpdated is received with updated category or hire_date
Then the cached row is updated with the new values and synced_at is set to now
AC-009: Manager change updates approval routing
Given employees have PENDING_APPROVAL leave requests with approver_id = old_manager
When a ManagerChanged is received
Then current_manager_id in cache is updated AND approver_id on pending requests is updated to new_manager
AC-010: Listener is idempotent
Given the same EmployeeCreated is published twice (duplicate)
When the listener processes both
Then only one CachedEmployeeData row exists (no duplicate, no error)
Règles OHADA & réglementaires¶
N/A — Ceci est une story d'infrastructure/intégration. Les règles OHADA s'appliquent à la logique métier qui utilise les données en cache (AB-004+), pas au mécanisme de mise en cache lui-même.
Standards & conventions¶
docs/standards/java-spring-guidelines.md— §Spring Modulith boundaries (Application Events pour la communication inter-modules, jamais d'appels de méthodes directs), §@ApplicationModuleListenerdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §Timestamp typesdocs/standards/multi-datasource-patterns.md— §Tenant-scoped repositories
Exigences de test¶
Tests unitaires¶
EmployeeEventListener.on(EmployeeCreated): vérifier l'insertion CachedEmployeeData + la création LeaveEmployeeProfile avec des soldes à zéroEmployeeEventListener.on(EmployeeCreated): événement dupliqué — vérifier l'idempotence (pas d'exception, pas de ligne en double)EmployeeEventListener.on(EmployeeUpdated): vérifier la mise à jour des champs du cache, synced_at rafraîchiEmployeeEventListener.on(EmployeeUpdated): employé non en cache — vérifier que l'erreur est loggée, pas d'exceptionEmployeeEventListener.on(ManagerChanged): vérifier la mise à jour du cache + mise à jour de l'approver_id des demandes en attente
Tests d'intégration¶
- Publier
EmployeeCreatedviaApplicationEventPublisher— vérifier que CachedEmployeeData et LeaveEmployeeProfile sont persistés en base - Publier
ManagerChangedavec des demandes PENDING_APPROVAL existantes — vérifier la mise à jour de l'approver_id en base - Publier un
EmployeeCreateddupliqué — vérifier une seule ligne, pas d'erreur de violation de contrainte
Ce que la QA validera¶
- N/A — Pas de fonctionnalité visible par l'utilisateur. Le développeur valide la gestion des événements via les tests d'intégration.
Hors périmètre¶
- Gestion de
EmployeeTerminated(AB-021 ou story ultérieure) EmployeeReassigned(V2)- Appels API directs au module Control Plane (viole les frontières Spring Modulith)
- Affichage frontend des données employé en cache
- Import en masse des employés existants dans le cache (story de migration séparée)
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes — N/A (pas d'endpoints)
- [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px — N/A (backend uniquement)
- [ ] Les 5 états UI implémentés (loading, empty, error, offline, success) — N/A
- [ ] Le micro-copy français correspond exactement à la spec de design — N/A
- [ ] 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 — Les événements portent le tenant_id, toutes les requêtes scopées par tenant_id
- [ ] L'écriture + synchronisation offline fonctionne (Papillon) / N/A (Enterprise) — N/A (backend uniquement)
- [ ] Pas de TypeScript
any— N/A - [ ] Pas de Java raw types — tous les generics explicites
- [ ] Les réponses API suivent le format d'enveloppe standard — N/A (pas d'endpoints)
- [ ] Entrées de piste d'audit pour chaque mutation de données — Création de LeaveEmployeeProfile et mises à jour de changement de manager loggées