Aller au contenu

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

  1. Charger la demande, déterminer le statut actuel
  2. Appliquer les règles d'annulation :
  3. PENDING_APPROVAL ou PENDING_DOCUMENTCANCELLED_BY_EMPLOYEE, aucun impact sur le solde
  4. APPROVED + startDate > todayCANCELLED_BY_EMPLOYEE, insérer CANCEL_CREDIT dans le grand livre, mettre à jour le solde en cache, publier LeaveCancelled
  5. APPROVED + startDate <= today (IN_PROGRESS) → 422 bloqué "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. Dérogation HR/DG : peut annuler n'importe quel statut sauf COMPLETED → CANCELLED_BY_HR avec motif obligatoire
  8. Définir cancelledAt, cancelledBy
  9. Publier LeaveCancelled avec numberOfDaysCredited
  10. 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 tenantId depuis ScopedValue<TenantContext>
  • Le crédit de solde utilise un verrou pessimiste filtré par tenant
  • LeaveCancelled inclut tenantId pour 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 statut
  • CancelConfirmationSheet — BottomSheet (mobile) / Modal (desktop) avec confirmation renforcée pour les demandes APPROVED
  • UndoToast — 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_CREDIT dans 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 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)

Exigences de test

Tests unitaires

  • LeaveRequestService.cancelRequest() : PENDING_APPROVAL → CANCELLED_BY_EMPLOYEE, aucune entrée de grand livre
  • LeaveRequestService.cancelRequest() : PENDING_DOCUMENT → CANCELLED_BY_EMPLOYEE, aucune entrée de grand livre
  • LeaveRequestService.cancelRequest() : APPROVED + startDate > today → CANCELLED_BY_EMPLOYEE, entrée de grand livre CANCEL_CREDIT, solde incrémenté
  • LeaveRequestService.cancelRequest() : APPROVED + startDate <= today (IN_PROGRESS) → 422
  • LeaveRequestService.cancelRequest() : COMPLETED → 422
  • LeaveRequestService.cancelRequest() : REJECTED → 422
  • LeaveRequestService.cancelRequest() : déjà CANCELLED → 422
  • LeaveRequestService.cancelRequest() : HR_MANAGER annule IN_PROGRESS → CANCELLED_BY_HR (autorisé)
  • LeaveRequestService.cancelRequest() : HR_MANAGER sans motif → 422
  • BalanceMutationService.creditBalance() : crédit correct avec verrou pessimiste

Tests d'intégration

  • PATCH /api/v1/absence/leave-requests/{id}/cancel sur 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 any TypeScript
  • [ ] 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