Aller au contenu

Story AB-022: Employee Termination Handler (Backend)

Module: absence-management Slice: VS-AB-016 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 22 Depends on: AB-009, AB-010 Concurrent with: AB-020, AB-021 Merge order: Any order Estimated complexity: M PRD User Stories: US-ABS-010


Objective

Listen to EmployeeTerminated and EmployeeOffboarded from Employee Management / Control Plane, auto-cancel all pending leave requests, preserve approved leaves within the notice period, compute the final leave balance, and publish a LeaveBalanceSettlement to Payroll for indemnite compensatrice de conges payes calculation. This ensures proper leave balance settlement upon employee departure as required by Ivorian labor law.


Backend Scope

Entities & Records

No new entities. This story adds a new ledger transaction type and inter-module events.

Ledger entry transaction type (extend enum):

Value Description
SETTLEMENT Final balance zeroing upon employee termination — negative amount equal to remaining balance

Events consumed:

// From Employee Management / Control Plane module
public record EmployeeTerminated(
    UUID employeeId,
    UUID tenantId,
    LocalDate terminationDate,
    LocalDate noticeEndDate
) {}

public record EmployeeOffboarded(
    UUID employeeId,
    UUID tenantId,
    LocalDate effectiveDate
) {}

Events published:

public record LeaveBalanceSettlement(
    UUID employeeId,
    UUID tenantId,
    BigDecimal remainingBalance,     // jours calendaires
    LocalDate lastAccrualDate,
    LocalDate terminationDate
) {}

DTOs:

public record LeaveBalanceSettlement(
    UUID employeeId,
    UUID tenantId,
    BigDecimal remainingBalanceCalendaires,
    LocalDate lastAccrualDate,
    LocalDate terminationDate,
    int cancelledPendingCount,
    int cancelledFutureApprovedCount,
    int preservedApprovedCount
) {}

Repository Layer

No new repositories. Uses existing repositories from AB-009 and AB-010: - LeaveEmployeeProfile repository (balance queries) - LeaveRequest repository (status updates for cancellation) - Ledger repository (SETTLEMENT entry creation)

Service Layer

EmployeeTerminationListener.java — @ApplicationModuleListener: - On EmployeeTerminated: 1. Cancel all PENDING_APPROVAL and PENDING_DOCUMENT leave requests for the employee with reason "Depart de l'employe" and status CANCELLED_BY_HR 2. For APPROVED future leaves: preserve those where start_date <= noticeEndDate; cancel those where start_date > noticeEndDate with reason "Depart de l'employe" 3. Delegate to TerminationSettlementService for balance computation - On EmployeeOffboarded: 1. Same logic as EmployeeTerminated but uses effectiveDate as the cutoff (no notice period distinction — all future leaves after effectiveDate are cancelled) 2. Delegate to TerminationSettlementService for balance computation

TerminationSettlementService.java: - computeSettlement(UUID tenantId, UUID employeeId, LocalDate terminationDate) -> LeaveBalanceSettlement: 1. Compute remaining annual balance from ledger SUM (jours ouvrables) 2. Convert remaining balance to jours calendaires for settlement event 3. Create SETTLEMENT ledger entry with negative amount equal to remaining balance (zeroes out balance) 4. Update cached annual_balance_ouvrables to 0 5. Publish LeaveBalanceSettlement to Payroll module via Application Event 6. Publish AuditEntryRequested event with settlement details 7. Return settlement summary

API Endpoints

No new API endpoints. This story is entirely event-driven (inter-module communication via Spring Modulith Application Events).

Validation Rules

  • Events must carry valid tenant_id and employee_id
  • Employee must exist in the absence module's LeaveEmployeeProfile for the given tenant
  • Settlement amount must be computed using BigDecimal (never double/float)
  • SETTLEMENT ledger entry amount must exactly zero out the remaining balance
  • Idempotency: if a settlement has already been processed for this employee (SETTLEMENT entry exists), skip re-processing and log a warning

Multi-Tenant Considerations

  • Events carry tenant_id — all operations scoped to the terminated employee's tenant
  • Leave request cancellation queries filtered by tenant_id + employee_id
  • Ledger operations use tenant-scoped pessimistic lock pattern from AB-012
  • No cross-tenant data access: settlement only affects the employee's data within their tenant

Flyway Migrations

No new migrations. SETTLEMENT transaction type is already part of the enum from AB-012 ledger setup.


Frontend Scope

Pages & Routes

N/A — Backend-only slice. Settlement results are visible in the HR employee balance detail (AB-021) if the employee record is viewed before full offboarding cleanup.

Components

N/A

UI States (ALL REQUIRED)

N/A — Backend-only slice.

Responsive Behavior

N/A

Interactions

N/A


Acceptance Criteria

