Aller au contenu

Story AB-020 : Ajustement Manuel du Solde (Backend + Frontend)

Module : absence-management Tranche : VS-AB-015 de l'architecture Contexte de marque : [LES DEUX] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 20 Dépend de : AB-012 Parallèle avec : AB-021 Ordre de fusion : Ordre quelconque avec AB-021 Complexité estimée : S PRD User Stories : N/A - Technical Story


Objectif

Permettre aux responsables RH et aux DG d'ajuster manuellement le solde de congé d'un employé (ajouter ou retirer des jours) avec un motif obligatoire, en créant une entrée de grand livre MANUAL_ADJUSTMENT auditée. Utilise le même pattern de verrouillage pessimiste que l'approbation de congé (depuis BalanceMutationService d'AB-012) pour éviter la corruption concurrente des soldes.


Périmètre Backend

Entités et Enregistrements

Aucune nouvelle entité. Cette story étend le modèle de grand livre existant d'AB-012.

Type de transaction d'entrée de grand livre (étendre l'enum) :

Valeur Description
MANUAL_ADJUSTMENT Ajustement manuel du solde par RH/DG — montant signé (positif=ajouter, négatif=retirer)

DTOs :

public record AdjustBalanceRequest(
    @NotBlank String leaveType,           // "ANNUAL" (MVP uniquement)
    @NotNull BigDecimal amount,           // positif=ajouter, négatif=retirer, jamais zéro
    @NotBlank @Size(min = 10) String reason,
    @NotNull LeaveUnit unit              // OUVRABLES, CALENDAIRES, OUVRES
) {}

Couche Repository

Aucun nouveau repository. Utilise les repositories de grand livre et LeaveEmployeeProfile existants d'AB-012.

Couche Service

BalanceMutationService.java (extension depuis AB-012) : - adjustBalance(UUID tenantId, UUID employeeId, String leaveType, BigDecimal amount, String reason, LeaveUnit unit) -> LeaveEmployeeProfile : 1. Acquérir le verrou pessimiste sur LeaveEmployeeProfile (SELECT FOR UPDATE) 2. Convertir le montant en jours ouvrables si l'unité d'entrée est CALENDAIRES ou OUVRES 3. Valider que le solde résultant n'est pas négatif (bloquer si le retrait ferait passer le solde en dessous de 0) 4. Insérer l'entrée de grand livre MANUAL_ADJUSTMENT (signé : positif pour ajouter, négatif pour retirer) 5. Mettre à jour le cache annual_balance_ouvrables 6. Publier l'événement AuditEntryRequested avec motif, montant, employeeId, performedBy 7. Libérer le verrou (fin de transaction)

Points de terminaison API

Méthode Chemin Corps de la requête Réponse Auth
PATCH /api/v1/absence/employees/{employeeId}/adjust-balance AdjustBalanceRequest LeaveEmployeeProfileResponse HR_MANAGER, DG

Règles de validation

  • reason est obligatoire : non vide, minimum 10 caractères
  • amount ne doit pas être zéro
  • leaveType doit être "ANNUAL" (MVP — seul le solde annuel est ajustable)
  • unit doit être une valeur d'enum LeaveUnit valide
  • Le solde résultant ne doit pas être négatif (bloquer si le retrait causerait un solde < 0)
  • Seuls les rôles HR_MANAGER ou DG peuvent invoquer cet endpoint

Considérations Multi-Tenant

  • Toutes les opérations limitées par tenant_id depuis TenantResolutionFilter
  • Le verrou pessimiste est isolé par tenant (SELECT FOR UPDATE sur le LeaveEmployeeProfile limité au tenant)
  • L'entrée de grand livre enregistre tenant_id pour la piste d'audit
  • L'employé doit appartenir au même tenant que l'utilisateur authentifié

Migrations Flyway

Aucune nouvelle migration. MANUAL_ADJUSTMENT fait déjà partie de l'enum transaction_type depuis la configuration du grand livre d'AB-012.


Périmètre Frontend

Pages et Routes

  • /absences/employes/:id/ajustement — Page de formulaire d'ajustement manuel du solde
  • Accès : HR_MANAGER, DG

