Story AB-013: Leave Cancellation (Backend + Frontend)¶
Module: absence-management Slice: VS-AB-007 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 13 Depends on: AB-010 Can develop concurrently with: AB-011, AB-012 Merge order: After AB-010 Estimated complexity: S PRD User Stories: US-ABS-009
Objective¶
Enable employees to cancel their leave requests with appropriate rules: free cancellation for pending requests, balance credit-back for approved-but-not-started leaves, and block for in-progress leaves.
Backend Scope¶
Entities & Records¶
Request record:
public record CancelLeaveRequestRequest(
@Size(max = 500) String reason // optional for employee, mandatory for HR/DG
) {}
Event published:
public record LeaveCancelled(
UUID employeeId,
UUID tenantId,
UUID leaveRequestId,
LeaveType leaveType,
BigDecimal numberOfDaysCredited, // 0 if was pending, >0 if was approved
String cancelledBy // EMPLOYEE or HR
) {}
Repository Layer¶
Uses existing repositories from AB-002:
- LeaveRequestRepository.findByIdAndTenantId(UUID id, UUID tenantId)
- LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeIdForUpdate(UUID tenantId, UUID employeeId) — SELECT FOR UPDATE for credit-back
- LeaveBalanceLedgerRepository.save(LeaveBalanceLedger entry)
Service Layer¶
LeaveRequestService.java — cancelRequest(UUID requestId, String reason):
- Load request, determine current status
- Apply cancellation rules:
PENDING_APPROVALorPENDING_DOCUMENT→CANCELLED_BY_EMPLOYEE, no balance impactAPPROVED+startDate > today→CANCELLED_BY_EMPLOYEE, insertCANCEL_CREDITin ledger, update cached balance, publishLeaveCancelledAPPROVED+startDate <= today(IN_PROGRESS) → 422 blocked"Impossible d'annuler un conge en cours. Contactez les RH pour un retour anticipe."COMPLETED,REJECTED,CANCELLED→ 422"Cette demande ne peut plus etre annulee."- HR/DG override: can cancel any status except COMPLETED →
CANCELLED_BY_HRwith mandatory reason - Set
cancelledAt,cancelledBy - Publish
LeaveCancelledwithnumberOfDaysCredited - Remove from approver's queue (if was PENDING_APPROVAL)
BalanceMutationService.java — creditBalance():
- Pessimistic lock on LeaveEmployeeProfile
- Insert CANCEL_CREDIT ledger entry (positive amount)
- Increment cached balance
- Version increment
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| PATCH | /api/v1/absence/leave-requests/{id}/cancel |
CancelLeaveRequestRequest |
LeaveRequestResponse |
EMPLOYEE (own), HR_MANAGER, DG (any) |
Validation Rules¶
- Employee can only cancel own requests
- HR_MANAGER and DG can cancel any request (with mandatory reason)
- IN_PROGRESS leaves cannot be cancelled by employee → 422
- COMPLETED, REJECTED, already-CANCELLED requests cannot be cancelled → 422
- Credit-back uses pessimistic lock to prevent race conditions
Multi-Tenant Considerations¶
- All queries include
tenantIdfromScopedValue<TenantContext> - Credit-back uses tenant-scoped pessimistic lock
LeaveCancelledincludestenantIdfor downstream consumers- Works across all 4 DB profiles
Frontend Scope¶
OFFLINE SYNC REMINDER (Papillon): This story involves data mutations (cancellation). Cancellation of PENDING requests CAN be queued offline (no balance impact). Cancellation of APPROVED requests REQUIRES connectivity (involves pessimistic lock for credit-back). When offline and request is APPROVED: disable cancel button with "Cette action necessite une connexion internet". When offline and request is PENDING: queue cancellation for sync. See
docs/standards/offline-sync-guidelines.md.
Pages & Routes¶
No new routes. Cancellation UI is added to the existing detail view from AB-011.
| Route | Page | Description |
|---|---|---|
/absences/demande/{id} |
LeaveRequestDetail (extended) | Cancel button added conditionally |
Components¶
CancelButton— Conditional button on detail view, adapts label and confirmation based on statusCancelConfirmationSheet— BottomSheet (mobile) / Modal (desktop) with reinforced confirmation for APPROVED requestsUndoToast— Toast with 8-second undo countdown after cancellation
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | N/A (button on existing detail view) | — |
| Error | Toast | "Impossible d'annuler la demande." |
| Offline (pending cancel) | Queued for sync (PENDING requests only) | "L'annulation sera effectuee a la reconnexion" |
| Offline (approved cancel) | Button disabled | "Cette action necessite une connexion internet" |
| Success | Undo toast with 8s countdown | "Demande annulee" / "Annuler (restant : {seconds}s)" |
Responsive Behavior¶
Mobile (360px — Papillon primary):
- On detail view, conditional cancel section based on status:
- PENDING_APPROVAL / PENDING_DOCUMENT: "Annuler la demande" (danger ghost button, fullWidth)
- APPROVED (not started): "Annuler le conge" with reinforced BottomSheet confirmation
- IN_PROGRESS: No button. Info banner: "Contactez les RH pour un retour anticipe"
- COMPLETED / REJECTED / CANCELLED: Read-only, no actions
- Cancel confirmation BottomSheet: warning text + "Confirmer l'annulation" (danger) + "Retour" (ghost)
- For APPROVED: reinforced text: "Ce conge approuve sera annule. Vos jours seront recredites."
- Touch targets >= 48px
Desktop (1024px+ — Enterprise primary): - Cancel button in detail panel action bar - Confirmation via Modal (400px wide) instead of BottomSheet - Same status-conditional logic
Interactions¶
- Tap "Annuler la demande" (PENDING) → immediate cancellation + undo toast
- Tap "Annuler le conge" (APPROVED) → confirmation BottomSheet/Modal → cancel + undo toast
- Undo toast: 8-second countdown. Tap "Annuler" within 8s → restore previous status + reverse ledger entry
- After 8s → cancellation finalized, toast dismissed
French Micro-Copy¶
cancel.pending_button: "Annuler la demande"cancel.approved_button: "Annuler le conge"cancel.confirm_title: "Confirmer l'annulation"cancel.confirm_pending: "Cette demande en attente sera annulee."cancel.confirm_approved: "Ce conge approuve sera annule. Vos jours seront recredites."cancel.confirm_button: "Confirmer l'annulation"cancel.back_button: "Retour"cancel.in_progress_info: "Contactez les RH pour un retour anticipe"cancel.success: "Demande annulee"cancel.undo: "Annuler (restant : {seconds}s)"
Acceptance Criteria¶
AC-042: Cancel pending request freely
Given my leave request is PENDING_APPROVAL
When I cancel it
Then it moves to CANCELLED and disappears from the approver's queue
AC-043: Cancel approved leave credits balance
Given my leave is APPROVED but has not started yet
When I cancel it
Then the days are credited back to my balance, and Manager and HR are notified
AC-044: Cannot cancel in-progress leave
Given my leave is APPROVED and has already started (I'm currently on leave)
When I try to cancel
Then I cannot — I must request an early return through HR
OHADA & Regulatory Rules¶
- FR-ABS-070: Pending requests may be freely cancelled by the employee with no balance impact.
- FR-ABS-071: Approved-but-not-started leave may be cancelled — balance is credited back via
CANCEL_CREDITledger entry. - FR-ABS-072: In-progress leave cannot be cancelled by the employee — early return must be recorded by HR.
- FR-ABS-073: HR/DG can cancel any request (except COMPLETED) with mandatory reason — for administrative corrections.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Pessimistic locking, §Application Eventsdocs/standards/database-guidelines.md— §SELECT FOR UPDATE pattern, §BigDecimal for financial calculationsdocs/standards/api-guidelines.md— §PATCH endpoint patterns, §ApiResponse envelope, §422 validation errorsdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18ndocs/standards/design-system.md— §BottomSheet, §Button (danger variant), §Toast (undo pattern), §Modaldocs/standards/offline-sync-guidelines.md— §Offline mutation queuing (PENDING only), §Actions requiring connectivity (APPROVED cancel)
Testing Requirements¶
Unit Tests¶
LeaveRequestService.cancelRequest(): PENDING_APPROVAL → CANCELLED_BY_EMPLOYEE, no ledger entryLeaveRequestService.cancelRequest(): PENDING_DOCUMENT → CANCELLED_BY_EMPLOYEE, no ledger entryLeaveRequestService.cancelRequest(): APPROVED + startDate > today → CANCELLED_BY_EMPLOYEE, CANCEL_CREDIT ledger entry, balance incrementedLeaveRequestService.cancelRequest(): APPROVED + startDate <= today (IN_PROGRESS) → 422LeaveRequestService.cancelRequest(): COMPLETED → 422LeaveRequestService.cancelRequest(): REJECTED → 422LeaveRequestService.cancelRequest(): already CANCELLED → 422LeaveRequestService.cancelRequest(): HR_MANAGER cancels IN_PROGRESS → CANCELLED_BY_HR (allowed)LeaveRequestService.cancelRequest(): HR_MANAGER without reason → 422BalanceMutationService.creditBalance(): correct credit with pessimistic lock
Integration Tests¶
- PATCH
/api/v1/absence/leave-requests/{id}/cancelon PENDING request → 200, status=CANCELLED_BY_EMPLOYEE - PATCH on APPROVED request (not started) → 200, balance credited in DB
- PATCH on IN_PROGRESS request by EMPLOYEE → 422
- PATCH on IN_PROGRESS request by HR_MANAGER with reason → 200
- Multi-tenant isolation: employee in tenant A cannot cancel request in tenant B
What QA Will Validate¶
- PENDING request detail shows "Annuler la demande" button
- APPROVED request detail shows "Annuler le conge" with confirmation sheet
- IN_PROGRESS request detail shows info banner (no cancel button)
- COMPLETED/REJECTED/CANCELLED detail shows no cancel actions
- Undo toast appears for 8 seconds after cancellation
- Undo within 8s restores previous status
- Offline (PENDING cancel): queued, synced on reconnect
- Offline (APPROVED cancel): button disabled with connectivity message
Out of Scope¶
- HR early return recording for in-progress leaves (V2)
- HR cancellation of in-progress leaves (handled in HR admin tools)
- Batch cancellation (Enterprise V2)
- Creating requests (AB-010)
- Approval flow (AB-012)
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 — cancellation scoped to tenant, RBAC enforced
- [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — PENDING cancellation queued offline, APPROVED cancel requires connectivity
- [ ] No TypeScript
any - [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format
- [ ] Audit trail entries for every data mutation — LeaveCancelled published, CANCEL_CREDIT ledger entry for approved cancellations