security = $this->createStub(Security::class); $this->employeeRepository = $this->createStub(EmployeeScopedRepositoryInterface::class); $this->workHourRepository = $this->createStub(WorkHourReadRepositoryInterface::class); $this->absenceRepository = $this->createStub(AbsenceReadRepositoryInterface::class); $this->requestStack = new RequestStack(); } public function testThrowsWhenAnonymous(): void { $this->security->method('getUser')->willReturn(null); $provider = new WorkHourWeeklySummaryProvider( $this->security, $this->requestStack, $this->employeeRepository, $this->workHourRepository, $this->absenceRepository, new AbsenceSegmentsResolver(), new WorkedHoursCreditPolicy($this->buildResolverStub()), $this->buildResolverStub() ); $this->expectException(AccessDeniedHttpException::class); $provider->provide(new Get()); } public function testBuildsWeeklyRowsWithOvertimeAndPresence(): void { $user = new User(); $timeEmployee = $this->buildEmployee(1, 'TIME', 35, 'Alice'); $presenceEmployee = $this->buildEmployee(2, 'PRESENCE', null, 'Bob'); $interimEmployee = $this->buildEmployee(3, 'TIME', 35, 'Charly', 'Interim'); $employees = [$timeEmployee, $presenceEmployee, $interimEmployee]; $workHours = []; foreach (['2026-02-16', '2026-02-17', '2026-02-18', '2026-02-19', '2026-02-20'] as $date) { $workHours[] = new WorkHour() ->setEmployee($timeEmployee) ->setWorkDate(new DateTimeImmutable($date)) ->setMorningFrom('09:00') ->setMorningTo('19:00') ; $workHours[] = new WorkHour() ->setEmployee($interimEmployee) ->setWorkDate(new DateTimeImmutable($date)) ->setMorningFrom('09:00') ->setMorningTo('19:00') ; } $absenceType = new AbsenceType() ->setCode('CP') ->setLabel('Congé') ->setColor('#000') ->setCountAsWorkedHours(true) ; $presenceAbsence = new Absence() ->setEmployee($presenceEmployee) ->setType($absenceType) ->setStartDate(new DateTime('2026-02-16')) ->setEndDate(new DateTime('2026-02-16')) ->setStartHalf(HalfDay::AM) ->setEndHalf(HalfDay::PM) ; $this->requestStack->push(new Request(query: ['weekStart' => '2026-02-16'])); $this->security->method('getUser')->willReturn($user); $this->employeeRepository->method('findScoped')->with($user)->willReturn($employees); $this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn($workHours); $this->absenceRepository->method('findForPrint')->willReturn([$presenceAbsence]); $provider = new WorkHourWeeklySummaryProvider( $this->security, $this->requestStack, $this->employeeRepository, $this->workHourRepository, $this->absenceRepository, new AbsenceSegmentsResolver(), new WorkedHoursCreditPolicy($this->buildResolverStub()), $this->buildWeeklyResolverStub($employees) ); $result = $provider->provide(new Get()); self::assertSame('2026-02-16', $result->weekStart); self::assertSame('2026-02-22', $result->weekEnd); self::assertCount(3, $result->rows); self::assertSame(3000, $result->rows[0]->weeklyTotalMinutes); self::assertSame(900, $result->rows[0]->weeklyOvertimeTotalMinutes); self::assertSame(120, $result->rows[0]->weeklyOvertime25Minutes); self::assertSame(210, $result->rows[0]->weeklyOvertime50Minutes); self::assertSame(1230, $result->rows[0]->weeklyRecoveryMinutes); self::assertSame(0.0, $result->rows[1]->weeklyPresenceCount); self::assertTrue($result->rows[1]->daily[0]->hasAbsence); self::assertSame('Congé', $result->rows[1]->daily[0]->absenceLabel); self::assertSame('#000', $result->rows[1]->daily[0]->absenceColor); self::assertSame(0, $result->rows[1]->weeklyOvertimeTotalMinutes); self::assertSame(0, $result->rows[2]->weeklyOvertime25Minutes); self::assertSame(0, $result->rows[2]->weeklyOvertime50Minutes); self::assertSame(0, $result->rows[2]->weeklyRecoveryMinutes); self::assertSame(900, $result->rows[2]->weeklyOvertimeTotalMinutes); } private function buildEmployee(int $id, string $trackingMode, ?int $weeklyHours, string $firstName, ?string $contractName = null): Employee { $contract = new Contract() ->setName($contractName ?? $trackingMode) ->setTrackingMode($trackingMode) ->setWeeklyHours($weeklyHours) ; $employee = new Employee() ->setFirstName($firstName) ->setLastName('Test') ->setContract($contract) ; $this->setEntityId($employee, $id); return $employee; } private function setEntityId(object $entity, int $id): void { $reflection = new ReflectionObject($entity); $property = $reflection->getProperty('id'); $property->setAccessible(true); $property->setValue($entity, $id); } private function buildResolverStub(): EmployeeContractResolver { $resolver = $this->createStub(EmployeeContractResolver::class); $resolver ->method('resolveForEmployeeAndDate') ->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract()) ; $resolver ->method('resolveForEmployeesAndDays') ->willReturn([]) ; return $resolver; } /** * @param list $employees */ private function buildWeeklyResolverStub(array $employees): EmployeeContractResolver { $resolver = $this->createStub(EmployeeContractResolver::class); $resolver ->method('resolveForEmployeeAndDate') ->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract()) ; $resolver ->method('resolveForEmployeesAndDays') ->willReturnCallback(static function (array $scopedEmployees, array $days): array { $map = []; foreach ($scopedEmployees as $employee) { $employeeId = $employee->getId(); if (!$employeeId) { continue; } foreach ($days as $day) { $map[$employeeId][$day] = $employee->getContract(); } } return $map; }) ; return $resolver; } }