Story AB-003: Employee Event Listeners + Cache Sync¶
Module: absence-management Slice: VS-AB-003 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 3 Depends on: AB-002 Can develop concurrently with: AB-004 Merge order: After AB-002 Estimated complexity: S PRD User Stories: N/A - Technical Story
Objective¶
Set up Spring Modulith event listeners that subscribe to EmployeeCreated, EmployeeUpdated,
and ManagerChanged from the Control Plane, caching employee data locally in the CachedEmployeeData table.
This eliminates compile-time coupling to the organization module and provides zero-latency lookups for leave request
validation and approval routing.
Backend Scope¶
Entities & Records¶
CachedEmployeeData — defined in AB-002, used here.
Events consumed (published by Control Plane module):
// These records are defined in the Control Plane module's public API
public record EmployeeCreated(
UUID tenantId, UUID employeeId, String matricule,
String firstName, String lastName, String category,
LocalDate hireDate, UUID orgUnitId, UUID managerId
) {}
public record EmployeeUpdated(
UUID tenantId, UUID employeeId,
String firstName, String lastName, String category,
LocalDate hireDate, UUID orgUnitId, String status
) {}
public record ManagerChanged(
UUID tenantId, UUID employeeId,
UUID oldManagerId, UUID newManagerId,
LocalDate effectiveDate
) {}
Repository Layer¶
CachedEmployeeDataRepository— defined in AB-002:findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData>-
Additional method needed:
existsByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> boolean -
LeaveEmployeeProfileRepository— defined in AB-002: -
findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<LeaveEmployeeProfile> -
LeaveRequestRepository— defined in AB-002: - Additional method needed:
findByTenantIdAndApproverIdAndStatus(UUID tenantId, UUID approverId, String status) -> List<LeaveRequest>
Service Layer¶
EmployeeEventListener.java — @ApplicationModuleListener in package com.altarys.papillon.modules.absence.listener:
@ApplicationModuleListener void on(EmployeeCreated event):- Check idempotency: if
CachedEmployeeDataalready exists for (tenantId, employeeId), log warning and return - Insert new
CachedEmployeeDatarow with all fields from event,synced_at = Instant.now() - Create new
LeaveEmployeeProfilewith:annual_balance_ouvrables = BigDecimal.ZEROexceptional_days_used_this_period = BigDecimal.ZEROsick_days_current_rolling_12m = BigDecimal.ZEROnumber_of_children_under_21 = 0reference_period_start/reference_period_end= current calendar year boundaries (Jan 1 to Dec 31)
-
Log:
"Employee cached and profile created: {} for tenant {}", employeeId, tenantId -
@ApplicationModuleListener void on(EmployeeUpdated event): - Find existing
CachedEmployeeDataby (tenantId, employeeId) — if not found, log error and return - Update fields:
firstName,lastName,category,hireDate,orgUnitId,employeeStatus - Set
synced_at = Instant.now() - Save updated entity
-
Log:
"Employee cache updated: {} for tenant {}", employeeId, tenantId -
@ApplicationModuleListener void on(ManagerChanged event): - Update
CachedEmployeeData.current_manager_idtonewManagerIdfor (tenantId, employeeId) - Find all
LeaveRequestwithtenant_id = tenantId AND approver_id = oldManagerId AND status = 'PENDING_APPROVAL'for the affected employee - Update
approver_idtonewManagerIdon each pending request - Set
synced_at = Instant.now()on cache entry - Log:
"Manager changed for employee {}: {} -> {}. Updated {} pending requests", employeeId, oldManagerId, newManagerId, count
API Endpoints¶
N/A — No endpoints in this slice. This is an event-driven internal service.
Validation Rules¶
- Event
tenantIdmust not be null - Event
employeeIdmust not be null ManagerChanged.newManagerIdmust not be null (old can be null for first manager assignment)- All BigDecimal profile fields initialized to
BigDecimal.ZERO, never0.0ornull
Multi-Tenant Considerations¶
- All events carry
tenantId— every database operation includestenantIdin the query - Event listeners process within the tenant context of the event (no TenantContext ScopedValue needed — tenant_id comes from the event payload directly)
- Cross-module communication uses Spring Modulith Application Events — no direct method calls to Control Plane module
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-007: Employee created event triggers cache insert
Given an EmployeeCreated is published by the Control Plane
When the Absence module listener receives it
Then a CachedEmployeeData row is inserted AND a LeaveEmployeeProfile is created with annual_balance_ouvrables = 0
AC-008: Employee updated event updates cache
Given a CachedEmployeeData row exists for an employee
When an EmployeeUpdated is received with updated category or hire_date
Then the cached row is updated with the new values and synced_at is set to now
AC-009: Manager change updates approval routing
Given employees have PENDING_APPROVAL leave requests with approver_id = old_manager
When a ManagerChanged is received
Then current_manager_id in cache is updated AND approver_id on pending requests is updated to new_manager
AC-010: Listener is idempotent
Given the same EmployeeCreated is published twice (duplicate)
When the listener processes both
Then only one CachedEmployeeData row exists (no duplicate, no error)
OHADA & Regulatory Rules¶
N/A — This is an infrastructure/integration story. OHADA rules apply to the business logic that uses the cached data (AB-004+), not the caching mechanism itself.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Spring Modulith boundaries (Application Events for inter-module communication, never direct method calls), §@ApplicationModuleListenerdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §Timestamp typesdocs/standards/multi-datasource-patterns.md— §Tenant-scoped repositories
Testing Requirements¶
Unit Tests¶
EmployeeEventListener.on(EmployeeCreated): verify CachedEmployeeData insert + LeaveEmployeeProfile creation with zero balancesEmployeeEventListener.on(EmployeeCreated): duplicate event — verify idempotency (no exception, no duplicate row)EmployeeEventListener.on(EmployeeUpdated): verify cache fields updated, synced_at refreshedEmployeeEventListener.on(EmployeeUpdated): employee not in cache — verify error logged, no exceptionEmployeeEventListener.on(ManagerChanged): verify cache update + pending request approver_id update
Integration Tests¶
- Publish
EmployeeCreatedviaApplicationEventPublisher— verify CachedEmployeeData and LeaveEmployeeProfile persisted in DB - Publish
ManagerChangedwith existing PENDING_APPROVAL requests — verify approver_id updated in DB - Publish duplicate
EmployeeCreated— verify single row, no constraint violation error
What QA Will Validate¶
- N/A — No user-facing functionality. Developer validates event handling via integration tests.
Out of Scope¶
EmployeeTerminatedhandling (AB-021 or later story)EmployeeReassigned(V2)- Direct API calls to the Control Plane module (violates Spring Modulith boundaries)
- Frontend display of cached employee data
- Bulk import of existing employees into cache (separate migration story)
Definition of Done¶
- [ ] All backend endpoints implemented and returning correct responses — N/A (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 — Events carry tenant_id, all queries scoped by 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 — LeaveEmployeeProfile creation and manager change updates logged