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 changeis_paid,requires_document,requires_approval,accrual_based,max_days_per_year - System types: can only change
is_active(to hide from employee UI) andname_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é"
codepath 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
tenantIdfor isolation - EMPLOYEE role sees only
is_active=truetypes; 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_paidbadge ("Payé" / "Non payé")requires_documentindicatorrequires_approvalindicatoris_activetoggle (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, §Cachingdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §Tenant isolationdocs/standards/api-guidelines.md— §PATCH semantics, §List endpoints, §Standard envelopedocs/standards/react-typescript-guidelines.md— §List components, §Toggle/Switch patterndocs/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=truetypes - 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)