Composants

  • ManualBalanceAdjustmentPage.tsx — Page complète (mobile) / Modal (desktop) pour l'ajustement du solde
  • BalancePreviewCard.tsx — Affiche le solde actuel et le nouveau solde calculé en temps réel
  • AdjustmentForm.tsx — Formulaire avec radio de direction, champ de saisie du montant, textarea du motif

États UI (TOUS REQUIS)

État Comportement Copie Française
Chargement Squelette pour la zone d'affichage du solde
Vide N/A (le formulaire s'affiche toujours)
Erreur (résultat négatif) Erreur inline sous le champ de saisie du montant "Le solde ne peut pas etre negatif."
Erreur (aucun motif) Erreur inline sur le textarea "Veuillez indiquer le motif de l'ajustement."
Erreur (motif trop court) Erreur inline sur le textarea "Le motif doit contenir au moins 10 caracteres."
Erreur (API) Toast "Impossible d'effectuer l'ajustement."
Hors ligne Formulaire bloqué, bandeau affiché "Cette action necessite une connexion internet"
Succès Toast + navigation retour "Solde ajuste -- {+/-n} jours"

Comportement Responsive

  • 360px (mobile) : Mise en page pleine page.
  • Titre de page : "Ajustement de solde" avec sous-titre nom de l'employé
  • Affichage du solde actuel : "Solde actuel : {n} JC"
  • RadioGroup : "Ajouter des jours" / "Retirer des jours"
  • Input (type=number) : "Nombre de jours (calendaires)"
  • Calcul en temps réel : "Nouveau solde : {n} JC" (se met à jour pendant la saisie)
  • Textarea (obligatoire) : label "Motif (obligatoire)", placeholder "Expliquez la raison..."
  • Bandeau d'avertissement : "Cet ajustement sera enregistre dans l'historique d'audit."
  • Button (primary, full-width) : "Confirmer l'ajustement"
  • 768px (tablette) : Même mise en page, formulaire plus large avec max-width 500px centré.
  • 1024px+ (desktop) : Modal ou panneau latéral, max-width 500px. Même contenu de formulaire.

Interactions

  • Bascule de direction : RadioGroup bascule entre "Ajouter des jours" et "Retirer des jours". Le basculement met à jour le signe du calcul en temps réel.
  • Aperçu du solde en temps réel : Au fur et à mesure que l'utilisateur saisit un montant, "Nouveau solde : {n} JC" se met à jour en temps réel. Si le résultat serait négatif, afficher une erreur inline immédiatement.
  • Envoi : Appelle l'endpoint PATCH. En cas de succès, toast "Solde ajuste -- {+/-n} jours" et navigation vers le détail du solde employé (AB-021).
  • Blocage hors ligne : Si l'appareil est hors ligne, les champs du formulaire sont désactivés et un bandeau est affiché. Aucune mise en file d'attente hors ligne pour cette action.

Critères d'Acceptation

AC-064 : Le RH peut ajuster le solde de n'importe quel employé
Étant donné que je suis authentifié en tant que HR_MANAGER ou DG
Quand je soumets un ajustement manuel du solde pour un employé avec amount=5, reason="Rattrapage jours non comptabilises en janvier", unit=OUVRABLES
Alors le solde est mis à jour de +5 jours ouvrables
Et une entrée de grand livre MANUAL_ADJUSTMENT est créée avec un montant positif, le motif et created_by
Et un événement AuditEntryRequested est publié

AC-065 : L'ajustement nécessite un motif obligatoire
Étant donné que j'essaie de soumettre un ajustement du solde
Quand le champ motif est vide ou inférieur à 10 caractères
Alors la soumission est bloquée avec "Veuillez indiquer le motif de l'ajustement."
Et aucune entrée de grand livre n'est créée

AC-066 : L'ajustement est audité dans le grand livre
Étant donné qu'un ajustement manuel est soumis avec motif "Correction suite a erreur de saisie" et amount=-3
Quand l'ajustement est traité
Alors une entrée de grand livre MANUAL_ADJUSTMENT est créée avec amount=-3, reason="Correction suite a erreur de saisie" et created_by={utilisateur actuel}
Et un événement AuditEntryRequested est publié
Et le cache annual_balance_ouvrables est décrémenté de 3

Règles OHADA et Réglementaires

  • FR-ABS-120 : Types de transactions du grand livre — MANUAL_ADJUSTMENT est un type de transaction valide pour le grand livre du solde de congé
  • FR-ABS-121 : Champs de l'entrée de grand livre — chaque entrée doit inclure : date, transaction_type, amount, reason, created_by, employee_id, tenant_id
  • FR-ABS-123 : Grand livre immuable — les corrections sont toujours de nouvelles entrées, jamais des modifications d'entrées existantes. Un MANUAL_ADJUSTMENT pour corriger une erreur précédente crée une nouvelle entrée compensatrice.

Standards et Conventions

  • docs/standards/java-spring-guidelines.md — Verrouillage pessimiste, patterns couche service, publication d'événements
  • docs/standards/database-guidelines.md — BigDecimal pour les calculs financiers/de solde, isolation tenant
  • docs/standards/api-guidelines.md — Sémantique PATCH, enveloppe ApiResponse standard, formats d'erreur 403/422
  • docs/standards/react-typescript-guidelines.md — Patterns de formulaires, validation inline, notifications toast
  • docs/standards/design-system.md — Composants RadioGroup, Input, Button, Toast, Banner
  • docs/standards/offline-sync-guidelines.md — Pattern d'action en ligne uniquement (pas de mise en file d'attente hors ligne)
  • docs/standards/multi-datasource-patterns.md — Accès aux données limité au tenant via JdbcClient

