Aller au contenu

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 CachedEmployeeData already exists for (tenantId, employeeId), log warning and return
  • Insert new CachedEmployeeData row with all fields from event, synced_at = Instant.now()
  • Create new LeaveEmployeeProfile with:
    • annual_balance_ouvrables = BigDecimal.ZERO
    • exceptional_days_used_this_period = BigDecimal.ZERO
    • sick_days_current_rolling_12m = BigDecimal.ZERO
    • number_of_children_under_21 = 0
    • reference_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 CachedEmployeeData by (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_id to newManagerId for (tenantId, employeeId)
  • Find all LeaveRequest with tenant_id = tenantId AND approver_id = oldManagerId AND status = 'PENDING_APPROVAL' for the affected employee
  • Update approver_id to newManagerId on 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 tenantId must not be null
  • Event employeeId must not be null
  • ManagerChanged.newManagerId must not be null (old can be null for first manager assignment)
  • All BigDecimal profile fields initialized to BigDecimal.ZERO, never 0.0 or null

Multi-Tenant Considerations

  • All events carry tenantId — every database operation includes tenantId in 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), §@ApplicationModuleListener
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Timestamp types
  • docs/standards/multi-datasource-patterns.md — §Tenant-scoped repositories

Testing Requirements

Unit Tests

  • EmployeeEventListener.on(EmployeeCreated): verify CachedEmployeeData insert + LeaveEmployeeProfile creation with zero balances
  • EmployeeEventListener.on(EmployeeCreated): duplicate event — verify idempotency (no exception, no duplicate row)
  • EmployeeEventListener.on(EmployeeUpdated): verify cache fields updated, synced_at refreshed
  • EmployeeEventListener.on(EmployeeUpdated): employee not in cache — verify error logged, no exception
  • EmployeeEventListener.on(ManagerChanged): verify cache update + pending request approver_id update

Integration Tests

  • Publish EmployeeCreated via ApplicationEventPublisher — verify CachedEmployeeData and LeaveEmployeeProfile persisted in DB
  • Publish ManagerChanged with 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

  • EmployeeTerminated handling (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