Aller au contenu

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 (OUVRIER ou CADRE)
  • Calculer l'ancienneté en années depuis hire_date
  • Rechercher la règle CCI dans SickLeaveIndemnizationRuleRepository par (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 * 30FULL_PAY (payRate=100)
    • Sinon si cumulativeDays <= (fullPayMonths + halfPayMonths) * 30HALF_PAY (payRate=50)
    • Sinon → ZERO_PAY (payRate=0)
  • Mettre à jour LeaveEmployeeProfile.sick_leave_phase et sick_days_current_rolling_12m
  • Si phase == ZERO_PAY → publier SickLeaveZeroPayAlert pour 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 de LeaveApproved

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 IllegalStateException et 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 tenantId depuis ScopedValue<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 LeaveBalanceLedger limité 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 enums
  • docs/standards/database-guidelines.md — §Pattern repository multi-tenant, §Isolation tenant dans chaque requête, §Patterns de données de peuplement
  • docs/standards/api-guidelines.md — §Communication inter-services via Application Events
  • docs/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=4m
  • SickLeaveIndemnizationService.determinePhase() : OUVRIER + 3 ans ancienneté → recherche (OUVRIER, 1-5) → fullPay=1m, halfPay=3m
  • SickLeaveIndemnizationService.determinePhase() : cumul=25 jours, fullPay=1m(30j) → FULL_PAY
  • SickLeaveIndemnizationService.determinePhase() : cumul=35 jours, fullPay=1m(30j), halfPay=3m(90j) → HALF_PAY
  • SickLeaveIndemnizationService.determinePhase() : cumul=125 jours, fullPay=1m(30j), halfPay=3m(90j) → ZERO_PAY
  • SickLeaveIndemnizationService.determinePhase() : cumul=90 jours exactement (limite), fullPay=3m(90j) → FULL_PAY (inclusif)
  • SickLeaveIndemnizationService.determinePhase() : cumul=91 jours, fullPay=3m(90j) → HALF_PAY
  • SickLeaveIndemnizationService.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