feat(absences) : mise en conformité légale (événements familiaux, demi-journée, CCN)

Périmètre 1-6 du design 2026-05-22-absence-legal-compliance-fixes (points
lourds — ancienneté, CP pendant maladie, rétention — reportés en backlog).

- Événements familiaux sans solde : AbsenceType::decrementsBalance() ne vaut
  true que pour les CP. Mariage/PACS, naissance, décès = droits par événement ;
  congé parental = suspension ; maladie = Sécu. Plus de solde fantôme.
- Décès : daysPerEvent = null (selon lien de parenté) + motif obligatoire à la
  création (REST + MCP), les minimums légaux étant rappelés dans l'aide.
- Ajout du congé naissance (type, policy 3 j, justificatif, libellés/couleur front).
- Garde-fou demi-journée : -0,5 appliqué uniquement si le jour-borne est
  réellement décompté (corrige un sous-décompte week-end/férié) — TDD.
- CCN documentée : paramètre app.absence.convention = "Syntec (IDCC 1486)",
  rappelée en sous-titre admin et dans l'aide /help.

Tests : AbsenceDayCalculatorTest (garde-fou demi-journée), AbsenceRequestLifecycle
(motif décès obligatoire + aucun solde touché). make test 52/52, build Nuxt OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-22 16:00:28 +02:00
parent e9aaccc62c
commit f9773b3a5e
12 changed files with 123 additions and 25 deletions

View File

@@ -17,6 +17,7 @@ use App\Repository\UserRepository;
use App\Service\AbsenceBalanceService;
use App\Service\AbsenceDayCalculator;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;
@@ -127,6 +128,42 @@ class AbsenceRequestLifecycleTest extends KernelTestCase
self::assertSame(0.0, $balance->getPending());
}
public function testBereavementRequiresReasonAndDoesNotTouchBalance(): void
{
// Ensure an active bereavement policy exists (no fixed entitlement).
$policy = $this->em->getRepository(AbsencePolicy::class)->findOneBy(['type' => AbsenceType::Bereavement]);
if (null === $policy) {
$policy = new AbsencePolicy();
$policy->setType(AbsenceType::Bereavement);
$policy->setCountWorkingDaysOnly(true);
$this->em->persist($policy);
}
$policy->setActive(true);
$this->em->flush();
$tool = $this->createTool($this->admin);
// Without a reason → rejected.
try {
($tool)($this->employee->getId(), 'deces', '2026-06-10', '2026-06-12');
self::fail('Expected an exception for bereavement without a reason.');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString('reason', $e->getMessage());
}
// With a reason → accepted, and no balance row is created (per-event right).
$data = json_decode(
($tool)($this->employee->getId(), 'deces', '2026-06-10', '2026-06-12', null, null, 'Décès grand-parent'),
true,
);
self::assertSame('pending', $data['status']);
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
->findOneForPeriod($this->employee, AbsenceType::Bereavement, '2026')
;
self::assertNull($balance, 'Bereavement must not create or touch any balance.');
}
private function securityFor(User $user): Security
{
$security = $this->createMock(Security::class);