buildEmployeeWithId(42); $employee->setContractEndDate('2026-03-05'); $employee->setContractPaidLeaveSettled(true); $contract = $employee->getContract(); self::assertInstanceOf(Contract::class, $contract); $todayPeriod = new EmployeeContractPeriod() ->setEmployee($employee) ->setContract($contract) ->setStartDate(new DateTimeImmutable('2026-03-01')) ->setEndDate(null) ->setContractNature(ContractNature::CDI) ; $persistProcessor = $this->createMock(ProcessorInterface::class); $removeProcessor = $this->createStub(ProcessorInterface::class); $entityManager = $this->createStub(EntityManagerInterface::class); $periodRepository = $this->createMock(EmployeeContractPeriodReadRepositoryInterface::class); $changeRequestFactory = new EmployeeContractChangeRequestFactory(); $periodManager = $this->createMock(EmployeeContractPeriodManagerInterface::class); $persistProcessor ->expects(self::once()) ->method('process') ->willReturn($employee) ; $periodRepository ->expects(self::once()) ->method('findOneCoveringDate') ->with($employee, self::isInstanceOf(DateTimeImmutable::class)) ->willReturn($todayPeriod) ; $periodManager ->expects(self::once()) ->method('closeCurrentPeriod') ->with( $todayPeriod, self::callback(static fn (DateTimeImmutable $value): bool => '2026-03-05' === $value->format('Y-m-d')), true ) ; $periodManager ->expects(self::never()) ->method('createNextPeriod') ; $this->mockOriginalContract($entityManager, $employee, $contract); $processor = new EmployeeWriteProcessor( $persistProcessor, $removeProcessor, $entityManager, $periodRepository, $changeRequestFactory, $periodManager ); $result = $processor->process($employee, new Patch()); self::assertSame($employee, $result); } public function testDelegatesNonCloseRequestToCreateNextPeriod(): void { $employee = $this->buildEmployeeWithId(43); $employee->setContractStartDate('2026-03-06'); $contract = $employee->getContract(); self::assertInstanceOf(Contract::class, $contract); $todayPeriod = new EmployeeContractPeriod() ->setEmployee($employee) ->setContract($contract) ->setStartDate(new DateTimeImmutable('2026-03-01')) ->setEndDate(null) ->setContractNature(ContractNature::CDI) ; $persistProcessor = $this->createMock(ProcessorInterface::class); $removeProcessor = $this->createStub(ProcessorInterface::class); $entityManager = $this->createStub(EntityManagerInterface::class); $periodRepository = $this->createMock(EmployeeContractPeriodReadRepositoryInterface::class); $changeRequestFactory = new EmployeeContractChangeRequestFactory(); $periodManager = $this->createMock(EmployeeContractPeriodManagerInterface::class); $persistProcessor ->expects(self::once()) ->method('process') ->willReturn($employee) ; $periodRepository ->expects(self::once()) ->method('findOneCoveringDate') ->with($employee, self::isInstanceOf(DateTimeImmutable::class)) ->willReturn($todayPeriod) ; $periodManager ->expects(self::once()) ->method('createNextPeriod') ->with( employee: $employee, contract: $contract, startDate: self::callback(static fn (DateTimeImmutable $value): bool => '2026-03-06' === $value->format('Y-m-d')), endDate: null, nature: ContractNature::CDI, todayPeriod: $todayPeriod ) ; $periodManager ->expects(self::never()) ->method('closeCurrentPeriod') ; $this->mockOriginalContract($entityManager, $employee, $contract); $processor = new EmployeeWriteProcessor( $persistProcessor, $removeProcessor, $entityManager, $periodRepository, $changeRequestFactory, $periodManager ); $result = $processor->process($employee, new Patch()); self::assertSame($employee, $result); } public function testSkipsPeriodOperationsWhenContractAndPeriodPayloadAreUnchanged(): void { $employee = $this->buildEmployeeWithId(44); $contract = $employee->getContract(); self::assertInstanceOf(Contract::class, $contract); $persistProcessor = $this->createMock(ProcessorInterface::class); $removeProcessor = $this->createStub(ProcessorInterface::class); $entityManager = $this->createStub(EntityManagerInterface::class); $periodRepository = $this->createMock(EmployeeContractPeriodReadRepositoryInterface::class); $changeRequestFactory = new EmployeeContractChangeRequestFactory(); $periodManager = $this->createMock(EmployeeContractPeriodManagerInterface::class); $persistProcessor ->expects(self::once()) ->method('process') ->willReturn($employee) ; $periodRepository->expects(self::never())->method('findOneCoveringDate'); $periodManager->expects(self::never())->method('closeCurrentPeriod'); $periodManager->expects(self::never())->method('createNextPeriod'); $this->mockOriginalContract($entityManager, $employee, $contract); $processor = new EmployeeWriteProcessor( $persistProcessor, $removeProcessor, $entityManager, $periodRepository, $changeRequestFactory, $periodManager ); $result = $processor->process($employee, new Patch()); self::assertSame($employee, $result); } public function testSetsEntryDateOnNewEmployee(): void { $employee = new Employee(); $employee->setFirstName('Jane'); $employee->setLastName('Doe'); $employee->setContractStartDate('2026-04-01'); $employee->setContractNature('CDI'); $contract = new Contract() ->setName('35h') ->setTrackingMode(Contract::TRACKING_TIME) ->setWeeklyHours(35) ; $employee->setContract($contract); $persistProcessor = $this->createMock(ProcessorInterface::class); $removeProcessor = $this->createStub(ProcessorInterface::class); $entityManager = $this->createStub(EntityManagerInterface::class); $periodRepository = $this->createStub(EmployeeContractPeriodReadRepositoryInterface::class); $changeRequestFactory = new EmployeeContractChangeRequestFactory(); $periodManager = $this->createMock(EmployeeContractPeriodManagerInterface::class); $persistProcessor ->expects(self::once()) ->method('process') ->willReturn($employee) ; $periodManager ->expects(self::once()) ->method('ensureContractPeriodExists') ; $processor = new EmployeeWriteProcessor( $persistProcessor, $removeProcessor, $entityManager, $periodRepository, $changeRequestFactory, $periodManager ); $processor->process($employee, new Post()); self::assertNotNull($employee->getEntryDate()); self::assertSame('2026-04-01', $employee->getEntryDate()->format('Y-m-d')); } public function testDeleteOperationDelegatesToRemoveProcessor(): void { $employee = $this->buildEmployeeWithId(45); $persistProcessor = $this->createMock(ProcessorInterface::class); $removeProcessor = $this->createMock(ProcessorInterface::class); $entityManager = $this->createStub(EntityManagerInterface::class); $periodRepository = $this->createStub(EmployeeContractPeriodReadRepositoryInterface::class); $changeRequestFactory = new EmployeeContractChangeRequestFactory(); $periodManager = $this->createStub(EmployeeContractPeriodManagerInterface::class); $persistProcessor->expects(self::never())->method('process'); $removeProcessor ->expects(self::once()) ->method('process') ->with($employee, self::isInstanceOf(Delete::class), [], []) ->willReturn(null) ; $processor = new EmployeeWriteProcessor( $persistProcessor, $removeProcessor, $entityManager, $periodRepository, $changeRequestFactory, $periodManager ); $result = $processor->process($employee, new Delete()); self::assertNull($result); } private function buildEmployeeWithId(int $id): Employee { $contract = new Contract() ->setName('39h') ->setTrackingMode(Contract::TRACKING_TIME) ->setWeeklyHours(39) ; $employee = new Employee() ->setFirstName('John') ->setLastName('Doe') ->setContract($contract) ; $ref = new ReflectionProperty($employee, 'id'); $ref->setValue($employee, $id); return $employee; } private function mockOriginalContract(EntityManagerInterface $entityManager, Employee $employee, Contract $contract): void { $unitOfWork = $this->createStub(UnitOfWork::class); $unitOfWork ->method('getOriginalEntityData') ->with($employee) ->willReturn(['contract' => $contract]) ; $entityManager ->method('getUnitOfWork') ->willReturn($unitOfWork) ; } }