Exigences de Test

Tests Unitaires

  • BalanceMutationService : adjustBalance avec montant positif (ajouter des jours)
  • BalanceMutationService : adjustBalance avec montant négatif (retirer des jours)
  • BalanceMutationService : rejeter l'ajustement qui causerait un solde négatif
  • BalanceMutationService : rejeter le motif vide ou court (< 10 caractères)
  • BalanceMutationService : rejeter le montant zéro
  • BalanceMutationService : conversion d'unité (CALENDAIRES/OUVRES vers OUVRABLES)

Tests d'Intégration

  • PATCH /api/v1/absence/employees/{id}/adjust-balance — flux complet avec verrouillage pessimiste
  • Endpoint PATCH — 403 pour le rôle EMPLOYE (non autorisé)
  • Endpoint PATCH — 422 pour solde résultant négatif
  • Endpoint PATCH — 422 pour motif manquant/court
  • Isolation tenant : l'ajustement pour l'employé du tenant A n'est pas accessible depuis le tenant B
  • Événement d'audit publié avec le payload correct

Ce que QA Validera

  • Le formulaire d'ajustement s'affiche correctement sur mobile (360px), tablette (768px), desktop (1024px+)
  • L'aperçu du solde en temps réel se met à jour pendant la saisie du montant
  • Les erreurs de validation inline s'affichent pour : motif vide, motif court, solde résultant négatif
  • L'état hors ligne bloque le formulaire et affiche le bandeau
  • Le toast de succès s'affiche et navigue vers le détail du solde
  • L'entrée de grand livre est visible dans le détail du solde employé (AB-021) après l'ajustement

Hors Périmètre

  • Ajustements de solde en masse (ajuster plusieurs employés à la fois) — Enterprise V2
  • Ajustement des compteurs de congé maladie ou exceptionnel — MVP ajuste uniquement le solde annuel
  • Workflow d'approbation pour les ajustements — MVP permet l'ajustement direct par RH/DG sans approbation secondaire
  • UI d'annulation/inversion d'ajustement — les corrections sont de nouvelles entrées MANUAL_ADJUSTMENT conformément à FR-ABS-123

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
  • [ ] Tous les états UI implémentés (chargement, variantes d'erreur, hors ligne, succès)
  • [ ] La micro-copie française correspond exactement à la spécification 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 (aucune fuite de données inter-tenant)
  • [ ] Synchronisation hors ligne (Papillon) — N/A (action en ligne uniquement)
  • [ ] Pas de any TypeScript — tous les types explicites
  • [ ] Pas de types bruts Java — tous les génériques explicites
  • [ ] Les réponses API suivent le format d'enveloppe standard
  • [ ] Entrées de piste d'audit créées pour chaque mutation de données