Aller au contenu

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 CachedEmployeeData existe déjà pour (tenantId, employeeId), logger un warning et retourner
  • Insérer une nouvelle ligne CachedEmployeeData avec tous les champs de l'événement, synced_at = Instant.now()
  • Créer un nouveau LeaveEmployeeProfile avec :
    • annual_balance_ouvrables = BigDecimal.ZERO
    • exceptional_days_used_this_period = BigDecimal.ZERO
    • sick_days_current_rolling_12m = BigDecimal.ZERO
    • number_of_children_under_21 = 0
    • reference_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 CachedEmployeeData existant 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_id avec newManagerId pour (tenantId, employeeId)
  • Trouver toutes les LeaveRequest avec tenant_id = tenantId AND approver_id = oldManagerId AND status = 'PENDING_APPROVAL' pour l'employé concerné
  • Mettre à jour approver_id avec newManagerId sur 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 tenantId de l'événement ne doit pas être null
  • Le employeeId de l'événement ne doit pas être null
  • ManagerChanged.newManagerId ne 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, jamais 0.0 ou null

Considérations multi-tenant

  • Tous les événements portent un tenantId — chaque opération en base de données inclut le tenantId dans 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), §@ApplicationModuleListener
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Timestamp types
  • docs/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éro
  • EmployeeEventListener.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îchi
  • EmployeeEventListener.on(EmployeeUpdated) : employé non en cache — vérifier que l'erreur est loggée, pas d'exception
  • EmployeeEventListener.on(ManagerChanged) : vérifier la mise à jour du cache + mise à jour de l'approver_id des demandes en attente

Tests d'intégration

  • Publier EmployeeCreated via ApplicationEventPublisher — vérifier que CachedEmployeeData et LeaveEmployeeProfile sont persistés en base
  • Publier ManagerChanged avec des demandes PENDING_APPROVAL existantes — vérifier la mise à jour de l'approver_id en base
  • Publier un EmployeeCreated dupliqué — 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