Story AB-010: Leave Request Creation — Annual & Unpaid (Backend API + Frontend Form)¶
@assignee: @joel23p
Module: absence-management Slice: VS-AB-005 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 10 Depends on: AB-003, AB-005, AB-009 Can develop concurrently with: AB-011 Merge order: After AB-009 Estimated complexity: M PRD User Stories: US-ABS-001, US-ABS-007
Objective¶
Enable employees to submit annual leave and unpaid leave requests via the backend API and frontend form (S2 — simple form variant), including offline request creation with auto-sync, client-side validation with soft warnings (15-day notice, 14-day consecutive block), and hard blocks (insufficient balance, <12 months service).
Backend Scope¶
Entities & Records¶
Request record:
public record CreateLeaveRequestRequest(
@NotNull LeaveType leaveType, // ANNUAL or UNPAID (this story)
@NotNull LocalDate startDate,
@NotNull LocalDate endDate,
boolean halfDayStart, // afternoon only
boolean halfDayEnd, // morning only
@Size(max = 500) String reason
) {}
Response record:
public record LeaveRequestResponse(
UUID id,
LeaveType leaveType,
LeaveRequestStatus status,
LocalDate startDate,
LocalDate endDate,
boolean halfDayStart,
boolean halfDayEnd,
BigDecimal numberOfDays,
String reason,
boolean isRetroactive,
List<SoftWarning> warnings, // soft warnings (not blocking)
Instant createdAt
) {}
public record SoftWarning(
String code, // SHORT_NOTICE, NO_14_DAY_BLOCK
String message // French human-readable message
) {}
Repository Layer¶
Uses existing repositories from AB-002:
- LeaveRequestRepository.save(LeaveRequest request)
- LeaveRequestRepository.findOverlappingRequests(UUID tenantId, UUID employeeId, LocalDate start, LocalDate end)
- LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId)
Service Layer¶
LeaveRequestService.java — createRequest():
1. Resolve approver via ApproverResolutionService (org-unit-based, uses manager_id from CachedEmployeeData)
2. Validate via LeaveValidationService
3. Compute days via DayComputationService (business days, exclude public holidays, handle half-days)
4. Set status to PENDING_APPROVAL (no document needed for ANNUAL/UNPAID)
5. Set isRetroactive = true if startDate < today (FR-ABS-043)
6. Publish LeaveRequestCreated
7. Return response with any soft warnings
LeaveValidationService.java:
- Hard blocks (return 422):
- FR-ABS-040: ANNUAL only after 12 months service
- FR-ABS-041: ANNUAL balance >= days requested
- FR-ABS-046: overlap with approved leave of same-priority type
- startDate > endDate → 422
- Soft warnings (returned in response, NOT blocking):
- FR-ABS-044: startDate < today + 15 days → short notice warning
- FR-ABS-045: no 14-day consecutive block taken this period → reminder
ApproverResolutionService.java:
- Resolves approver from CachedEmployeeData.managerId (org-unit-based routing)
- Falls back to HR_MANAGER if no direct manager configured
DayComputationService.java:
- Computes business days between start and end dates
- Excludes public holidays (from PublicHolidayRepository)
- Handles half-days: subtract 0.5 for halfDayStart and/or halfDayEnd (FR-ABS-031)
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| POST | /api/v1/absence/leave-requests |
CreateLeaveRequestRequest |
LeaveRequestResponse (201) |
EMPLOYEE (self), HR_MANAGER (any) |
Validation Rules¶
- FR-ABS-040: ANNUAL only after 12 months service → 422
"Vous devez completer 12 mois de service avant de prendre un conge annuel." - FR-ABS-041: ANNUAL balance >= days requested → 422
"Solde insuffisant. Votre solde disponible est de {balance} jours." - FR-ABS-043: retroactive flag if
startDate < today - FR-ABS-044:
startDate < today + 15 days→ soft warning (not blocking) - FR-ABS-045: no 14-day block this period → soft warning (not blocking)
- FR-ABS-046: overlap with approved leave → 422 blocked for same-priority types
- All day computations use
BigDecimalwithRoundingMode.HALF_UP,scale(4)
Multi-Tenant Considerations¶
- All queries include
tenantIdfromScopedValue<TenantContext> - Public holiday lookup is tenant-scoped (tenants may have custom holidays)
- Approver resolution uses tenant's org structure
- Works across all 4 DB profiles
Frontend Scope¶
OFFLINE SYNC REMINDER (Papillon — CRITICAL): This story involves data mutations. The form MUST work identically offline. On submit offline: save to IndexedDB as DRAFT with sync-pending status. On reconnect: Service Worker auto-submits via POST. If server validation fails: push notification with reason + link to edit. Auto-save: every field change persisted to IndexedDB — form survives app restart, tab close, crash. See
docs/standards/offline-sync-guidelines.mdfor the full offline write+sync pattern.
Pages & Routes¶
| Route | Page | Description |
|---|---|---|
/absences/demande |
LeaveRequestPage | Leave request creation form (simple variant for ANNUAL/UNPAID) |
Components¶
LeaveRequestForm— Main form container with auto-save logicSelect— Type selector (ANNUAL, UNPAID for this story)DatePicker— Start date and end date pickers (side by side on mobile)Checkbox— Half-day toggles (revealed after dates are filled)DayComputationCard— Computed days display withprimary-subtlebackgroundInlineBanner— Soft warning banners (amber)Input(textarea) — Reason field (optional)Button— Submit button (primary, lg, fullWidth)AutoSaveIndicator— Caption showing draft save status
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | N/A (form is empty initially) | — |
| Empty | Form with placeholders | "Choisir un type de conge" |
| Error (hard block) | Red inline error below field | "Solde insuffisant. Votre solde disponible est de {balance} jours." / "Vous devez completer 12 mois de service..." |
| Error (server) | Toast | "Impossible d'envoyer la demande. Reessayez." |
| Warning (soft) | Amber InlineBanner below trigger field | "Attention : le preavis legal est de 15 jours avant le debut du conge." / "Rappel : la loi impose un conge principal d'au moins 14 jours ouvrables consecutifs par an." |
| Offline | Form works identically, saved as DRAFT | "Demande enregistree -- sera envoyee automatiquement" |
| Success | Check animation + Toast + navigate to /absences |
"Demande envoyee !" |
Responsive Behavior¶
Mobile (360px — Papillon primary):
- Full-screen scrollable form
- Type Select → Date pickers (side by side) → Half-day checkboxes (revealed after dates filled) → Day computation card (primary-subtle bg) → Soft warning banners (amber) → Reason textarea → Submit button (primary, lg, fullWidth)
- Auto-save caption at bottom
- Touch targets >= 48px
Desktop (1024px+ — Enterprise primary): - Modal (600px wide) or side panel - Same form flow, more compact spacing
Interactions¶
- Selecting type populates available date ranges
- Filling both dates triggers day computation (immediate, client-side)
- Half-day checkboxes appear only after both dates are set
- Day computation card updates in real-time as dates/half-days change
- Shows
{n} jours calendaires+Solde apres demande : {n} JC+dont {n} jour(s) ferie(s) non compte(s) - Submit: client-side validation first, then POST. On success: check animation + toast + navigate
- Auto-save: every field change → IndexedDB. Form survives app restart
French Micro-Copy¶
form.type_label: "Type de conge"form.type_placeholder: "Choisir un type de conge"form.start_date: "Date de debut"form.end_date: "Date de fin"form.half_day_start: "Demi-journee debut (apres-midi uniquement)"form.half_day_end: "Demi-journee fin (matin uniquement)"form.reason: "Motif (optionnel)"form.reason_placeholder: "Decrivez la raison de votre demande..."form.days_computed: "{n} jours calendaires"form.balance_after: "Solde apres demande : {n} JC"form.holiday_excluded: "dont {n} jour(s) ferie(s) non compte(s)"form.submit: "Envoyer la demande"form.autosave: "Brouillon sauvegarde automatiquement"info.retroactive: "Demande retroactive -- sera signalee dans l'historique d'audit."
Acceptance Criteria¶
AC-032: Employee can view balance and submit leave request
Given I am an employee with an active account
When I open the Absence module
Then I see my current leave balance (by type, in jours calendaires)
AC-033: Offline request creation with auto-sync
Given I fill out a leave request form offline
When connectivity returns
Then the request is auto-submitted to my manager for approval
AC-034: Soft warning for short notice period
Given I submit a request
When the start date is < 15 days from today
Then I see a soft warning about the 15-day notice period but can still submit
AC-035: Soft warning for 14-day consecutive block
Given I submit a request for annual leave and have NOT yet taken a 14-consecutive-day block this year
Then I see a soft warning about the legal 14-day minimum requirement
OHADA & Regulatory Rules¶
- FR-ABS-030 (CT Art. 25.9): Employee submits leave request to direct manager via org-unit hierarchy.
- FR-ABS-031 (CT Art. 25.10): Half-day computation — start day afternoon-only or end day morning-only deducts 0.5 from total days.
- FR-ABS-040 (CT Art. 25.7): ANNUAL leave requires 12 months of continuous service. Hard block.
- FR-ABS-041: Balance must be sufficient for the requested number of days. Hard block.
- FR-ABS-043: Retroactive requests (start_date < today) are flagged in audit trail.
- FR-ABS-044 (CT Art. 25.11): 15-day advance notice is recommended. Soft warning only.
- FR-ABS-045 (CCI Art. 25.12): At least one 14-consecutive-jours-ouvrables block per reference period. Soft warning reminder.
- FR-ABS-046: Overlap with same-priority approved leave is blocked. Hard block.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Validation, §Application Eventsdocs/standards/database-guidelines.md— §BigDecimal for financial calculations, §Multi-tenant repository patterndocs/standards/api-guidelines.md— §POST endpoint patterns, §ApiResponse envelope, §Validation error formatdocs/standards/react-typescript-guidelines.md— §Form patterns, §CSS Modules, §i18n, §Error handlingdocs/standards/design-system.md— §Select, §DatePicker, §Button, §InlineBanner, §Toastdocs/standards/offline-sync-guidelines.md— §Offline form submission, §IndexedDB auto-save, §Service Worker sync, §Conflict resolution
Testing Requirements¶
Unit Tests¶
LeaveValidationService: ANNUAL + <12 months service → 422 with correct French messageLeaveValidationService: ANNUAL + insufficient balance → 422 with balance in messageLeaveValidationService: start_date < today+15 → soft warning returned, not blockingLeaveValidationService: no 14-day block this period → soft warning returnedLeaveValidationService: overlap with approved leave → 422DayComputationService: 2026-03-02 to 2026-03-13 → correct business days minus holidaysDayComputationService: half-day start + half-day end → correctly subtracts 1.0 from totalApproverResolutionService: employee with manager → returns manager_idApproverResolutionService: employee without manager → falls back to HR_MANAGERLeaveRequestService.createRequest(): valid request → status=PENDING_APPROVAL, event published
Integration Tests¶
- POST
/api/v1/absence/leave-requestswith valid ANNUAL request → 201 with correct response - POST with insufficient balance → 422 with validation error in ApiResponse envelope
- POST with EMPLOYEE role for own request → 201
- POST with EMPLOYEE role for another employee → 403
- Multi-tenant: request created in tenant A not visible in tenant B
What QA Will Validate¶
- Form renders at 360px with correct field order and spacing
- Half-day checkboxes appear only after dates are filled
- Day computation card updates in real-time
- Soft warnings appear as amber banners, do not block submission
- Hard blocks show red inline errors, disable submit
- Offline: form works, saves as DRAFT, auto-syncs on reconnect
- Auto-save: close tab, reopen → form state preserved
- Success: check animation + toast + navigates to
/absences
Out of Scope¶
- Sick leave form variant (AB-014)
- Exceptional leave form variant (AB-015)
- Maternity leave form variant (AB-016)
- Document upload (AB-014)
- Approval flow (AB-012)
- Request cancellation (AB-013)
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 — requests scoped to tenant context
- [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — Form saves as DRAFT offline, auto-syncs on reconnect, auto-save persists to 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, retroactive flag logged