Aller au contenu

Story AB-007: Leave Type Configuration

Module: absence-management Slice: VS-AB-018 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 7 Depends on: AB-002 Can develop concurrently with: AB-006, AB-008 Merge order: Any order with AB-006, AB-008 Estimated complexity: S PRD User Stories: N/A - Technical Story


Objective

Allow HR managers to view and manage leave type configurations (enable/disable types, view properties). System types (ANNUAL, SICK, MATERNITY, etc.) are immutable; tenants can toggle active/inactive for non-system types. Config is cached in Redis.


Backend Scope

Entities & Records

LeaveTypeConfig (from AB-002, tenant DB):

Field Type Constraints
id UUID v7 PK, server-generated
tenant_id UUID NOT NULL
code String NOT NULL, unique per tenant, max 50
name_fr String NOT NULL, max 100
is_paid boolean NOT NULL
requires_document boolean NOT NULL
requires_approval boolean NOT NULL
accrual_based boolean NOT NULL
max_days_per_year BigDecimal Nullable
is_system boolean NOT NULL, default true
is_active boolean NOT NULL, default true
created_at Instant NOT NULL
updated_at Instant NOT NULL

7 CI default leave types (seeded per tenant):

Code name_fr is_paid requires_document requires_approval accrual_based max_days_per_year is_system Can deactivate?
ANNUAL Congé annuel true false true true 26 true No
SICK Congé maladie true true true false null true No
MATERNITY Congé maternité true true true false 98 true No
PATERNITY Congé paternité true false true false 2 true Yes
EXCEPTIONAL Permission exceptionnelle true true true false 10 true Yes
UNPAID Congé sans solde false false true false null true Yes
UNJUSTIFIED Absence injustifiée false false false false null true Yes

DTOs:

public record LeaveTypeConfigResponse(
    UUID id,
    String code,
    String nameFr,
    boolean isPaid,
    boolean requiresDocument,
    boolean requiresApproval,
    boolean accrualBased,
    BigDecimal maxDaysPerYear,
    boolean isSystem,
    boolean isActive
) {}

public record UpdateLeaveTypeConfigRequest(
    @Nullable String nameFr,
    @Nullable Boolean isActive
) {}

Repository Layer

  • LeaveTypeConfigRepository.java:
  • findByTenantId(UUID tenantId) → List<LeaveTypeConfig>
  • findByTenantIdAndCode(UUID tenantId, String code) → Optional<LeaveTypeConfig>

Service Layer

LeaveTypeConfigService.java: - getLeaveTypes(UUID tenantId) → List<LeaveTypeConfig> — Reads from Redis cache (absence:leave-types:{tenantId}), falls back to DB - updateLeaveType(UUID tenantId, String code, UpdateLeaveTypeConfigRequest request) → LeaveTypeConfig — Updates non-system fields, invalidates Redis cache - seedDefaults(UUID tenantId) → void — Creates 7 CI leave types; called at module activation

Cache strategy: - Redis key: absence:leave-types:{tenantId} - TTL: 1 hour - Invalidated on any PATCH to leave types - Cache-aside pattern: read from cache → miss → query DB → populate cache

API Endpoints

Method Path Request Body Response Auth
GET /api/v1/absence/config/leave-types LeaveTypeConfigResponse[] EMPLOYEE (active only), HR_MANAGER (all)
PATCH /api/v1/absence/config/leave-types/:code UpdateLeaveTypeConfigRequest LeaveTypeConfigResponse HR_MANAGER, DG

Validation Rules

  • System types (is_system=true): cannot change is_paid, requires_document, requires_approval, accrual_based, max_days_per_year
  • System types: can only change is_active (to hide from employee UI) and name_fr
  • ANNUAL, SICK, MATERNITY cannot be deactivated (always is_active=true)
  • If attempt to modify protected fields on system type → 422 with "Les types de congé système ne peuvent pas être modifiés"
  • If attempt to deactivate ANNUAL, SICK, or MATERNITY → 422 with "Ce type de congé ne peut pas être désactivé"
  • code path parameter must match an existing leave type for the tenant

Multi-Tenant Considerations

  • LeaveTypeConfig is stored in tenant DB, scoped by tenant_id
  • Redis cache key includes tenantId for isolation
  • EMPLOYEE role sees only is_active=true types; HR_MANAGER sees all
  • Each tenant has its own copy of the 7 default types (independent configuration)

Frontend Scope

Pages & Routes

  • /absences/parametres/types — Leave types section within the settings page (S11 in design spec)

