fix(absences) : garde-fou solde négatif à l'approbation + cohérence fixture

- AbsenceBalanceService::availableForRequest() : jours disponibles (acquis N-1
  + en cours N − pris) pour la période de la demande, null si type non suivi.
- Blocage de l'approbation si countedDays > disponible, dans les deux chemins
  (REST AbsenceReviewProcessor + MCP ReviewAbsenceRequestTool), comme le motif
  décès. Les CP en cours d'acquisition restent posables, mais pas au-delà du
  droit total (plus de solde négatif silencieux à l'approbation).
- Fixture : demande pending CP d'alice replacée dans sa période de référence
  2025-2026 (26→29/05/2026, 4 j ouvrés) et solde pending aligné (5 → 4) ;
  plus de "en attente" orphelin non lié à une demande.
- Test fonctionnel testApproveBeyondAvailableBalanceIsBlocked + employé de test
  doté d'un droit pour que les approbations existantes passent le garde-fou.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-22 16:48:49 +02:00
parent f9773b3a5e
commit 65df36dd1a
5 changed files with 89 additions and 4 deletions
+11
View File
@@ -58,6 +58,17 @@ final readonly class AbsenceReviewProcessor implements ProcessorInterface
assert($admin instanceof User);
if ($isApprove) {
// Never let an approval push the balance below zero (CP only): the
// days being accrued (N) are posable, but not beyond the entitlement.
$available = $this->balanceService->availableForRequest($data);
if (null !== $available && $data->getCountedDays() > $available + 1e-9) {
throw new UnprocessableEntityHttpException(sprintf(
'Approving this request would put the balance below zero: %g day(s) requested but only %g available.',
$data->getCountedDays(),
$available,
));
}
$data->setStatus(AbsenceStatus::Approved);
$data->setRejectionReason(null);
$this->balanceService->applyApproval($data);