buildEmployee([ ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(1, $phases); self::assertSame(ContractType::H39, $phases[0]->contractType); self::assertTrue($phases[0]->isCurrent); self::assertNull($phases[0]->endDate); self::assertSame(ContractNature::CDI, $phases[0]->contractNature); } public function testThreeConsecutivePeriodsSameSignatureCollapseIntoSinglePhase(): void { $employee = $this->buildEmployee([ ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2021-05-31'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-06-01', 'end' => '2022-05-31'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2022-06-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(1, $phases); self::assertCount(3, $phases[0]->periodIds); self::assertSame('2020-06-01', $phases[0]->startDate->format('Y-m-d')); self::assertNull($phases[0]->endDate); } public function testSwitchFromH39ToForfaitProducesTwoPhasesMostRecentFirst(): void { $employee = $this->buildEmployee([ ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2026-04-30'], ['type' => ContractType::FORFAIT, 'hours' => 39, 'driver' => false, 'start' => '2026-05-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(2, $phases); self::assertSame(ContractType::FORFAIT, $phases[0]->contractType); self::assertTrue($phases[0]->isCurrent); self::assertSame(ContractType::H39, $phases[1]->contractType); self::assertFalse($phases[1]->isCurrent); self::assertSame('2026-04-30', $phases[1]->endDate?->format('Y-m-d')); } public function testInterimBetweenTwoH39PeriodsBreaksThePhases(): void { $employee = $this->buildEmployee([ ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2023-12-31'], ['type' => ContractType::INTERIM, 'hours' => null, 'driver' => false, 'start' => '2024-01-01', 'end' => '2024-04-30'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2024-05-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(3, $phases); self::assertSame(ContractType::H39, $phases[0]->contractType); self::assertSame(ContractType::INTERIM, $phases[1]->contractType); self::assertSame(ContractType::H39, $phases[2]->contractType); } public function testCustomPhasesSplitOnWeeklyHoursChange(): void { $employee = $this->buildEmployee([ ['type' => ContractType::CUSTOM, 'hours' => 28, 'driver' => false, 'start' => '2024-01-01', 'end' => '2024-12-31'], ['type' => ContractType::CUSTOM, 'hours' => 30, 'driver' => false, 'start' => '2025-01-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(2, $phases); self::assertSame(30, $phases[0]->weeklyHours); self::assertSame(28, $phases[1]->weeklyHours); } public function testPhasesSplitOnIsDriverChange(): void { $employee = $this->buildEmployee([ ['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2023-01-01', 'end' => '2024-12-31'], ['type' => ContractType::H35, 'hours' => 35, 'driver' => true, 'start' => '2025-01-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(2, $phases); self::assertTrue($phases[0]->isDriver); self::assertFalse($phases[1]->isDriver); } public function testPhasesEntirelyBeforeDataStartDateAreFilteredOut(): void { // H35 phase ends before 2026-02-23 → must be hidden; H39 phase spans the date → kept. $employee = $this->buildEmployee([ ['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2014-07-01', 'end' => '2025-10-31'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2025-11-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver('2026-02-23')->resolvePhases($employee); self::assertCount(1, $phases); self::assertSame(ContractType::H39, $phases[0]->contractType); } public function testPhaseEndingExactlyOnDataStartDateIsKept(): void { // Edge case: a phase whose endDate equals the data start date is kept // (the inequality is `>= $dataStart`, not strict). $employee = $this->buildEmployee([ ['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2020-01-01', 'end' => '2026-02-23'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2026-02-24', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver('2026-02-23')->resolvePhases($employee); self::assertCount(2, $phases); } public function testNoFilteringWhenDataStartDateIsEmpty(): void { $employee = $this->buildEmployee([ ['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2014-07-01', 'end' => '2020-12-31'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-01-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver()->resolvePhases($employee); self::assertCount(2, $phases); } public function testInvalidDataStartDateStringIsTreatedAsNull(): void { $employee = $this->buildEmployee([ ['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2014-07-01', 'end' => '2020-12-31'], ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-01-01', 'end' => null], ]); $phases = new EmployeeContractPhaseResolver('not-a-date')->resolvePhases($employee); self::assertCount(2, $phases); } /** * @param list $periodsSpec */ private function buildEmployee(array $periodsSpec): Employee { $employee = new Employee(); $id = 0; foreach ($periodsSpec as $spec) { $contract = new Contract(); $contract->setName($spec['type']->value); $contract->setTrackingMode( ContractType::FORFAIT === $spec['type'] ? TrackingMode::PRESENCE->value : TrackingMode::TIME->value ); $contract->setWeeklyHours($spec['hours']); $period = new EmployeeContractPeriod(); $reflection = new ReflectionProperty(EmployeeContractPeriod::class, 'id'); $reflection->setValue($period, ++$id); $period->setEmployee($employee); $period->setContract($contract); $period->setStartDate(new DateTimeImmutable($spec['start'])); $period->setEndDate(null !== $spec['end'] ? new DateTimeImmutable($spec['end']) : null); $period->setContractNature(ContractNature::CDI); $period->setIsDriver($spec['driver']); $employee->getContractPeriods()->add($period); } return $employee; } }