validator = new EmployeeContractPeriodValidator(); } public function testAssertPeriodDatesRejectsEndBeforeStart(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('contractEndDate cannot be before contractStartDate.'); $this->validator->assertPeriodDates( new DateTimeImmutable('2026-03-10'), new DateTimeImmutable('2026-03-01'), ContractNature::CDD ); } public function testAssertPeriodDatesRejectsMissingEndDateForCdd(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('contractEndDate is required for CDD and INTERIM.'); $this->validator->assertPeriodDates( new DateTimeImmutable('2026-03-01'), null, ContractNature::CDD ); } public function testAssertPeriodDatesRejectsEndDateForCdiWhenNotAllowed(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('contractEndDate must be empty for CDI.'); $this->validator->assertPeriodDates( new DateTimeImmutable('2026-03-01'), new DateTimeImmutable('2026-03-10'), ContractNature::CDI ); } public function testAssertCloseEndDateCanBeAppliedRejectsIncrease(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('contractEndDate cannot be increased on current contract.'); $this->validator->assertCloseEndDateCanBeApplied( new DateTimeImmutable('2026-03-01'), new DateTimeImmutable('2026-03-10'), new DateTimeImmutable('2026-03-11'), ContractNature::CDI ); } public function testAssertNextStartDateCompatibleRejectsWhenNotAfterCurrentOpenStart(): void { $currentPeriod = $this->buildCurrentPeriod('2026-03-05', null); $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('contractStartDate must be after current contract start date.'); $this->validator->assertNextStartDateCompatible(new DateTimeImmutable('2026-03-05'), $currentPeriod); } public function testAssertNextStartDateCompatibleRejectsWhenNotAfterCurrentClosedEnd(): void { $currentPeriod = $this->buildCurrentPeriod('2026-03-01', '2026-03-10'); $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('contractStartDate must be after current contract end date.'); $this->validator->assertNextStartDateCompatible(new DateTimeImmutable('2026-03-10'), $currentPeriod); } public function testAssertWorkDaysHoursAcceptsNullForStandardContract(): void { $this->validator->assertWorkDaysHours($this->buildContract(35), ContractNature::CDI, null); self::assertTrue(true); // no exception } public function testAssertWorkDaysHoursRejectsScheduleOn35hContract(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->validator->assertWorkDaysHours($this->buildContract(35), ContractNature::CDI, [1 => 120]); } public function testAssertWorkDaysHoursRejectsScheduleOnForfaitContract(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->validator->assertWorkDaysHours($this->buildContract(null, Contract::TRACKING_PRESENCE), ContractNature::CDI, [1 => 120]); } public function testAssertWorkDaysHoursAcceptsNullForInterim(): void { $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::INTERIM, null); self::assertTrue(true); } public function testAssertWorkDaysHoursRequiresScheduleForCustomContract(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('workDaysHours is required'); $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, null); } public function testAssertWorkDaysHoursRequiresScheduleForCustomContractOnEmptyArray(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('workDaysHours is required'); $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, []); } public function testAssertWorkDaysHoursRejectsIsoOutsideOneToFive(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('iso weekdays 1-5'); $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, [6 => 120, 7 => 120]); } public function testAssertWorkDaysHoursRejectsIsoZero(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('iso weekdays 1-5'); $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, [0 => 240]); } public function testAssertWorkDaysHoursRejectsNegativeMinutes(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('non-negative integer minutes'); $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, [1 => -120, 4 => 360]); } public function testAssertWorkDaysHoursRejectsSumMismatch(): void { $this->expectException(UnprocessableEntityHttpException::class); $this->expectExceptionMessage('total must equal contract weekly hours'); $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, [1 => 60, 4 => 60]); } public function testAssertWorkDaysHoursAcceptsValidScheduleFor4hContract(): void { $this->validator->assertWorkDaysHours($this->buildContract(4), ContractNature::CDI, [1 => 120, 4 => 120]); self::assertTrue(true); } private function buildContract(?int $weeklyHours, string $trackingMode = Contract::TRACKING_TIME): Contract { return new Contract() ->setName('Test') ->setTrackingMode($trackingMode) ->setWeeklyHours($weeklyHours) ; } private function buildCurrentPeriod(string $startDate, ?string $endDate): EmployeeContractPeriod { $contract = new Contract() ->setName('35h') ->setTrackingMode(Contract::TRACKING_TIME) ->setWeeklyHours(35) ; $employee = new Employee() ->setFirstName('Test') ->setLastName('User') ->setContract($contract) ; return new EmployeeContractPeriod() ->setEmployee($employee) ->setContract($contract) ->setStartDate(new DateTimeImmutable($startDate)) ->setEndDate(null !== $endDate ? new DateTimeImmutable($endDate) : null) ->setContractNature(ContractNature::CDI) ; } }