From a552843cec6e5e10526929fb625aa23b49e94c38 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 11 Jun 2026 11:48:27 +0200 Subject: [PATCH] feat(night-contingent) : builder agregation mensuelle des heures de nuit --- .../NightContingentExportBuilder.php | 86 +++++++++++++++++ .../NightContingentExportBuilderTest.php | 94 +++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/Service/WorkHours/NightContingentExportBuilder.php create mode 100644 tests/Service/WorkHours/NightContingentExportBuilderTest.php diff --git a/src/Service/WorkHours/NightContingentExportBuilder.php b/src/Service/WorkHours/NightContingentExportBuilder.php new file mode 100644 index 0000000..c9dae57 --- /dev/null +++ b/src/Service/WorkHours/NightContingentExportBuilder.php @@ -0,0 +1,86 @@ += 4h de nuit dans la journee). Fenetre 21h->6h via + * NightHoursCalculator. Conducteurs : minutes saisies (nightHoursMinutes). + * Aucun credit absence/ferie : seules les heures reellement travaillees comptent. + */ +final readonly class NightContingentExportBuilder +{ + private const int NIGHT_DAY_THRESHOLD_MINUTES = 240; + + public function __construct( + private WorkHourReadRepositoryInterface $workHourRepository, + private EmployeeContractResolver $contractResolver, + private NightHoursCalculator $nightHoursCalculator, + ) {} + + /** + * @param list $employees + * + * @return list + */ + public function buildRows(array $employees, int $year): array + { + $from = new DateTimeImmutable(sprintf('%d-01-01', $year)); + $to = new DateTimeImmutable(sprintf('%d-12-31', $year)); + + $workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees); + + $byEmployee = []; + foreach ($workHours as $wh) { + $employeeId = $wh->getEmployee()?->getId(); + if (null === $employeeId) { + continue; + } + $byEmployee[$employeeId][] = $wh; + } + + $rows = []; + foreach ($employees as $employee) { + $employeeId = $employee->getId(); + if (null === $employeeId) { + continue; + } + + $months = []; + for ($m = 1; $m <= 12; ++$m) { + $months[$m] = ['nightMinutes' => 0, 'nightDays' => 0]; + } + + foreach ($byEmployee[$employeeId] ?? [] as $wh) { + $date = DateTimeImmutable::createFromInterface($wh->getWorkDate()); + $isDriver = $this->contractResolver->resolveIsDriverForEmployeeAndDate($employee, $date); + $nightMin = $this->nightHoursCalculator->nightMinutesForWorkHour($wh, $isDriver); + if ($nightMin <= 0) { + continue; + } + + $month = (int) $date->format('n'); + $months[$month]['nightMinutes'] += $nightMin; + if ($nightMin >= self::NIGHT_DAY_THRESHOLD_MINUTES) { + ++$months[$month]['nightDays']; + } + } + + $rows[] = new NightContingentRow( + employeeId: $employeeId, + employeeName: trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')), + months: $months, + ); + } + + return $rows; + } +} diff --git a/tests/Service/WorkHours/NightContingentExportBuilderTest.php b/tests/Service/WorkHours/NightContingentExportBuilderTest.php new file mode 100644 index 0000000..7e75714 --- /dev/null +++ b/tests/Service/WorkHours/NightContingentExportBuilderTest.php @@ -0,0 +1,94 @@ +makeEmployee(1, 'Dupont', 'Jean'); + + // Janvier : un jour 4h de nuit (>=240 -> 1 jour) + un jour 3h59 (<240 -> 0 jour). + $whFull = new WorkHour()->setEmployee($employee) + ->setWorkDate(new DateTimeImmutable('2026-01-10')) + ->setEveningFrom('21:00')->setEveningTo('01:00') // 240 min nuit + ; + $whShort = new WorkHour()->setEmployee($employee) + ->setWorkDate(new DateTimeImmutable('2026-01-11')) + ->setEveningFrom('21:00')->setEveningTo('00:59') // 239 min nuit + ; + + $workHourRepo = $this->createStub(WorkHourReadRepositoryInterface::class); + $workHourRepo->method('findByDateRangeAndEmployees')->willReturn([$whFull, $whShort]); + + $contractResolver = $this->createStub(EmployeeContractResolver::class); + $contractResolver->method('resolveIsDriverForEmployeeAndDate')->willReturn(false); + + $builder = new NightContingentExportBuilder( + $workHourRepo, + $contractResolver, + new NightHoursCalculator(), + ); + + $rows = $builder->buildRows([$employee], 2026); + + self::assertCount(1, $rows); + self::assertSame(479, $rows[0]->months[1]['nightMinutes']); // 240 + 239 + self::assertSame(1, $rows[0]->months[1]['nightDays']); // seul le jour >=240 + self::assertSame(0, $rows[0]->months[2]['nightMinutes']); // fevrier vide + self::assertSame(0, $rows[0]->months[2]['nightDays']); + } + + public function testDriverUsesManualNightMinutes(): void + { + $employee = $this->makeEmployee(2, 'Martin', 'Paul'); + + $wh = new WorkHour()->setEmployee($employee) + ->setWorkDate(new DateTimeImmutable('2026-03-05')) + ->setNightHoursMinutes(300) + ->setMorningFrom('08:00')->setMorningTo('12:00') // ignore (driver) + ; + + $workHourRepo = $this->createStub(WorkHourReadRepositoryInterface::class); + $workHourRepo->method('findByDateRangeAndEmployees')->willReturn([$wh]); + + $contractResolver = $this->createStub(EmployeeContractResolver::class); + $contractResolver->method('resolveIsDriverForEmployeeAndDate')->willReturn(true); + + $builder = new NightContingentExportBuilder( + $workHourRepo, + $contractResolver, + new NightHoursCalculator(), + ); + + $rows = $builder->buildRows([$employee], 2026); + + self::assertSame(300, $rows[0]->months[3]['nightMinutes']); + self::assertSame(1, $rows[0]->months[3]['nightDays']); // 300 >= 240 + } + + private function makeEmployee(int $id, string $last, string $first): Employee + { + $employee = new Employee(); + $employee->setLastName($last)->setFirstName($first); + $ref = new ReflectionProperty(Employee::class, 'id'); + $ref->setValue($employee, $id); + + return $employee; + } +}