Story AB-017 : Moteur d'Indemnisation Maladie (Backend)¶
Module : absence-management Tranche : VS-AB-012 de l'architecture Contexte de marque : [LES DEUX] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 17 Dépend de : AB-012, AB-014 Peut se développer en parallèle avec : AB-016 Ordre de fusion : Après AB-014 Complexité estimée : M PRD User Stories : US-ABS-005
Objectif¶
Implémenter le moteur de recherche dans les tables d'indemnisation CCI qui détermine les phases de prise en charge du congé maladie (FULL_PAY, HALF_PAY, ZERO_PAY) en fonction de la catégorie de l'employé, de son ancienneté et du cumul de jours de maladie sur une fenêtre glissante de 12 mois. Référence : les tranches d'indemnisation CCI sont définies dans docs/architecture/absence-management-arch.md (§Indemnisation Congé Maladie) et docs/research/3_validated/absence-management-legal-validated.md. Ces informations de phase sont incluses dans le LeaveApproved pour que la Paie puisse calculer la rémunération correcte.
Périmètre Backend¶
Entités et Enregistrements¶
SickLeaveIndemnizationRule (depuis AB-002) :
public class SickLeaveIndemnizationRule {
private UUID id;
private UUID tenantId;
private String category; // OUVRIER ou CADRE
private int seniorityMinYears; // borne inférieure inclusive
private int seniorityMaxYears; // borne supérieure exclusive (Integer.MAX_VALUE pour illimité)
private int fullPayMonths;
private int halfPayMonths;
private Instant createdAt;
private Instant updatedAt;
}
Enums :
public enum EmployeeCategory {
OUVRIER, // Ouvriers & Employes
CADRE // Cadres & Agents de Maitrise
}
public enum SickLeavePhase {
FULL_PAY, // 100% salaire
HALF_PAY, // 50% salaire
ZERO_PAY // 0% — non rémunéré
}
Enregistrement de réponse :
public record SickLeavePhaseResult(
SickLeavePhase phase,
int payRatePercent, // 100, 50 ou 0
int cumulativeSickDays, // total glissant sur 12 mois
int fullPayDaysRemaining,
int halfPayDaysRemaining,
boolean isZeroPayReached
) {}
Couche Repository¶
SickLeaveIndemnizationRuleRepository.java :
- findByTenantIdAndCategoryAndSeniority(UUID tenantId, String category, int seniorityYears) — retourne la règle CCI correspondant à la catégorie et la tranche d'ancienneté de l'employé
- findAllByTenantId(UUID tenantId) — retourne toutes les règles d'un tenant (pour vérification du peuplement)
- saveAll(List<SickLeaveIndemnizationRule> rules) — insertion en masse pour le peuplement lors de la création du tenant
Couche Service¶
SickLeaveIndemnizationService.java :
determinePhase(UUID employeeId, UUID tenantId): SickLeavePhaseResult- Obtenir la catégorie de l'employé depuis CachedEmployeeData (
OUVRIERouCADRE) - Calculer l'ancienneté en années depuis
hire_date - Rechercher la règle CCI dans
SickLeaveIndemnizationRuleRepositorypar (catégorie, tranche d'ancienneté) - Compter le cumul de jours de maladie sur 12 mois glissants depuis
LeaveBalanceLedger(entrées de type SICK_LEAVE_DEDUCTION dans les 365 derniers jours) - Déterminer la phase actuelle :
- Si
cumulativeDays <= fullPayMonths * 30→FULL_PAY(payRate=100) - Sinon si
cumulativeDays <= (fullPayMonths + halfPayMonths) * 30→HALF_PAY(payRate=50) - Sinon →
ZERO_PAY(payRate=0)
- Si
- Mettre à jour
LeaveEmployeeProfile.sick_leave_phaseetsick_days_current_rolling_12m -
Si phase == ZERO_PAY → publier
SickLeaveZeroPayAlertpour notification RH (FR-ABS-082) -
S'intègre au flux d'approbation : quand un congé maladie est approuvé (écoute l'événement d'approbation interne d'AB-012), appelle
determinePhase()et inclut la phase + payRate dans le payload deLeaveApproved
Tables d'Indemnisation CCI (peuplées à la création du tenant via données Flyway) :
Ouvriers & Employes :
| Ancienneté | Plein salaire | Demi-salaire | Zéro après |
|---|---|---|---|
| < 1 an | 1 mois | 3 mois | 4 mois |
| 1-5 ans | 1 mois | 3 mois | 4 mois |
| 6-10 ans | 2 mois | 3 mois | 5 mois |
| 11-15 ans | 3 mois | 3 mois | 6 mois |
| > 15 ans | 4 mois | 4 mois | 8 mois |
Cadres & Agents de Maîtrise :
| Ancienneté | Plein salaire | Demi-salaire | Zéro après |
|---|---|---|---|
| < 1 an | 1 mois | 3 mois | 4 mois |
| 1-5 ans | 2 mois | 3 mois | 5 mois |
| 6-10 ans | 3 mois | 4 mois | 7 mois |
| 11-15 ans | 4 mois | 4 mois | 8 mois |
| > 15 ans | 5 mois | 5 mois | 10 mois |
Points de terminaison API¶
Aucun nouveau endpoint public. Il s'agit d'un moteur de calcul backend consommé en interne :
- Invoqué par le flux d'approbation (AB-012) lors de l'approbation d'un congé maladie
- Résultats affichés dans Détail Solde RH (AB-021)
- Informations de phase incluses dans LeaveApproved pour la consommation par le module Paie
Règles de validation¶
- L'employé doit exister et être ACTIVE
- La catégorie de l'employé doit être résolvable (OUVRIER ou CADRE) — si manquante, journaliser un avertissement et utiliser OUVRIER par défaut (grille de prise en charge plus conservative)
- Le calcul de l'ancienneté utilise
hire_date— si null, traiter comme < 1 an - Les règles CCI doivent exister pour le tenant — si manquantes (corruption de données), lancer
IllegalStateExceptionet alerter les ops
Considérations Multi-Tenant¶
- Les règles d'indemnisation CCI sont peuplées par tenant à la création (Flyway + onboarding tenant)
- Toutes les requêtes incluent
tenantIddepuisScopedValue<TenantContext> - Les tenants ne peuvent pas personnaliser les tables CCI en MVP (données de peuplement en lecture seule) — personnalisation en V2
- Les requêtes de comptage de jours de maladie sur 12 mois glissants interrogent
LeaveBalanceLedgerlimité au tenant - Fonctionne avec les 4 profils de base de données (conçu pour BYO-DB en priorité)
Périmètre Frontend¶
Aucun Périmètre Frontend — il s'agit d'un moteur de calcul backend. Les résultats sont affichés dans :
- Détail Solde RH (AB-021) — affiche la phase actuelle et le cumul de jours de maladie
- Module Paie — consomme LeaveApproved avec la phase et le payRate
Critères d'Acceptation¶
AC-054 : Recherche dans la table CCI par catégorie et ancienneté
Étant donné un employé avec catégorie = CADRE et ancienneté = 7 ans
Quand son congé maladie est approuvé
Alors le système recherche la règle CCI : 3 mois plein salaire, 4 mois demi-salaire
AC-055 : Le cumul de jours de maladie sur 12 mois glissants détermine la phase
Étant donné qu'un employé a accumulé 95 jours de maladie sur la période glissante de 12 mois
Quand la règle CCI indique full_pay = 3 mois (90 jours)
Alors la phase actuelle est HALF_PAY (seuil de plein salaire dépassé)
AC-056 : La phase est incluse dans le LeaveApproved pour la Paie
Étant donné qu'un congé maladie est approuvé avec phase = HALF_PAY
Quand le LeaveApproved est publié
Alors le payload de l'événement inclut sickLeavePhase = "HALF_PAY" et payRate = 50
Règles OHADA et Réglementaires¶
- FR-ABS-080 : Tables d'indemnisation CCI différenciées par catégorie d'employé (Ouvrier/Cadre) et tranches d'ancienneté
- FR-ABS-081 : Suivi de phase basé sur le cumul de jours de maladie sur 12 mois glissants — réinitialisation lorsque la fenêtre de 12 mois se libère
- FR-ABS-082 : Alerter le RH lorsque l'employé atteint la phase ZERO_PAY (publier SickLeaveZeroPayAlert)
- FR-ABS-083 : Cotisations CNPS pendant le congé maladie basées sur le montant effectivement payé (TV-ABS-02) — responsabilité du module Paie
Standards et Conventions¶
docs/standards/java-spring-guidelines.md— §Patterns couche service, §Application Events, §Gestion des enumsdocs/standards/database-guidelines.md— §Pattern repository multi-tenant, §Isolation tenant dans chaque requête, §Patterns de données de peuplementdocs/standards/api-guidelines.md— §Communication inter-services via Application Eventsdocs/standards/multi-datasource-patterns.md— §Requêtes repository limitées au tenant, §Patterns JdbcClient
Exigences de Test¶
Tests Unitaires¶
SickLeaveIndemnizationService.determinePhase(): CADRE + 7 ans ancienneté → recherche (CADRE, 6-10) → fullPay=3m, halfPay=4mSickLeaveIndemnizationService.determinePhase(): OUVRIER + 3 ans ancienneté → recherche (OUVRIER, 1-5) → fullPay=1m, halfPay=3mSickLeaveIndemnizationService.determinePhase(): cumul=25 jours, fullPay=1m(30j) → FULL_PAYSickLeaveIndemnizationService.determinePhase(): cumul=35 jours, fullPay=1m(30j), halfPay=3m(90j) → HALF_PAYSickLeaveIndemnizationService.determinePhase(): cumul=125 jours, fullPay=1m(30j), halfPay=3m(90j) → ZERO_PAYSickLeaveIndemnizationService.determinePhase(): cumul=90 jours exactement (limite), fullPay=3m(90j) → FULL_PAY (inclusif)SickLeaveIndemnizationService.determinePhase(): cumul=91 jours, fullPay=3m(90j) → HALF_PAYSickLeaveIndemnizationService.determinePhase(): ZERO_PAY atteint → SickLeaveZeroPayAlert publiéSickLeaveIndemnizationService.determinePhase(): catégorie manquante → OUVRIER par défaut- Calcul ancienneté : hire_date=2020-01-15, aujourd'hui=2026-03-13 → 6 ans
Tests d'Intégration¶
- L'approbation d'un congé maladie déclenche determinePhase() et LeaveApproved inclut la phase
- Payload LeaveApproved : FULL_PAY → payRate=100
- Payload LeaveApproved : HALF_PAY → payRate=50
- Payload LeaveApproved : ZERO_PAY → payRate=0
- Règles CCI peuplées à la création du tenant → 10 lignes présentes (5 OUVRIER + 5 CADRE)
- Multi-tenant : les jours de maladie du tenant A n'affectent pas le calcul de phase du tenant B
Ce que QA Validera¶
- N/A (backend uniquement — résultats vérifiés via AB-021 Détail Solde RH et intégration Paie)
Hors Périmètre¶
- Licenciement pour inaptitude (Gestion des Employés V2)
- Calcul des cotisations CNPS (module Paie)
- Personnalisation des tables CCI par tenant (V2)
- Congé maladie partiel / reprise progressive du travail (V2)
- Affichage frontend des informations de phase (AB-021)
Définition de Done¶
- [ ] SickLeaveIndemnizationService implémenté avec la méthode determinePhase()
- [ ] Règles d'indemnisation CCI peuplées via migration Flyway pour les deux catégories (10 lignes)
- [ ] La détermination de phase s'intègre au flux d'approbation (AB-012)
- [ ] LeaveApproved inclut les champs sickLeavePhase et payRate
- [ ] SickLeaveZeroPayAlert publié lorsque ZERO_PAY est atteint
- [ ] 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 — règles et cumuls limités au tenant
- [ ] Pas de types bruts Java — tous les génériques explicites
- [ ] Entrées de piste d'audit pour les transitions de phase