Story AB-013 : Annulation de congés (Backend + Frontend)¶
Module : absence-management Slice : VS-AB-007 de l'architecture Contexte produit : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 13 Dépend de : AB-010 Peut être développé en parallèle avec : AB-011, AB-012 Ordre de merge : Après AB-010 Complexité estimée : S PRD User Stories : US-ABS-009
Objectif¶
Permettre aux employés d'annuler leurs demandes de congés avec les règles appropriées : annulation libre pour les demandes en attente, crédit de solde pour les congés approuvés mais non commencés, et blocage pour les congés en cours.
Périmètre Backend¶
Entités & Enregistrements¶
Enregistrement de demande :
public record CancelLeaveRequestRequest(
@Size(max = 500) String reason // optional for employee, mandatory for HR/DG
) {}
Événement publié :
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
) {}
Couche Repository¶
Utilise les repositories existants d'AB-002 :
- LeaveRequestRepository.findByIdAndTenantId(UUID id, UUID tenantId)
- LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeIdForUpdate(UUID tenantId, UUID employeeId) — SELECT FOR UPDATE pour le crédit de solde
- LeaveBalanceLedgerRepository.save(LeaveBalanceLedger entry)
Couche Service¶
LeaveRequestService.java — cancelRequest(UUID requestId, String reason) :
- Charger la demande, déterminer le statut actuel
- Appliquer les règles d'annulation :
PENDING_APPROVALouPENDING_DOCUMENT→CANCELLED_BY_EMPLOYEE, aucun impact sur le soldeAPPROVED+startDate > today→CANCELLED_BY_EMPLOYEE, insérerCANCEL_CREDITdans le grand livre, mettre à jour le solde en cache, publierLeaveCancelledAPPROVED+startDate <= today(IN_PROGRESS) → 422 bloqué"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."- Dérogation HR/DG : peut annuler n'importe quel statut sauf COMPLETED →
CANCELLED_BY_HRavec motif obligatoire - Définir
cancelledAt,cancelledBy - Publier
LeaveCancelledavecnumberOfDaysCredited - Retirer de la file d'attente de l'approbateur (si était PENDING_APPROVAL)
BalanceMutationService.java — creditBalance() :
- Verrou pessimiste sur LeaveEmployeeProfile
- Insérer une entrée CANCEL_CREDIT dans le grand livre (montant positif)
- Incrémenter le solde en cache
- Incrémentation de version
Endpoints API¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| PATCH | /api/v1/absence/leave-requests/{id}/cancel |
CancelLeaveRequestRequest |
LeaveRequestResponse |
EMPLOYEE (own), HR_MANAGER, DG (any) |
Règles de validation¶
- L'employé peut uniquement annuler ses propres demandes
- HR_MANAGER et DG peuvent annuler n'importe quelle demande (avec motif obligatoire)
- Les congés IN_PROGRESS ne peuvent pas être annulés par l'employé → 422
- Les demandes COMPLETED, REJECTED ou déjà CANCELLED ne peuvent pas être annulées → 422
- Le crédit de solde utilise un verrou pessimiste pour éviter les conditions de concurrence
Considérations multi-tenant¶
- Toutes les requêtes incluent
tenantIddepuisScopedValue<TenantContext> - Le crédit de solde utilise un verrou pessimiste filtré par tenant
LeaveCancelledincluttenantIdpour les consommateurs en aval- Fonctionne sur les 4 profils DB
Périmètre Frontend¶
RAPPEL OFFLINE SYNC (Papillon) : Cette story implique des mutations de données (annulation). L'annulation des demandes PENDING PEUT être mise en file d'attente hors connexion (aucun impact sur le solde). L'annulation des demandes APPROVED NÉCESSITE une connexion (implique un verrou pessimiste pour le crédit de solde). Hors connexion et demande APPROVED : désactiver le bouton d'annulation avec "Cette action nécessite une connexion internet". Hors connexion et demande PENDING : mettre l'annulation en file d'attente pour synchronisation. Voir
docs/standards/offline-sync-guidelines.md.
Pages & Routes¶
Pas de nouvelles routes. L'interface d'annulation est ajoutée à la vue détaillée existante d'AB-011.
| Route | Page | Description |
|---|---|---|
/absences/demande/{id} |
LeaveRequestDetail (étendu) | Bouton d'annulation ajouté de manière conditionnelle |
Composants¶
CancelButton— Bouton conditionnel sur la vue détaillée, adapte le libellé et la confirmation selon le statutCancelConfirmationSheet— BottomSheet (mobile) / Modal (desktop) avec confirmation renforcée pour les demandes APPROVEDUndoToast— Toast avec compte à rebours d'annulation de 8 secondes
États UI (TOUS OBLIGATOIRES)¶
| 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)" |
Comportement responsive¶
Mobile (360px — priorité Papillon) :
- Sur la vue détaillée, section d'annulation conditionnelle selon le statut :
- PENDING_APPROVAL / PENDING_DOCUMENT : "Annuler la demande" (bouton danger ghost, fullWidth)
- APPROVED (non commencé) : "Annuler le congé" avec BottomSheet de confirmation renforcée
- IN_PROGRESS : Pas de bouton. Bannière info : "Contactez les RH pour un retour anticipé"
- COMPLETED / REJECTED / CANCELLED : Lecture seule, aucune action
- BottomSheet de confirmation d'annulation : texte d'avertissement + "Confirmer l'annulation" (danger) + "Retour" (ghost)
- Pour APPROVED : texte renforcé : "Ce congé approuvé sera annulé. Vos jours seront recrédités."
- Cibles de toucher >= 48px
Desktop (1024px+ — priorité Enterprise) : - Bouton d'annulation dans la barre d'action du panneau de détail - Confirmation via Modal (400px de large) à la place du BottomSheet - Même logique conditionnelle selon le statut
Interactions¶
- Appuyer sur "Annuler la demande" (PENDING) → annulation immédiate + toast d'annulation
- Appuyer sur "Annuler le congé" (APPROVED) → BottomSheet/Modal de confirmation → annulation + toast d'annulation
- Toast d'annulation : compte à rebours de 8 secondes. Appuyer sur "Annuler" dans les 8s → rétablir le statut précédent + inverser l'entrée du grand livre
- Après 8s → annulation finalisée, toast rejeté
Micro-copy français¶
cancel.pending_button: "Annuler la demande"cancel.approved_button: "Annuler le congé"cancel.confirm_title: "Confirmer l'annulation"cancel.confirm_pending: "Cette demande en attente sera annulée."cancel.confirm_approved: "Ce congé approuvé sera annulé. Vos jours seront recrédités."cancel.confirm_button: "Confirmer l'annulation"cancel.back_button: "Retour"cancel.in_progress_info: "Contactez les RH pour un retour anticipé"cancel.success: "Demande annulée"cancel.undo: "Annuler (restant : {seconds}s)"
Critères d'acceptation¶
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
Règles OHADA & Réglementaires¶
- FR-ABS-070 : Les demandes en attente peuvent être librement annulées par l'employé sans impact sur le solde.
- FR-ABS-071 : Les congés approuvés mais non commencés peuvent être annulés — le solde est recrédité via une entrée
CANCEL_CREDITdans le grand livre. - FR-ABS-072 : Les congés en cours ne peuvent pas être annulés par l'employé — le retour anticipé doit être enregistré par les RH.
- FR-ABS-073 : HR/DG peuvent annuler n'importe quelle demande (sauf COMPLETED) avec un motif obligatoire — pour les corrections administratives.
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)
Exigences de test¶
Tests unitaires¶
LeaveRequestService.cancelRequest(): PENDING_APPROVAL → CANCELLED_BY_EMPLOYEE, aucune entrée de grand livreLeaveRequestService.cancelRequest(): PENDING_DOCUMENT → CANCELLED_BY_EMPLOYEE, aucune entrée de grand livreLeaveRequestService.cancelRequest(): APPROVED + startDate > today → CANCELLED_BY_EMPLOYEE, entrée de grand livre CANCEL_CREDIT, solde incrémentéLeaveRequestService.cancelRequest(): APPROVED + startDate <= today (IN_PROGRESS) → 422LeaveRequestService.cancelRequest(): COMPLETED → 422LeaveRequestService.cancelRequest(): REJECTED → 422LeaveRequestService.cancelRequest(): déjà CANCELLED → 422LeaveRequestService.cancelRequest(): HR_MANAGER annule IN_PROGRESS → CANCELLED_BY_HR (autorisé)LeaveRequestService.cancelRequest(): HR_MANAGER sans motif → 422BalanceMutationService.creditBalance(): crédit correct avec verrou pessimiste
Tests d'intégration¶
- PATCH
/api/v1/absence/leave-requests/{id}/cancelsur une demande PENDING → 200, status=CANCELLED_BY_EMPLOYEE - PATCH sur une demande APPROVED (non commencée) → 200, solde crédité en base
- PATCH sur une demande IN_PROGRESS par EMPLOYEE → 422
- PATCH sur une demande IN_PROGRESS par HR_MANAGER avec motif → 200
- Isolation multi-tenant : un employé du tenant A ne peut pas annuler une demande du tenant B
Ce que la QA validera¶
- Le détail d'une demande PENDING affiche le bouton "Annuler la demande"
- Le détail d'une demande APPROVED affiche "Annuler le congé" avec le BottomSheet de confirmation
- Le détail d'une demande IN_PROGRESS affiche la bannière info (pas de bouton d'annulation)
- Le détail d'une demande COMPLETED/REJECTED/CANCELLED n'affiche pas d'actions d'annulation
- Le toast d'annulation s'affiche pendant 8 secondes après l'annulation
- L'annulation dans les 8s rétablit le statut précédent
- Hors connexion (annulation PENDING) : mise en file d'attente, synchronisée à la reconnexion
- Hors connexion (annulation APPROVED) : bouton désactivé avec message de connexion
Hors périmètre¶
- Enregistrement du retour anticipé par les RH pour les congés en cours (V2)
- Annulation par les RH des congés en cours (géré dans les outils d'administration RH)
- Annulation groupée (Enterprise V2)
- Création de demandes (AB-010)
- Flux d'approbation (AB-012)
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
- [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px
- [ ] Les 5 états UI implémentés (loading, empty, error, offline, success)
- [ ] Le micro-copy français correspond exactement au spec design
- [ ] Tous les critères d'acceptation passent manuellement
- [ ] Tests unitaires écrits et passants
- [ ] Tests d'intégration écrits et passants
- [ ] Isolation multi-tenant vérifiée — annulation filtrée par tenant, RBAC appliqué
- [ ] Offline write+sync fonctionne (Papillon) / N/A (Enterprise) — Annulation PENDING mise en file d'attente hors connexion, annulation APPROVED nécessite une connexion
- [ ] Pas de
anyTypeScript - [ ] Pas de types raw Java — tous les generics explicites
- [ ] Les réponses API suivent le format envelope standard
- [ ] Entrées de piste d'audit pour chaque mutation de données — LeaveCancelled publié, entrée de grand livre CANCEL_CREDIT pour les annulations approuvées