Aller au contenu

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.lastAccrualDate est 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.employeeId
    • tenant_id = profile.tenantId
    • reference_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 BigDecimal avec RoundingMode.HALF_UP et scale(4) — jamais double ou float
  • MONTHLY_ACCRUAL_OUVRABLES doit être exactement new BigDecimal("2.2"), pas BigDecimal.valueOf(2.2) (éviter le constructeur à virgule flottante)
  • Idempotence : lastAccrualDate doit ê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_id pour l'isolation tenant
  • La constante SYSTEM_USER_ID est 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 types
  • docs/standards/database-guidelines.md — §BigDecimal for financial calculations, §Timestamp types
  • docs/standards/multi-datasource-patterns.md — §Tenant-scoped repositories
  • docs/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.8333
  • AccrualService.processMonthlyAccrual() : vérifier la précision BigDecimal — new BigDecimal("2.2"), pas BigDecimal.valueOf(2.2)
  • AccrualService.processMonthlyAccrual() : vérifier que le solde du profil est incrémenté d'exactement 2,2
  • AccrualService.processMonthlyAccrual() : idempotence — mois déjà traité → ignorer, pas d'entrée créée
  • AccrualService.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