Story AB-004 : Scheduled Accrual Job — Logique de base¶
Module : absence-management Slice : VS-AB-004 (part 1) from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 4 Depends on : AB-002, AB-003 Can develop concurrently with : AB-006, AB-007, AB-008 Merge order : Avant AB-005 Estimated complexity : M PRD User Stories : N/A - Technical Story
Objectif¶
Implémenter le cron job mensuel qui crée du journal comptable MONTHLY_ACCRUAL (monthly accrual = comptabilisation mensuelle) pour tous les employés actifs,
accumulant les congés annuels à 2,2 jours ouvrables par mois selon le taux de la Convention Collective Interprofessionnelle (CCI) de Côte d'Ivoire.
C'est le moteur principal qui alimente les soldes de congés et constitue la fondation de tout le système de calcul des soldes.
Périmètre Backend¶
Entités & Records¶
Entités utilisées (définies dans AB-002) :
- LeaveEmployeeProfile — lire last_accrual_date, mettre à jour annual_balance_ouvrables et last_accrual_date
- LeaveBalanceLedger — insérer des entrées MONTHLY_ACCRUAL
- CachedEmployeeData — lire employee_status pour filtrer les employés actifs
Constantes / Enums :
// Accrual rates per CI CCI (Convention Collective Interprofessionnelle)
public static final BigDecimal MONTHLY_ACCRUAL_OUVRABLES = new BigDecimal("2.2"); // jours ouvrables/month
public static final BigDecimal OUVRABLES_TO_CALENDAIRES = new BigDecimal("1.25"); // × 1.25
public static final BigDecimal OUVRABLES_TO_OUVRES = new BigDecimal("0.833333"); // × 5/6
// Transaction types
public enum TransactionType {
MONTHLY_ACCRUAL, SENIORITY_BONUS, CHILDREN_BONUS,
DEDUCTION, CARRY_OVER, MANUAL_ADJUSTMENT
}
Couche Repository¶
LeaveEmployeeProfileRepository(de AB-002) :-
Méthode supplémentaire :
findAllByTenantId(UUID tenantId) -> List<LeaveEmployeeProfile> -
LeaveBalanceLedgerRepository(de AB-002) : -
save(LeaveBalanceLedger entry)— insert standard CrudRepository -
CachedEmployeeDataRepository(de AB-002) : findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData>
Couche Service¶
AccrualScheduler.java — dans le package com.altarys.papillon.modules.absence.scheduler :
@Component
public class AccrualScheduler {
// @Scheduled(cron = "0 0 2 1 * *") // 1st of each month at 02:00
// In this story: single-tenant execution (multi-tenant iteration is AB-005)
public void runMonthlyAccrual() {
// 1. Get current tenant from context (single-tenant for now)
// 2. Find all LeaveEmployeeProfile records for the tenant
// 3. For each profile, check if employee is ACTIVE via CachedEmployeeData
// 4. Call accrualService.processMonthlyAccrual(profile)
}
}
AccrualService.java — dans le package com.altarys.papillon.modules.absence.service :
processMonthlyAccrual(LeaveEmployeeProfile profile):- Vérification d'idempotence : Si
profile.lastAccrualDateest dans le mois en cours ou ultérieur, ignorer (déjà traité). Logger et retourner. - Créer l'entrée de ledger :
leave_type = "ANNUAL"transaction_type = "MONTHLY_ACCRUAL"amount = 2.2(BigDecimal, jours ouvrables — taux CCI)amount_calendaires = 2.2 × 1.25 = 2.75(BigDecimal)amount_ouvres = 2.2 × 5/6 = 1.8333...(BigDecimal, scale 4, HALF_UP)employee_id = profile.employeeIdtenant_id = profile.tenantIdreference_id = null(pas de demande de congé liée)reason = "Acquisition mensuelle - {month}/{year}"created_by = SYSTEM_USER_ID(UUID dédié pour les opérations système)
- Mettre à jour le solde en cache :
profile.annualBalanceOuvrables += 2.2 - Mettre à jour la date d'accumulation :
profile.lastAccrualDate = dernier jour du mois d'accumulation - Sauvegarder le profil et l'entrée de ledger dans la même transaction
- Log :
"Accrual processed for employee {} tenant {}: +2.2 ouvrables", employeeId, tenantId
Formules de calcul (BigDecimal, RoundingMode.HALF_UP, scale 4) :
- Ouvrables → Calendaires : ouvrables × 1.25
- Ouvrables → Ouvres : ouvrables × 5 / 6
- Accumulation mensuelle : 2.2 jours ouvrables (taux CCI, plus favorable que le taux CT de 2.0)
Endpoints API¶
N/A — C'est un job planifié, pas un endpoint API.
Règles de validation¶
- Tous les calculs de jours/montants utilisent
BigDecimalavecRoundingMode.HALF_UPetscale(4)— jamaisdoubleoufloat MONTHLY_ACCRUAL_OUVRABLESdoit être exactementnew BigDecimal("2.2"), pasBigDecimal.valueOf(2.2)(éviter le constructeur à virgule flottante)- Idempotence :
lastAccrualDatedoit être vérifié avant le traitement — si déjà dans le mois en cours, ignorer - Les entrées de ledger sont en ajout uniquement — jamais mettre à jour ou supprimer des entrées existantes
Considérations multi-tenant¶
- Dans cette story (AB-004), le scheduler traite un seul tenant (l'itération multi-tenant est dans AB-005)
- Toutes les requêtes incluent le paramètre
tenantId - Les entrées de ledger incluent le
tenant_idpour l'isolation tenant - La constante
SYSTEM_USER_IDest un UUID bien connu utilisé pour toutes les opérations générées par le système à travers les tenants
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-011: Cron job runs monthly and creates accrual entries
Given a scheduled accrual job triggers at month end
When the job processes active employees
Then one MONTHLY_ACCRUAL ledger entry is created per active employee with amount = 2.2 jours ouvrables
AC-012: Cached balance is updated atomically
Given an accrual entry is inserted into the ledger
When the transaction commits
Then LeaveEmployeeProfile.annual_balance_ouvrables is incremented by 2.2 and last_accrual_date is updated
AC-013: Job is idempotent — no double processing
Given the accrual job has already processed March 2026 for an employee
When the job runs again for March 2026 (duplicate trigger)
Then no additional ledger entry is created (checked via last_accrual_date)
Règles OHADA & réglementaires¶
- FR-ABS-001 (CCI Art. 25.7) : Les congés annuels s'accumulent à raison de 2,2 jours ouvrables par mois de travail effectif. Ce taux CCI est plus favorable que le taux du Code du Travail de 2,0 j/mois, et selon l'Art. 1.3 de la CCI, le taux le plus favorable s'applique.
- FR-ABS-002 (CT Art. 25.2) : L'unité d'accumulation est le jour ouvrable (semaine de 6 jours : lundi à samedi).
- FR-ABS-003 (CT Art. 25.2 + CCI Art. 25.7) : Système de conversion à trois unités :
- Ouvrables (semaine de 6 jours) — unité canonique
- Calendaires (semaine de 7 jours) = ouvrables × 1,25
- Ouvres (semaine de 5 jours) = ouvrables × 5/6
- FR-ABS-005 (CT Art. 25.1) : L'accumulation commence dès le premier jour de travail de l'employé (date d'embauche), pas après une période d'essai.
Standards & conventions¶
docs/standards/java-spring-guidelines.md— §Scheduled jobs, §Transaction boundaries, §Records & sealed typesdocs/standards/database-guidelines.md— §BigDecimal for financial calculations, §Timestamp typesdocs/standards/multi-datasource-patterns.md— §Tenant-scoped repositoriesdocs/standards/api-guidelines.md— §System user ID convention
Exigences de test¶
Tests unitaires¶
AccrualService.processMonthlyAccrual(): vérifier la création d'entrée de ledger avec amount = 2.2, amount_calendaires = 2.75, amount_ouvres = 1.8333AccrualService.processMonthlyAccrual(): vérifier la précision BigDecimal —new BigDecimal("2.2"), pasBigDecimal.valueOf(2.2)AccrualService.processMonthlyAccrual(): vérifier que le solde du profil est incrémenté d'exactement 2,2AccrualService.processMonthlyAccrual(): idempotence — mois déjà traité → ignorer, pas d'entrée crééeAccrualService.processMonthlyAccrual(): vérifier que last_accrual_date est défini au dernier jour du mois d'accumulation
Tests d'intégration¶
- Cycle d'accumulation complet : créer le profil employé → exécuter l'accumulation → vérifier l'entrée de ledger + le solde mis à jour en base
- Idempotence : exécuter l'accumulation deux fois pour le même mois → vérifier exactement 1 entrée de ledger
- Employés multiples : créer 3 profils → exécuter l'accumulation → vérifier 3 entrées de ledger
- Employé inactif (status != ACTIVE) : exécuter l'accumulation → vérifier qu'aucune entrée de ledger n'est créée
Ce que la QA validera¶
- N/A — Pas de fonctionnalité visible par l'utilisateur. Le développeur valide via les tests d'intégration. L'exactitude des soldes sera visible dans les stories d'affichage des soldes (AB-006+).
Hors périmètre¶
- Bonus d'ancienneté (FR-ABS-010 à FR-ABS-012) — calcul en limite de période, story séparée
- Bonus enfants (FR-ABS-013 à FR-ABS-015) — calcul en limite de période, story séparée
- Itération multi-tenant — AB-005
- Rattrapage des mois manqués — AB-005
- Règles de pause/reprise pour les statuts de congé spéciaux — AB-005
- Traitement du report (FR-ABS-022) — story séparée
- Affichage frontend des soldes — story séparée (AB-006+)
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes — N/A (job planifié, 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 — Toutes les requêtes incluent le tenant_id, entrées de ledger taguées avec le 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 — Les entrées de ledger servent de piste d'audit pour les changements de solde