feat(absences) : fondation backend du module de gestion des absences

Module type Payfit (étapes 1+2 de la spec V1) : demande d'absence, validation
admin, soldes à jour.

- Enums : AbsenceType, AbsenceStatus, HalfDay, ContractType, FamilySituation
- Entités : AbsencePolicy, AbsenceBalance, AbsenceRequest + champs RH sur User
- Services : PublicHolidayProvider (fériés FR métropole en PHP pur, Computus),
  AbsenceDayCalculator (décompte jours ouvrés/ouvrables + demi-journées, TDD),
  AbsenceBalanceService (périodes + pending/taken/recrédit)
- API Platform : providers/processors (création, approve/reject/cancel) + RBAC
  me/admin, contrôleurs preview (dry-run), upload/download justificatif, calendrier
- Migrations : une par table + colonnes RH user (DEFAULT puis DROP DEFAULT)
- Fixtures : 5 policies par défaut, salariés démo, soldes et demandes
- Tests unitaires : PublicHolidayProvider, AbsenceDayCalculator (12 tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-21 14:45:14 +02:00
parent 325a7b07f9
commit de98924fd3
32 changed files with 2554 additions and 3 deletions

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Enum\HalfDay;
use App\Service\AbsenceDayCalculator;
use App\Service\PublicHolidayProvider;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
class AbsenceDayCalculatorTest extends TestCase
{
private AbsenceDayCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new AbsenceDayCalculator(new PublicHolidayProvider());
}
public function testFullWeekIsFiveWorkingDays(): void
{
// Mon 2026-06-01 to Fri 2026-06-05, no holidays that week
self::assertSame(5.0, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-06-01'),
new DateTimeImmutable('2026-06-05'),
));
}
public function testWeekendIsSkipped(): void
{
// Fri 2026-06-05 to Mon 2026-06-08 => Fri + Mon = 2
self::assertSame(2.0, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-06-05'),
new DateTimeImmutable('2026-06-08'),
));
}
public function testHolidayIsSkipped(): void
{
// Thu 2026-05-07 to Fri 2026-05-08 (Victoire 1945) => Thu only = 1
self::assertSame(1.0, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-05-07'),
new DateTimeImmutable('2026-05-08'),
));
}
public function testHalfDayStartSubtractsHalf(): void
{
self::assertSame(4.5, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-06-01'),
new DateTimeImmutable('2026-06-05'),
HalfDay::Afternoon,
));
}
public function testBothHalfDaysSubtractOne(): void
{
self::assertSame(4.0, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-06-01'),
new DateTimeImmutable('2026-06-05'),
HalfDay::Afternoon,
HalfDay::Morning,
));
}
public function testSingleHalfDay(): void
{
self::assertSame(0.5, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-06-01'),
new DateTimeImmutable('2026-06-01'),
HalfDay::Morning,
));
}
public function testWorkingDaysVsOpenDays(): void
{
// Fri 2026-06-05 to Mon 2026-06-08, "ouvrables" includes Saturday
// => Fri + Sat + Mon = 3 (Sunday always skipped)
self::assertSame(3.0, $this->calculator->countWorkingDays(
new DateTimeImmutable('2026-06-05'),
new DateTimeImmutable('2026-06-08'),
null,
null,
false,
));
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\PublicHolidayProvider;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
class PublicHolidayProviderTest extends TestCase
{
private PublicHolidayProvider $provider;
protected function setUp(): void
{
$this->provider = new PublicHolidayProvider();
}
public function testReturnsElevenHolidaysForMetropole(): void
{
self::assertCount(11, $this->provider->getHolidays(2026));
}
public function testFixedHolidaysHaveCorrectLabels(): void
{
$holidays = $this->provider->getHolidays(2026);
self::assertSame('Jour de l\'an', $holidays['2026-01-01']);
self::assertSame('Fête du Travail', $holidays['2026-05-01']);
self::assertSame('Victoire 1945', $holidays['2026-05-08']);
self::assertSame('Fête nationale', $holidays['2026-07-14']);
self::assertSame('Assomption', $holidays['2026-08-15']);
self::assertSame('Toussaint', $holidays['2026-11-01']);
self::assertSame('Armistice 1918', $holidays['2026-11-11']);
self::assertSame('Noël', $holidays['2026-12-25']);
}
/**
* Easter 2026 is April 5th, so Easter Monday is April 6th,
* Ascension is May 14th and Whit Monday is May 25th.
*/
public function testEasterBasedHolidays2026(): void
{
$holidays = $this->provider->getHolidays(2026);
self::assertSame('Lundi de Pâques', $holidays['2026-04-06']);
self::assertSame('Ascension', $holidays['2026-05-14']);
self::assertSame('Lundi de Pentecôte', $holidays['2026-05-25']);
}
/**
* Easter 2025 is April 20th, so Easter Monday is April 21st.
*/
public function testEasterBasedHolidays2025(): void
{
$holidays = $this->provider->getHolidays(2025);
self::assertSame('Lundi de Pâques', $holidays['2025-04-21']);
}
public function testIsHoliday(): void
{
self::assertTrue($this->provider->isHoliday(new DateTimeImmutable('2026-05-01')));
self::assertTrue($this->provider->isHoliday(new DateTimeImmutable('2026-05-14')));
self::assertFalse($this->provider->isHoliday(new DateTimeImmutable('2026-05-02')));
self::assertFalse($this->provider->isHoliday(new DateTimeImmutable('2026-06-01')));
}
}