Aller au contenu

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:

public enum HolidaySource { PLATFORM_DEFAULT, TENANT_OVERRIDE }

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 durationDays must 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."
  • year query parameter is required for holidays endpoint
  • holidayCode must 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, §Validation
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Tenant isolation, §Date handling
  • docs/standards/api-guidelines.md — §Query parameters, §PATCH semantics, §Standard envelope
  • docs/standards/react-typescript-guidelines.md — §List components, §DatePicker, §Inline editing
  • docs/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