Components

  • LeaveTypeList.tsx — List of leave type cards/rows
  • Each row shows:
  • Icon (leave type icon)
  • name_fr (leave type name)
  • is_paid badge ("Payé" / "Non payé")
  • requires_document indicator
  • requires_approval indicator
  • is_active toggle (Switch component)
  • System types show lock icon — toggle disabled for ANNUAL, SICK, MATERNITY

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Skeleton list (7 rows)
Empty N/A (always seeded with 7 types)
Error Toast on save failure "Impossible de mettre à jour le type de congé."
Offline Read from cache, toggle blocked "Cette action nécessite une connexion internet"
Success Toast on toggle "Paramètres enregistrés"

Responsive Behavior

  • 360px (mobile): Card list, each card shows icon + name + paid badge + toggle. Properties collapsed.
  • 768px (tablet): Same card layout but wider, properties visible inline.
  • 1024px+ (desktop): Table view within sidebar layout: Nom, Payé, Justificatif, Approbation, Actif.

Interactions

  • Section title: "Types de congé"
  • Lock icon tooltip on system types: "Type système — non modifiable"
  • Toggle label for active/inactive: "Actif" / "Inactif"
  • Paid badge: "Payé" (green) / "Non payé" (neutral)
  • Requires document indicator: "Justificatif requis"
  • Requires approval indicator: "Approbation requise"

Acceptance Criteria

AC-021: HR can view all leave types
Given the ABSMGT module is active
When HR calls GET /api/v1/absence/config/leave-types
Then all 7 CI leave types are returned with their properties (name, is_paid, requires_document, etc.)

AC-022: Cannot modify system type properties
Given leave type ANNUAL has is_system=true
When HR tries to PATCH with { isPaid: false }
Then the API returns 422 with error "Les types de congé système ne peuvent pas être modifiés"

AC-023: Custom types can be toggled active/inactive
Given a non-system leave type exists
When HR PATCHes with { isActive: false }
Then the type is deactivated and no longer shown to employees

AC-024: Config is cached in Redis
Given leave types have been loaded from the database
When the same GET request is made again
Then the response is served from Redis cache (no DB query)

OHADA & Regulatory Rules

  • 7 CI leave types per B.1 catalog:
  • ANNUAL: paid, accrual-based, 2.2 jours ouvrables/month
  • SICK: paid per CCI table (100%/75%/50% by seniority bracket)
  • MATERNITY: paid 100%, 14 weeks (98 days)
  • PATERNITY: paid, 2 days
  • EXCEPTIONAL: paid, 10 jours/year cap (permission exceptionnelle)
  • UNPAID: not paid, no accrual
  • UNJUSTIFIED: not paid, disciplinary implications
  • ANNUAL, SICK, MATERNITY are legally mandated — cannot be deactivated

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Entities, §Service layer, §Caching
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Tenant isolation
  • docs/standards/api-guidelines.md — §PATCH semantics, §List endpoints, §Standard envelope
  • docs/standards/react-typescript-guidelines.md — §List components, §Toggle/Switch pattern
  • docs/standards/design-system.md — §Switch, §Card, §Toast components

Testing Requirements

Unit Tests

  • LeaveTypeConfigService: seedDefaults() creates exactly 7 types with correct properties
  • LeaveTypeConfigService: updateLeaveType() rejects modification of system type protected fields
  • LeaveTypeConfigService: updateLeaveType() rejects deactivation of ANNUAL, SICK, MATERNITY
  • LeaveTypeConfigService: getLeaveTypes() reads from Redis cache on cache hit
  • LeaveTypeConfigService: updateLeaveType() invalidates Redis cache after update

Integration Tests

  • GET /api/v1/absence/config/leave-types — returns 7 seeded types
  • GET as EMPLOYEE — returns only is_active=true types
  • PATCH /api/v1/absence/config/leave-types/ANNUAL with { isPaid: false } → 422
  • PATCH /api/v1/absence/config/leave-types/UNPAID with { isActive: false } → 200
  • Tenant isolation: tenant A types not visible to tenant B

What QA Will Validate

  • Leave type list displays correctly on all breakpoints (360px, 768px, 1024px)
  • System types show lock icon and disabled toggle for ANNUAL, SICK, MATERNITY
  • Non-system type toggle works and triggers toast
  • Paid/unpaid badges display correctly
  • Offline state blocks toggle with appropriate message

Out of Scope

  • Creating custom leave types (V2)
  • Modifying system type properties (is_paid, requires_document, etc.)
  • Leave type-specific approval flows
  • Leave type icons/colors customization
  • Per-leave-type accrual rate configuration (uses global rate from AB-002)

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success)
  • [ ] French micro-copy matches design spec exactly
  • [ ] 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) — toggle blocked offline, read from cache
  • [ ] No TypeScript any — all types explicit
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for every data mutation
  • [ ] Redis cache tested (hit, miss, invalidation)