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

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Tests\Functional\Mcp;
use App\Entity\AbsenceBalance;
use App\Entity\AbsencePolicy;
use App\Entity\User;
use App\Enum\AbsenceType;
@@ -62,6 +63,15 @@ class AbsenceRequestLifecycleTest extends KernelTestCase
$policy->setActive(true);
}
// Entitlement so CP requests can be approved without breaching the
// no-negative-balance guard (period of the June 2026 test requests).
$balance = new AbsenceBalance();
$balance->setUser($this->employee);
$balance->setType(AbsenceType::PaidLeave);
$balance->setPeriod('2026-2027');
$balance->setAcquired(25.0);
$this->em->persist($balance);
$this->em->flush();
}
@@ -110,6 +120,33 @@ class AbsenceRequestLifecycleTest extends KernelTestCase
self::assertSame(5.0, $balance->getTaken());
}
public function testApproveBeyondAvailableBalanceIsBlocked(): void
{
$created = json_decode(
($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
true,
);
// Shrink the entitlement below the 5 requested days.
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
;
$balance->setAcquired(2.0);
$balance->setAcquiring(0.0);
$this->em->flush();
try {
($this->reviewTool($this->admin))($created['id'], 'approve');
self::fail('Expected approval to be blocked when it would breach the balance.');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString('below zero', $e->getMessage());
}
// Approval bailed out before mutating: nothing moved to taken, days stay reserved.
self::assertSame(0.0, $balance->getTaken());
self::assertSame(5.0, $balance->getPending());
}
public function testAdminCancelApprovedReleasesTaken(): void
{
$created = json_decode(