feat(night-contingent) : builder agregation mensuelle des heures de nuit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\WorkHours;
|
||||
|
||||
use App\Dto\WorkHours\NightContingentRow;
|
||||
use App\Entity\Employee;
|
||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Construit, par employe, les totaux mensuels d'heures de nuit et le nombre de
|
||||
* nuits travaillees (>= 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<Employee> $employees
|
||||
*
|
||||
* @return list<NightContingentRow>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service\WorkHours;
|
||||
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\WorkHour;
|
||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\NightContingentExportBuilder;
|
||||
use App\Service\WorkHours\NightHoursCalculator;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionProperty;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class NightContingentExportBuilderTest extends TestCase
|
||||
{
|
||||
public function testAggregatesNightMinutesAndDaysPerMonth(): void
|
||||
{
|
||||
$employee = $this->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user