Story AB-008: Public Holidays + Exceptional Leave Config¶
@assignee: @joel23p
Module: absence-management Slice: VS-AB-019 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 8 Depends on: AB-002 Can develop concurrently with: AB-006, AB-007 Merge order: Any order with AB-006, AB-007 Estimated complexity: S PRD User Stories: US-ABS-007
Objective¶
Allow HR managers to view and manage public holidays (fixed holidays are read-only; variable Islamic holidays can have tenant-specific date overrides) and configure exceptional leave (permission exceptionnelle) durations above the CCI minimums. Both are seeded with CI defaults at tenant creation.
Backend Scope¶
Entities & Records¶
PublicHoliday (from AB-002, tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| country_code | String | NOT NULL, default "CI", max 2 |
| year | int | NOT NULL |
| holiday_code | String | NOT NULL, max 50 (e.g., "NEW_YEAR", "EID_FITR") |
| name_fr | String | NOT NULL, max 100 |
| date | LocalDate | NOT NULL |
| is_fixed | boolean | NOT NULL |
| source | HolidaySource enum | NOT NULL |
| created_at | Instant | NOT NULL |
| updated_at | Instant | NOT NULL |
HolidaySource enum:
14 CI public holidays:
| Code | name_fr | is_fixed | Default date (2026) |
|---|---|---|---|
| NEW_YEAR | Jour de l'An | true | 01/01 |
| LABOUR_DAY | Fête du Travail | true | 01/05 |
| INDEPENDENCE_DAY | Fête de l'Indépendance | true | 07/08 |
| ASSUMPTION | Assomption | true | 15/08 |
| ALL_SAINTS | Toussaint | true | 01/11 |
| PEACE_DAY | Journée de la Paix | true | 15/11 |
| CHRISTMAS | Noël | true | 25/12 |
| EASTER_MONDAY | Lundi de Pâques | false | variable |
| ASCENSION | Ascension | false | variable |
| WHIT_MONDAY | Lundi de Pentecôte | false | variable |
| EID_FITR | Aïd el-Fitr | false | variable |
| EID_ADHA | Aïd el-Adha | false | variable |
| MAWLID | Mawlid (Naissance du Prophète) | false | variable |
| LAYLAT_AL_QADR | Laylat al-Qadr (Nuit du Destin) | false | variable |
ExceptionalLeaveConfig (from AB-002, tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| event_type_code | String | NOT NULL, unique per tenant, max 50 |
| name_fr | String | NOT NULL, max 100 |
| duration_days | int | NOT NULL, >= CCI minimum |
| cci_minimum_days | int | NOT NULL (immutable reference) |
| created_at | Instant | NOT NULL |
| updated_at | Instant | NOT NULL |
9 CCI exceptional leave event types (seeded per tenant):
| Code | name_fr | CCI minimum (days) |
|---|---|---|
| MARRIAGE_SELF | Mariage du salarié | 4 |
| MARRIAGE_CHILD | Mariage d'un enfant | 2 |
| BIRTH_CHILD | Naissance d'un enfant | 2 |
| DEATH_SPOUSE | Décès du conjoint | 5 |
| DEATH_CHILD | Décès d'un enfant | 5 |
| DEATH_PARENT | Décès père/mère | 5 |
| DEATH_SIBLING | Décès frère/sœur | 2 |
| DEATH_IN_LAW | Décès beau-père/belle-mère | 2 |
| MOVE | Déménagement | 2 |
DTOs:
public record PublicHolidayResponse(
UUID id,
String holidayCode,
String nameFr,
LocalDate date,
boolean isFixed,
String source
) {}
public record UpdatePublicHolidayRequest(
@NotBlank String countryCode,
@NotNull int year,
@NotBlank String holidayCode,
@NotNull LocalDate date
) {}
public record ExceptionalLeaveConfigResponse(
UUID id,
String eventTypeCode,
String nameFr,
int durationDays,
int cciMinimumDays
) {}
public record UpdateExceptionalLeaveRequest(
@NotNull int durationDays
) {}
Repository Layer¶
PublicHolidayRepository.java:findByTenantIdAndCountryCodeAndYear(UUID tenantId, String countryCode, int year) → List<PublicHoliday>- Custom query to merge platform defaults with tenant overrides (tenant override takes precedence for same
holiday_code) ExceptionalLeaveConfigRepository.java:findByTenantId(UUID tenantId) → List<ExceptionalLeaveConfig>findByTenantIdAndEventTypeCode(UUID tenantId, String eventTypeCode) → Optional<ExceptionalLeaveConfig>
Service Layer¶
HolidayConfigService.java:
- getEffectiveHolidays(UUID tenantId, int year) → List<PublicHoliday> — Merges platform defaults with tenant overrides. Applies Sunday substitution rule (FR-ABS-103): if a holiday falls on Sunday, Monday is the observed date.
- updateVariableHoliday(UUID tenantId, int year, String holidayCode, LocalDate newDate) → PublicHoliday — Creates a TENANT_OVERRIDE record for variable holidays only. Rejects fixed holidays.
- seedHolidays(UUID tenantId, int year) → void — Seeds CI fixed holidays + platform variable dates for the given year.
ExceptionalLeaveConfigService.java:
- getConfigs(UUID tenantId) → List<ExceptionalLeaveConfig> — Returns all 9 event types with tenant-configured durations.
- updateDuration(UUID tenantId, String eventTypeCode, int durationDays) → ExceptionalLeaveConfig — Validates durationDays >= cci_minimum_days, updates, publishes ExceptionalLeaveConfigUpdated event.
- seedDefaults(UUID tenantId) → void — Seeds 9 event types with CCI minimums.
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/absence/config/public-holidays?year=2026 | — | PublicHolidayResponse[] | All (read-only) |
| PATCH | /api/v1/absence/config/public-holidays | UpdatePublicHolidayRequest | PublicHolidayResponse | HR_MANAGER, DG |
| GET | /api/v1/absence/config/exceptional-leave | — | ExceptionalLeaveConfigResponse[] | All (read-only) |
| PATCH | /api/v1/absence/config/exceptional-leave/:eventTypeCode | UpdateExceptionalLeaveRequest | ExceptionalLeaveConfigResponse | HR_MANAGER, DG |
Validation Rules¶
- Fixed holidays (
is_fixed=true) cannot be overridden — PATCH blocked with 422 - Variable holiday dates must be valid dates for the given year
- Exceptional leave
durationDaysmust be >= CCI minimum: - MARRIAGE_SELF >= 4, MARRIAGE_CHILD >= 2, BIRTH_CHILD >= 2
- DEATH_SPOUSE >= 5, DEATH_CHILD >= 5, DEATH_PARENT >= 5
- DEATH_SIBLING >= 2, DEATH_IN_LAW >= 2, MOVE >= 2
- If
durationDays< CCI minimum → 422 with "La durée minimale légale (CCI) pour cet événement est de {min} jours." yearquery parameter is required for holidays endpointholidayCodemust match an existing variable holiday
Multi-Tenant Considerations¶
- Both entities stored in tenant DB, scoped by
tenant_id - Platform defaults are seeded per tenant (not shared across tenants)
- Tenant overrides only affect the specific tenant
- Holiday list is year-specific — tenants may override variable dates differently each year
Frontend Scope¶
Pages & Routes¶
/absences/parametres/jours-feries— Public Holidays page (S12 in design spec)- Exceptional leave section within
/absences/parametres/types(S11 settings page)
Components¶
Public Holidays Page:
- PublicHolidaysPage.tsx — Year navigation (prev/next arrows), two sections: fixed + variable
- FixedHolidayRow.tsx — Read-only row: name, date, lock icon
- VariableHolidayRow.tsx — Editable row: name, DatePicker for date, platform default shown as helper text
- Year selector with prev/next navigation
Exceptional Leave Section (within settings page):
- ExceptionalLeaveConfigSection.tsx — List of 9 event types
- Each row: event name, inline Input (type=number) for duration, CCI minimum as helper text
- Save button per row or global save
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton list of holidays / event types | — |
| Empty | N/A (always seeded) | — |
| Error | Toast on save | "Impossible d'enregistrer. Réessayez." |
| Error (CCI min) | Inline error below duration input | "La durée minimale légale (CCI) pour cet événement est de {min} jours." |
| Offline | Read from cache, edits blocked | "Cette action nécessite une connexion internet" |
| Success | Toast | "Paramètres enregistrés" |
Responsive Behavior¶
- 360px (mobile): Single column, holidays stacked vertically, date pickers full-width
- 768px (tablet): Two-column layout for fixed holidays
- 1024px+ (desktop): Card container within sidebar layout, table view for holidays
Interactions¶
Public Holidays: - Page title: "Jours fériés — {year}" - Fixed section header: "Jours fériés fixes" - Variable section header: "Jours fériés variables" - Lock icon tooltip on fixed holidays: "Non modifiable — fixé par la loi" - Platform default helper text below DatePicker: "Défaut plateforme : {date}" - Info text at top: "Les jours fériés fixes sont définis par la loi. Vous pouvez modifier les dates des fêtes religieuses variables." - Year navigation: "< 2025 | 2026 | 2027 >"
Exceptional Leave: - Section title: "Permissions exceptionnelles" - Duration label per row: "Durée : [{n}] jours (min : {min})" - Info text: "Les durées minimales sont fixées par la CCI. Vous pouvez les augmenter mais pas les réduire." - Event type names (French): - Mariage du salarié - Mariage d'un enfant - Naissance d'un enfant - Décès du conjoint - Décès d'un enfant - Décès père/mère - Décès frère/sœur - Décès beau-père/belle-mère - Déménagement
Acceptance Criteria¶
AC-025: List fixed and variable holidays per year
Given the ABSMGT module is active for a CI tenant
When anyone calls GET /api/v1/absence/config/public-holidays?year=2026
Then both fixed (7) and variable (7) holidays are returned with their dates for 2026
AC-026: HR can override variable holiday dates
Given Aïd el-Fitr has a platform default date of 30/03/2026
When HR PATCHes the public-holidays endpoint with { holidayCode: "EID_FITR", date: "2026-03-31" }
Then a TENANT_OVERRIDE record is created and the effective holiday list for this tenant shows 31/03
AC-027: Exceptional leave durations enforce CCI minimum
Given the CCI minimum for "Mariage du salarié" is 4 jours
When the tenant tries to configure a duration of 3 jours
Then the API returns 422 with "La durée minimale légale (CCI) pour cet événement est de 4 jours."
AC-028: Seeded at tenant creation with CI defaults
Given a new CI tenant is created
When the ABSMGT module initializes
Then 14 public holidays (7 fixed + 7 variable) and 9 exceptional leave event types are seeded with CCI defaults
OHADA & Regulatory Rules¶
- FR-ABS-100: 10 fixed CI public holidays (7 fixed-date + 3 variable Christian: Easter Monday, Ascension, Whit Monday).
- FR-ABS-101: 4 variable Islamic holidays (Aïd el-Fitr, Aïd el-Adha, Mawlid, Laylat al-Qadr).
- FR-ABS-102: Two-level management — platform admin sets defaults, tenant can override variable dates.
- FR-ABS-103: Sunday substitution rule — if a holiday falls on Sunday, Monday is observed.
- FR-ABS-PERM-01: CCI minimum enforcement for exceptional leave durations.
- CCI Art. 46: Permission exceptionnelle durations — marriage=4, birth=2, death spouse/child/parent=5, death sibling/in-law=2, move=2.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Entities, §Service layer, §Validationdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §Tenant isolation, §Date handlingdocs/standards/api-guidelines.md— §Query parameters, §PATCH semantics, §Standard envelopedocs/standards/react-typescript-guidelines.md— §List components, §DatePicker, §Inline editingdocs/standards/design-system.md— §Input, §DatePicker, §Card, §Toast components
Testing Requirements¶
Unit Tests¶
- HolidayConfigService:
getEffectiveHolidays()merges platform defaults with tenant overrides - HolidayConfigService:
updateVariableHoliday()rejects fixed holidays - HolidayConfigService: Sunday substitution rule shifts holiday to Monday
- HolidayConfigService:
seedHolidays()creates exactly 14 holidays - ExceptionalLeaveConfigService:
updateDuration()rejects duration below CCI minimum - ExceptionalLeaveConfigService:
updateDuration()accepts duration equal to CCI minimum - ExceptionalLeaveConfigService:
seedDefaults()creates exactly 9 event types
Integration Tests¶
- GET /api/v1/absence/config/public-holidays?year=2026 — returns 14 holidays
- PATCH /api/v1/absence/config/public-holidays with fixed holiday → 422
- PATCH /api/v1/absence/config/public-holidays with variable holiday → 200, creates TENANT_OVERRIDE
- GET /api/v1/absence/config/exceptional-leave — returns 9 event types
- PATCH /api/v1/absence/config/exceptional-leave/MARRIAGE_SELF with durationDays=3 → 422
- PATCH /api/v1/absence/config/exceptional-leave/MARRIAGE_SELF with durationDays=5 → 200
- Tenant isolation: tenant A overrides not visible to tenant B
What QA Will Validate¶
- Public holidays page displays correctly on all breakpoints (360px, 768px, 1024px)
- Fixed holidays show lock icon, no date picker
- Variable holidays show date picker with platform default as helper
- Year navigation works (prev/next)
- Exceptional leave section shows all 9 event types with correct CCI minimums
- Inline validation error when duration < CCI minimum
- Save triggers toast on success
- Offline state blocks edits with appropriate message
Out of Scope¶
- Adding new fixed holidays
- Removing holidays from the calendar
- Multi-country holiday support (V2 for BJ/CM)
- Automatic Islamic holiday date calculation (platform admin sets dates manually)
- Holiday impact on leave balance calculation (handled in leave request processing stories)
- Exceptional leave request flow (AB-009+)
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) — edits 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