AC-070: Pending requests auto-cancelled on termination
Given an employee has 2 PENDING_APPROVAL and 1 PENDING_DOCUMENT leave requests
When an EmployeeTerminated is received for that employee
Then all 3 requests are set to CANCELLED_BY_HR with reason "Depart de l'employe"
And an AuditEntryRequested event is published for each cancellation

AC-071: Approved future leaves handled correctly
Given an employee is terminated with terminationDate=2026-04-30 and noticeEndDate=2026-05-15
And the employee has APPROVED leaves: one starting 2026-05-10 (within notice) and one starting 2026-06-01 (after notice)
When the EmployeeTerminated is received
Then the 2026-05-10 leave is preserved (remains APPROVED)
And the 2026-06-01 leave is set to CANCELLED_BY_HR with reason "Depart de l'employe"

AC-072: Settlement event published to Payroll
Given an employee is terminated and has a remaining annual balance of 12.5 jours ouvrables
When the settlement is computed
Then a SETTLEMENT ledger entry is created with amount=-12.5 (zeroing the balance)
And a LeaveBalanceSettlement is published with remainingBalance in jours calendaires
And the cached annual_balance_ouvrables is set to 0

OHADA & Regulatory Rules

  • FR-ABS-024: Upon termination, remaining leave balance MUST be paid out as indemnite compensatrice de conges payes. This is mandatory per Ivorian labor law — the employer cannot forfeit unused leave days.
  • Art. 25.9 CT 2015: Compensatory payment is due on any form of contract rupture (resignation, dismissal, mutual agreement, end of fixed-term contract).
  • FR-ABS-111: EmployeeTerminated event handling specification — absence module must react to lifecycle events from Employee Management.
  • FR-ABS-120: SETTLEMENT is a valid ledger transaction type.
  • FR-ABS-123: Immutable ledger — SETTLEMENT entry cannot be modified after creation.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — @ApplicationModuleListener, event publishing, Spring Modulith boundaries
  • docs/standards/database-guidelines.md — BigDecimal for balance calculations, tenant isolation, ledger append-only pattern
  • docs/standards/api-guidelines.md — N/A (no API endpoints in this story)
  • docs/standards/multi-datasource-patterns.md — Tenant-scoped data access, pessimistic locking for balance mutation
  • docs/standards/offline-sync-guidelines.md — N/A (backend-only)

Testing Requirements

Unit Tests

  • EmployeeTerminationListener: cancel all PENDING_APPROVAL requests on termination
  • EmployeeTerminationListener: cancel all PENDING_DOCUMENT requests on termination
  • EmployeeTerminationListener: preserve APPROVED leaves within notice period
  • EmployeeTerminationListener: cancel APPROVED leaves after notice end date
  • EmployeeTerminationListener: handle EmployeeOffboarded (no notice period distinction)
  • TerminationSettlementService: compute correct remaining balance from ledger
  • TerminationSettlementService: create SETTLEMENT entry that zeroes balance
  • TerminationSettlementService: convert ouvrables to calendaires for settlement event
  • TerminationSettlementService: idempotency — skip if SETTLEMENT already exists

Integration Tests

  • Full flow: publish EmployeeTerminated, verify pending requests cancelled, settlement created, event published
  • Full flow: publish EmployeeOffboarded, verify same behavior with effectiveDate cutoff
  • Tenant isolation: termination event for tenant A does not affect tenant B employees
  • Idempotency: processing same termination event twice does not create duplicate SETTLEMENT entries
  • Verify LeaveBalanceSettlement payload contains correct remainingBalance in jours calendaires

What QA Will Validate

  • After termination event: verify in AB-021 (HR balance detail) that balance is 0 and SETTLEMENT entry is visible in ledger
  • Verify cancelled requests show reason "Depart de l'employe" in request history
  • Verify preserved leaves (within notice period) remain visible and APPROVED

Out of Scope

  • Calculating the monetary value of the settlement — Payroll module responsibility (consumes LeaveBalanceSettlement)
  • Licenciement pour inaptitude procedures — Employee Management V2
  • Early return handling for in-progress leaves at termination — V2 (current MVP assumes leave must end before termination effective date or is handled manually)
  • Partial settlement (prorating for mid-month termination) — V2
  • Employee reinstatement (reversing a settlement) — V2
  • Frontend notification of termination settlement — HR sees results in AB-021 balance detail

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses — N/A (event-driven, no endpoints)
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px — N/A (backend-only)
  • [ ] All 5 UI states implemented — N/A (backend-only)
  • [ ] French micro-copy matches design spec exactly — N/A (backend-only, cancellation reason is hardcoded French string)
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified (no cross-tenant data leaks)
  • [ ] Offline write+sync works (Papillon) — N/A (backend-only)
  • [ ] No TypeScript any — N/A (backend-only)
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format — N/A (no API endpoints)
  • [ ] Audit trail entries created for every data mutation