Aller au contenu

Story AB-004: Scheduled Accrual Job — Core Logic

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: Before AB-005 Estimated complexity: M PRD User Stories: N/A - Technical Story


Objective

Implement the monthly cron job that creates MONTHLY_ACCRUAL ledger entries for all active employees, accruing annual leave at 2.2 jours ouvrables per month per CI Convention Collective Interprofessionnelle (CCI) rate. This is the core engine that populates leave balances and is the foundation of the entire balance calculation system.


Backend Scope

Entities & Records

Entities used (defined in AB-002): - LeaveEmployeeProfile — read last_accrual_date, update annual_balance_ouvrables and last_accrual_date - LeaveBalanceLedger — insert MONTHLY_ACCRUAL entries - CachedEmployeeData — read employee_status to filter active employees

Constants / 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
}

Repository Layer

  • LeaveEmployeeProfileRepository (from AB-002):
  • Additional method: findAllByTenantId(UUID tenantId) -> List<LeaveEmployeeProfile>

  • LeaveBalanceLedgerRepository (from AB-002):

  • save(LeaveBalanceLedger entry) — standard CrudRepository insert

  • CachedEmployeeDataRepository (from AB-002):

  • findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData>

Service Layer

AccrualScheduler.java — in 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 — in package com.altarys.papillon.modules.absence.service:

  • processMonthlyAccrual(LeaveEmployeeProfile profile):
  • Idempotency check: If profile.lastAccrualDate is in the current month or later, skip (already processed). Log and return.
  • Create ledger entry:
    • leave_type = "ANNUAL"
    • transaction_type = "MONTHLY_ACCRUAL"
    • amount = 2.2 (BigDecimal, jours ouvrables — CCI rate)
    • 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 (no linked leave request)
    • reason = "Acquisition mensuelle - {month}/{year}"
    • created_by = SYSTEM_USER_ID (dedicated UUID for system operations)
  • Update cached balance: profile.annualBalanceOuvrables += 2.2
  • Update accrual date: profile.lastAccrualDate = last day of accrual month
  • Save profile and ledger entry in same transaction
  • Log: "Accrual processed for employee {} tenant {}: +2.2 ouvrables", employeeId, tenantId

Calculation formulas (BigDecimal, RoundingMode.HALF_UP, scale 4): - Ouvrables → Calendaires: ouvrables × 1.25 - Ouvrables → Ouvres: ouvrables × 5 / 6 - Monthly accrual: 2.2 jours ouvrables (CCI rate, more favorable than CT's 2.0)

API Endpoints

N/A — This is a scheduled job, not an API endpoint.

Validation Rules

  • All day/amount calculations use BigDecimal with RoundingMode.HALF_UP and scale(4) — never double or float
  • MONTHLY_ACCRUAL_OUVRABLES must be exactly new BigDecimal("2.2"), not BigDecimal.valueOf(2.2) (avoid floating-point constructor)
  • Idempotency: lastAccrualDate must be checked before processing — if already in current month, skip
  • Ledger entries are append-only — never update or delete existing entries

Multi-Tenant Considerations

  • In this story (AB-004), the scheduler processes a single tenant only (multi-tenant iteration is AB-005)
  • All queries include tenantId parameter
  • Ledger entries include tenant_id for tenant isolation
  • The SYSTEM_USER_ID constant is a well-known UUID used for all system-generated operations across tenants

Frontend Scope

N/A — Backend infrastructure story.

Pages & Routes

N/A

Components

N/A

UI States (ALL REQUIRED)

N/A — Backend only.

Responsive Behavior

N/A

Interactions

N/A


Acceptance Criteria

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)

OHADA & Regulatory Rules

  • FR-ABS-001 (CCI Art. 25.7): Annual leave accrues at 2.2 jours ouvrables per month of effective work. This CCI rate is more favorable than the Code du Travail rate of 2.0 j/month, and per CCI Art. 1.3, the more favorable rate applies.
  • FR-ABS-002 (CT Art. 25.2): The accrual unit is jours ouvrables (6-day working week: Monday to Saturday).
  • FR-ABS-003 (CT Art. 25.2 + CCI Art. 25.7): Three-unit conversion system:
  • Ouvrables (6-day week) — canonical unit
  • Calendaires (7-day week) = ouvrables × 1.25
  • Ouvres (5-day week) = ouvrables × 5/6
  • FR-ABS-005 (CT Art. 25.1): Accrual starts from the employee's first day of work (hire date), not after a probation period.

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

Testing Requirements

Unit Tests

  • AccrualService.processMonthlyAccrual(): verify ledger entry created with amount = 2.2, amount_calendaires = 2.75, amount_ouvres = 1.8333
  • AccrualService.processMonthlyAccrual(): verify BigDecimal precision — new BigDecimal("2.2"), not BigDecimal.valueOf(2.2)
  • AccrualService.processMonthlyAccrual(): verify profile balance incremented by exactly 2.2
  • AccrualService.processMonthlyAccrual(): idempotency — already processed month → skip, no entry created
  • AccrualService.processMonthlyAccrual(): verify last_accrual_date set to last day of accrual month

Integration Tests

  • Full accrual cycle: create employee profile → run accrual → verify ledger entry + updated balance in DB
  • Idempotency: run accrual twice for same month → verify exactly 1 ledger entry
  • Multiple employees: create 3 profiles → run accrual → verify 3 ledger entries
  • Inactive employee (status != ACTIVE): run accrual → verify no ledger entry created

What QA Will Validate

  • N/A — No user-facing functionality. Developer validates via integration tests. Balance correctness will be visible in balance display stories (AB-006+).

Out of Scope

  • Seniority bonus (FR-ABS-010 to FR-ABS-012) — period-start calculation, separate story
  • Children bonus (FR-ABS-013 to FR-ABS-015) — period-start calculation, separate story
  • Multi-tenant iteration — AB-005
  • Catch-up for missed months — AB-005
  • Pause/continue rules for special leave statuses — AB-005
  • Carry-over processing (FR-ABS-022) — separate story
  • Frontend balance display — separate story (AB-006+)

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses — N/A (scheduled job, no endpoints)
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px — N/A (backend only)
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success) — N/A
  • [ ] French micro-copy matches design spec exactly — N/A
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified — All queries include tenant_id, ledger entries tagged with tenant_id
  • [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — N/A (backend only)
  • [ ] No TypeScript any — N/A
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format — N/A (no endpoints)
  • [ ] Audit trail entries for every data mutation — Ledger entries serve as audit trail for balance changes