Story AB-015: Permission Exceptionnelle Request (Backend + Frontend)¶
@assignee: @joel23p
Module: absence-management Slice: VS-AB-005 (exceptional extension) from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 15 Depends on: AB-010, AB-008 Can develop concurrently with: AB-014, AB-016 Merge order: After AB-010 and AB-008 Estimated complexity: M PRD User Stories: US-ABS-007
Objective¶
Extend the leave request creation flow to support permission exceptionnelle requests with the 2-step wizard: Step 1 selects the event type (large tappable cards with icons showing CCI or tenant-configured durations), Step 2 auto-fills dates and requires a supporting document.
Backend Scope¶
Entities & Records¶
Request record (extends CreateLeaveRequestRequest from AB-010):
// Additional fields when leaveType=EXCEPTIONAL
public record CreateLeaveRequestRequest(
@NotNull LeaveType leaveType, // EXCEPTIONAL
@NotNull LocalDate startDate,
@NotNull LocalDate endDate, // auto-computed, read-only
boolean halfDayStart,
boolean halfDayEnd,
@Size(max = 500) String reason,
// Exceptional-specific:
@NotNull String eventTypeCode, // MARRIAGE_SELF, BIRTH_CHILD, etc.
UUID supportingDocumentId // optional at creation
) {}
Enums:
public enum ExceptionalEventType {
MARRIAGE_SELF, // Mariage du salarie — CCI min: 4 jours
MARRIAGE_CHILD, // Mariage d'un enfant — CCI min: 2 jours
BIRTH_CHILD, // Naissance d'un enfant — CCI min: 2 jours
DEATH_SPOUSE, // Deces du conjoint — CCI min: 5 jours
DEATH_CHILD, // Deces d'un enfant — CCI min: 5 jours
DEATH_PARENT, // Deces pere/mere — CCI min: 5 jours
DEATH_SIBLING, // Deces frere/soeur — CCI min: 3 jours
DEATH_IN_LAW, // Deces beau-pere/belle-mere — CCI min: 3 jours
MOVE // Demenagement — CCI min: 1 jour
}
Repository Layer¶
Uses existing repositories from AB-002 + AB-008:
- ExceptionalLeaveConfigRepository.findByTenantIdAndEventTypeCode(UUID tenantId, String eventTypeCode) — from AB-008
- LeaveRequestRepository — standard save/query
- LeaveBalanceLedgerRepository — for checking exceptional days used this period
Service Layer¶
LeaveRequestService.java — extends createRequest() for EXCEPTIONAL type:
1. Look up duration from ExceptionalLeaveConfig:
- If tenant has custom config for this event type → use tenant's durationDays
- Else → use CCI default duration
2. Validate annual cap: totalExceptionalTakenThisPeriod + numberOfDays <= 10 (FR-ABS-042, FR-ABS-PERM-02)
3. Auto-compute endDate = startDate + duration (in jours ouvrables, skipping weekends and holidays)
4. If supportingDocumentId IS NULL → status = PENDING_DOCUMENT
5. If supportingDocumentId IS NOT NULL → status = PENDING_APPROVAL
6. Publish LeaveRequestCreated
ExceptionalLeaveConfigService.java (from AB-008, used here):
- getDurationForEvent(UUID tenantId, String eventTypeCode): returns tenant config or CCI default
- validateMinimumDuration(String eventTypeCode, int configuredDays): blocks below CCI minimum → 422
API Endpoints¶
Note: Uses the same POST endpoint as AB-010 (POST /api/v1/absence/leave-requests), extended to handle leaveType=EXCEPTIONAL with eventTypeCode.
No new endpoints. The event type list and durations are served by AB-008's config endpoints.
Validation Rules¶
- FR-ABS-PERM-01: CCI minimum enforcement — tenant cannot configure a duration below the CCI minimum for any event type → 422
"La duree minimale legale (CCI) pour cet evenement est de {X} jours." - FR-ABS-PERM-02 / FR-ABS-042: Annual cap of 10 jours for all permissions exceptionnelles combined → 422
"Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles." eventTypeCodemust be a validExceptionalEventTypeenum value → 422endDateis auto-computed — if provided by client, it is ignored (server computes)- Supporting document required (justificatif) — same PENDING_DOCUMENT logic as AB-014
Multi-Tenant Considerations¶
- Duration lookup is tenant-scoped: tenant config overrides CCI defaults
- Annual cap check queries ledger entries within the current reference period for the tenant
- All queries include
tenantIdfromScopedValue<TenantContext> - Works across all 4 DB profiles
Frontend Scope¶
OFFLINE SYNC REMINDER (Papillon — CRITICAL): This story involves data mutations. The permission exceptionnelle form MUST work offline. Event type list and CCI durations are cached in IndexedDB at first load. Tenant-configured durations are cached on sync. Form saves as DRAFT to IndexedDB on submit offline. File attachments stored as blobs in IndexedDB, uploaded on reconnect. See
docs/standards/offline-sync-guidelines.md.
Pages & Routes¶
No new routes. Extends the existing leave request form from AB-010.
| Route | Page | Description |
|---|---|---|
/absences/demande |
LeaveRequestPage (extended) | 2-step wizard when type=EXCEPTIONAL |
Components¶
Step 1 — Event Selection:
- StepIndicator — Step indicator (steps=2, current=1)
- EventTypeCard — Large tappable card with icon, event name, and duration in days
- EventTypeList — Grid of EventTypeCards (first 5 visible, rest behind expander)
- RemainingDaysHelper — Helper text showing "Restant cette annee: {remaining}/10 jours"
- ExpanderButton — "Plus d'evenements" toggle for remaining event types
Step 2 — Dates + Document:
- StepIndicator — Step indicator (steps=2, current=2)
- EventSummary — Summary card of step 1 selection (icon, name, duration)
- DatePicker — Start date only (end date auto-calculated, read-only with lock icon)
- AutoComputedDate — Read-only end date display with lock icon
- FileUpload — Document upload zone (from AB-014, required for exceptional)
- Button — Submit button
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Shimmer on event type cards | — |
| Empty | N/A (event types always available from CCI defaults) | — |
| Error (cap reached) | Inline error below event cards | "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles." |
| Error (validation) | Red inline below field | Various validation messages |
| Offline | Form works, saved as DRAFT | "Demande enregistree -- sera envoyee automatiquement" |
| Success | Check animation + Toast + navigate to /absences |
"Demande envoyee !" |
Responsive Behavior¶
Mobile (360px — Papillon primary): - Step 1: Full-screen, StepIndicator at top, event type cards in single-column list - Each card: icon (left) + event name + duration in days (right). Touch targets >= 48px - First 5 events visible, "Plus d'evenements" expander for remaining 4 - "Restant cette annee: {remaining}/10 jours" helper text below cards - Step 2: Full-screen, StepIndicator at top, summary of step 1, start date DatePicker, auto-computed end date (read-only with lock icon), FileUpload zone, submit button (primary, lg, fullWidth) - Caption below submit: "Vous pouvez envoyer sans document. La demande restera en attente du justificatif."
Desktop (1024px+ — Enterprise primary): - Step 1: Event type cards in 2-column grid within modal/panel (600px wide) - Step 2: Same form flow, more compact spacing - All event types visible (no expander needed at wider viewport)
Interactions¶
- Tap event type card → selected state (checkmark overlay) + "Suivant" button appears
- Tap "Suivant" → slide to Step 2 with left animation
- Step 2: only start date is editable. End date auto-calculates: "Fin calculee: debut + {n} jours ouvrables"
- Tap "Retour" on Step 2 → slide back to Step 1, selection preserved
- File upload: same behavior as AB-014 (tap → file picker, drag-drop on desktop)
- Submit: client-side cap validation, then POST
French Micro-Copy¶
exceptional.step1_title: "Quel evenement ?"exceptional.step2_title: "Dates et justificatif"exceptional.remaining: "Restant cette annee : {remaining}/10 jours"exceptional.more_events: "Plus d'evenements"exceptional.next: "Suivant"exceptional.back: "Retour"exceptional.end_computed: "Fin calculee : debut + {n} jours ouvrables"exceptional.doc_required: "Justificatif requis"exceptional.doc_optional_caption: "Vous pouvez envoyer sans document. La demande restera en attente du justificatif."exceptional.cap_reached: "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles."exceptional.cci_minimum_block: "La duree minimale legale (CCI) pour cet evenement est de {X} jours."- Event type labels:
event.MARRIAGE_SELF: "Mariage du salarie"event.MARRIAGE_CHILD: "Mariage d'un enfant"event.BIRTH_CHILD: "Naissance d'un enfant"event.DEATH_SPOUSE: "Deces du conjoint"event.DEATH_CHILD: "Deces d'un enfant"event.DEATH_PARENT: "Deces pere/mere"event.DEATH_SIBLING: "Deces frere/soeur"event.DEATH_IN_LAW: "Deces beau-pere/belle-mere"event.MOVE: "Demenagement"
Acceptance Criteria¶
AC-048: System pre-fills duration from CCI or tenant config
Given I select "Permission exceptionnelle" as leave type
When I choose the event type (mariage, naissance, deces, etc.)
Then the system pre-fills the number of days based on the CCI minimum for CI (or tenant's configured duration if higher)
AC-049: Tenant-configured durations override CCI
Given the tenant has configured more generous durations (above CCI minimum)
When I select the event type
Then the system pre-fills the tenant's duration
AC-050: CCI minimum blocks below-minimum configuration
Given the tenant tries to configure a duration below the CCI minimum
When they save
Then the system blocks with: "La duree minimale legale (CCI) pour cet evenement est de X jours."
OHADA & Regulatory Rules¶
- FR-ABS-PERM-01 (CCI Art. 46): CCI minimum enforcement — each event type has a CCI-mandated minimum duration. Tenants may configure higher durations but never lower. System blocks below-minimum configuration.
- FR-ABS-PERM-02 / FR-ABS-042 (CCI Art. 46): Annual cap of 10 jours ouvrables for all permissions exceptionnelles combined per reference period. Once reached, no further exceptional leave requests are accepted.
- CCI Art. 46 durations: Mariage salarie=4j, Mariage enfant=2j, Naissance=2j, Deces conjoint/enfant/parent=5j, Deces frere-soeur/beau-parent=3j, Demenagement=1j.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Enum handling, §Application Eventsdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §BigDecimal for day computationsdocs/standards/api-guidelines.md— §POST endpoint patterns, §ApiResponse envelope, §422 validation errorsdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18n, §Multi-step form patternsdocs/standards/design-system.md— §Card (selectable variant), §StepIndicator, §DatePicker, §FileUpload, §Buttondocs/standards/offline-sync-guidelines.md— §Cached reference data (event types), §Offline form submission, §Blob storage in IndexedDB
Testing Requirements¶
Unit Tests¶
LeaveRequestService.createRequest(): EXCEPTIONAL + MARRIAGE_SELF → duration=4j from CCI defaultLeaveRequestService.createRequest(): EXCEPTIONAL + MARRIAGE_SELF + tenant config 6j → duration=6j (tenant override)LeaveRequestService.createRequest(): total exceptional used=8j + requesting 3j → 422 (cap exceeded)LeaveRequestService.createRequest(): total exceptional used=8j + requesting 2j → allowed (exactly at cap)ExceptionalLeaveConfigService.validateMinimumDuration(): configured 2j for MARRIAGE_SELF (CCI min=4j) → 422ExceptionalLeaveConfigService.validateMinimumDuration(): configured 5j for MARRIAGE_SELF (CCI min=4j) → allowed- End date auto-computation: startDate=Monday + 4 jours ouvrables → endDate=Thursday (same week)
- End date auto-computation: startDate=Thursday + 4 jours ouvrables → endDate=Tuesday (skips weekend)
Integration Tests¶
- POST with EXCEPTIONAL type + valid event code → 201, duration from config
- POST with EXCEPTIONAL type + cap exceeded → 422
- POST with EXCEPTIONAL type + no document → 201, status=PENDING_DOCUMENT
- POST with invalid eventTypeCode → 422
- Multi-tenant: tenant A config does not affect tenant B's durations
What QA Will Validate¶
- Step 1: event type cards render at 360px with icons and durations
- First 5 events visible, "Plus d'evenements" expander works
- "Restant cette annee" helper updates correctly
- Cap reached → inline error, cards disabled
- Step 2: start date picker, end date auto-computed and read-only
- End date shows "Fin calculee: debut + {n} jours ouvrables"
- FileUpload zone appears with correct helper text
- Submit without doc → PENDING_DOCUMENT toast
- Offline: form works, event types from cache, saved as DRAFT
- Desktop: 2-column grid for event types in modal
Out of Scope¶
- Permission exceptionnelle config management (AB-008)
- Approval flow (AB-012)
- Document upload infrastructure (uses platform MinIO API)
- Custom event types beyond the 9 CCI-defined types (V2)
- Partial-day permissions exceptionnelles (always full days per CCI)
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 — tenant config overrides scoped correctly, cap check per tenant
- [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — Event types cached, form saves DRAFT offline, blobs in IndexedDB
- [ ] No TypeScript
any - [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format
- [ ] Audit trail entries for every data mutation — LeaveRequestCreated published with eventTypeCode