Aller au contenu

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):

  1. Load request, determine current status
  2. Apply cancellation rules:
  3. PENDING_APPROVAL or PENDING_DOCUMENTCANCELLED_BY_EMPLOYEE, no balance impact
  4. APPROVED + startDate > todayCANCELLED_BY_EMPLOYEE, insert CANCEL_CREDIT in ledger, update cached balance, publish LeaveCancelled
  5. APPROVED + startDate <= today (IN_PROGRESS) → 422 blocked "Impossible d'annuler un conge en cours. Contactez les RH pour un retour anticipe."
  6. COMPLETED, REJECTED, CANCELLED → 422 "Cette demande ne peut plus etre annulee."
  7. HR/DG override: can cancel any status except COMPLETED → CANCELLED_BY_HR with mandatory reason
  8. Set cancelledAt, cancelledBy
  9. Publish LeaveCancelled with numberOfDaysCredited
  10. 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 tenantId from ScopedValue<TenantContext>
  • Credit-back uses tenant-scoped pessimistic lock
  • LeaveCancelled includes tenantId for 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 status
  • CancelConfirmationSheet — BottomSheet (mobile) / Modal (desktop) with reinforced confirmation for APPROVED requests
  • UndoToast — 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_CREDIT ledger 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 Events
  • docs/standards/database-guidelines.md — §SELECT FOR UPDATE pattern, §BigDecimal for financial calculations
  • docs/standards/api-guidelines.md — §PATCH endpoint patterns, §ApiResponse envelope, §422 validation errors
  • docs/standards/react-typescript-guidelines.md — §Component patterns, §CSS Modules, §i18n
  • docs/standards/design-system.md — §BottomSheet, §Button (danger variant), §Toast (undo pattern), §Modal
  • docs/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 entry
  • LeaveRequestService.cancelRequest(): PENDING_DOCUMENT → CANCELLED_BY_EMPLOYEE, no ledger entry
  • LeaveRequestService.cancelRequest(): APPROVED + startDate > today → CANCELLED_BY_EMPLOYEE, CANCEL_CREDIT ledger entry, balance incremented
  • LeaveRequestService.cancelRequest(): APPROVED + startDate <= today (IN_PROGRESS) → 422
  • LeaveRequestService.cancelRequest(): COMPLETED → 422
  • LeaveRequestService.cancelRequest(): REJECTED → 422
  • LeaveRequestService.cancelRequest(): already CANCELLED → 422
  • LeaveRequestService.cancelRequest(): HR_MANAGER cancels IN_PROGRESS → CANCELLED_BY_HR (allowed)
  • LeaveRequestService.cancelRequest(): HR_MANAGER without reason → 422
  • BalanceMutationService.creditBalance(): correct credit with pessimistic lock

Integration Tests

  • PATCH /api/v1/absence/leave-requests/{id}/cancel on 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