diff --git a/src/Bundle/ChillPersonBundle/Actions/Upsert/Handler/PersonUpsertHandler.php b/src/Bundle/ChillPersonBundle/Actions/Upsert/Handler/PersonUpsertHandler.php index 0b579f637..6629cd63b 100644 --- a/src/Bundle/ChillPersonBundle/Actions/Upsert/Handler/PersonUpsertHandler.php +++ b/src/Bundle/ChillPersonBundle/Actions/Upsert/Handler/PersonUpsertHandler.php @@ -20,18 +20,25 @@ use Chill\MainBundle\Repository\GenderRepository; use Chill\MainBundle\Repository\PostalCodeRepositoryInterface; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Household\MembersEditorFactory; +use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository; use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; use Chill\PersonBundle\Actions\Upsert\UpsertMessage; +use libphonenumber\NumberParseException; +use Psr\Log\LoggerInterface; use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; +use Symfony\Component\Validator\Validator\ValidatorInterface; #[AsMessageHandler] readonly class PersonUpsertHandler { + private const LOG_PREFIX = '[PersonUpsertHandler] '; + public function __construct( private PersonIdentifierDefinitionRepository $personIdentifierDefinitionRepository, private PersonIdentifierRepository $personIdentifierRepository, @@ -41,25 +48,29 @@ readonly class PersonUpsertHandler private CenterRepositoryInterface $centerRepository, private GenderRepository $genderRepository, private ClockInterface $clock, + private \libphonenumber\PhoneNumberUtil $phoneNumberUtil, + private LoggerInterface $logger, + private PersonIdentifierManagerInterface $personIdentifierManager, + private ValidatorInterface $validator, ) {} private function createAddressWithMessage(UpsertMessage $message): Address { $newAddress = new Address(); - if (null !== $message->address) { - $newAddress->setStreet($message->address); + if (null !== $message->addressStreet) { + $newAddress->setStreet($message->addressStreet); } - if (null !== $message->streetNumber) { - $newAddress->setStreetNumber($message->streetNumber); + if (null !== $message->addressStreetNumber) { + $newAddress->setStreetNumber($message->addressStreetNumber); } - if (null !== $message->postcode) { - $postalCode = $this->postalCodeRepository->findOneBy(['code' => $message->postcode]); + if (null !== $message->addressPostcode) { + $postalCode = $this->postalCodeRepository->findOneBy(['code' => $message->addressPostcode]); if (null !== $postalCode) { $newAddress->setPostcode($postalCode); } } - if (null !== $message->extra) { - $newAddress->setExtra($message->extra); + if (null !== $message->addressExtra) { + $newAddress->setExtra($message->addressExtra); } $newAddress->setValidFrom(\DateTime::createFromImmutable($this->clock->now())); @@ -68,10 +79,10 @@ readonly class PersonUpsertHandler private function isMessageAddressMatch(Address $existingAddress, UpsertMessage $message): bool { - $streetMatches = $message->address === $existingAddress->getStreet(); - $streetNumberMatches = $message->streetNumber === $existingAddress->getStreetNumber(); + $streetMatches = $message->addressStreet === $existingAddress->getStreet(); + $streetNumberMatches = $message->addressStreetNumber === $existingAddress->getStreetNumber(); $postcodeMatches = null !== $existingAddress->getPostcode() - && $message->postcode === $existingAddress->getPostcode()->getCode(); + && $message->addressPostcode === $existingAddress->getPostcode()->getCode(); return $streetMatches && $streetNumberMatches && $postcodeMatches; } @@ -113,17 +124,15 @@ readonly class PersonUpsertHandler private function handlePersonMessage(UpsertMessage $message, Person $person): Person { - $membersEditor = $this->membersEditorFactory->createEditor(); $currentHousehold = $person->getCurrentHousehold(); // Check if address information is provided in the message $hasAddressInfo = $message->hasAddressInfo(); if (null !== $currentHousehold && $hasAddressInfo) { - $currentAddresses = $currentHousehold->getAddresses()->toArray(); - $lastCurrentAddress = end($currentAddresses); + $lastCurrentAddress = $currentHousehold->getCurrentAddress(); - if (false !== $lastCurrentAddress) { + if (null !== $lastCurrentAddress) { $messageAddressMatch = $this->isMessageAddressMatch($lastCurrentAddress, $message); if (!$messageAddressMatch) { $newAddress = $this->createAddressWithMessage($message); @@ -167,6 +176,18 @@ readonly class PersonUpsertHandler $person->setLastName($message->lastName); } + // Handle birthDate + if (null !== $message->birthdate) { + try { + $person->setBirthdate(new \DateTime($message->birthdate)); + } catch (\Exception $e) { + $this->logger->error(self::LOG_PREFIX.'Could not parse birthdate: '.$message->birthdate, [ + 'exception' => $e->getTraceAsString(), + 'birthdate' => $message->birthdate, + ]); + } + } + // Handle gender $gender = $this->findGenderByValue($message->gender); if (null === $gender) { @@ -183,6 +204,42 @@ readonly class PersonUpsertHandler $person->setCenter($center); } + // mobileNumber and phoneNumber + if (null !== $message->phoneNumber) { + try { + $person->setPhonenumber($this->phoneNumberUtil->parse($message->phoneNumber)); + } catch (NumberParseException $e) { + $this->logger->error(self::LOG_PREFIX.'Could not parse phoneNumber', [ + 'exception' => $e->getTraceAsString(), + 'phoneNumber' => $message->phoneNumber, + ]); + throw new UnrecoverableMessageHandlingException('Could not parse phoneNumber: '.$message->phoneNumber); + } + } + + if (null !== $message->mobileNumber) { + try { + $person->setMobilenumber($this->phoneNumberUtil->parse($message->mobileNumber)); + } catch (NumberParseException $e) { + $this->logger->error(self::LOG_PREFIX.'Could not parse mobileNumber', [ + 'exception' => $e->getTraceAsString(), + 'mobileNumber' => $message->mobileNumber, + ]); + throw new UnrecoverableMessageHandlingException('Could not parse mobileNumber: '.$message->mobileNumber); + } + } + + $errors = $this->validator->validate($person); + + if ($errors->count() > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + $this->logger->error(self::LOG_PREFIX.'Person created / updated not valid', ['errors' => implode(', ', $errorMessages)]); + throw new UnrecoverableMessageHandlingException('Person created / updated not valid: '.implode(', ', $errorMessages)); + } + return $person; } @@ -191,14 +248,16 @@ readonly class PersonUpsertHandler // 1. Retrieve definition $definition = $this->personIdentifierDefinitionRepository->find($message->personIdentifierDefinitionId); if (null === $definition) { - throw new \RuntimeException('PersonIdentifierDefinition not found for id '.$message->personIdentifierDefinitionId); + $this->logger->error(self::LOG_PREFIX.'Person message not found: '.$message->personIdentifierDefinitionId); + throw new UnrecoverableMessageHandlingException('PersonIdentifierDefinition not found for id '.$message->personIdentifierDefinitionId); } // 2. Search identifiers $identifiers = $this->personIdentifierRepository->findByDefinitionAndCanonical($definition, $message->externalId); if (count($identifiers) > 1) { - throw new \RuntimeException('More than one identifier found for definition.'); + $this->logger->error(self::LOG_PREFIX.'Person message contains more than one identifier'); + throw new UnrecoverableMessageHandlingException('More than one identifier found for definition.'); } if (0 === count($identifiers)) { @@ -210,7 +269,12 @@ readonly class PersonUpsertHandler // Create and bound identifier $identifier = new PersonIdentifier($definition); $identifier->setPerson($person); - $identifier->setValue(['content' => $message->externalId]); + $identifier->setValue($value = ['content' => $message->externalId]); + $identifier->setCanonical( + $this->personIdentifierManager + ->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition()) + ->canonicalizeValue($value) + ); $this->entityManager->persist($identifier); } else { // 4. Update existing person @@ -221,5 +285,6 @@ readonly class PersonUpsertHandler } $this->entityManager->flush(); + $this->entityManager->clear(); } } diff --git a/src/Bundle/ChillPersonBundle/Actions/Upsert/UpsertMessage.php b/src/Bundle/ChillPersonBundle/Actions/Upsert/UpsertMessage.php index a2fc92e0a..99ac19920 100644 --- a/src/Bundle/ChillPersonBundle/Actions/Upsert/UpsertMessage.php +++ b/src/Bundle/ChillPersonBundle/Actions/Upsert/UpsertMessage.php @@ -21,15 +21,20 @@ class UpsertMessage public ?string $birthdate = null; public ?string $mobileNumber = null; public ?string $phoneNumber = null; - public ?string $address = null; - public ?string $extra = null; - public ?string $streetNumber = null; - public ?string $postcode = null; - public ?string $city = null; + + public ?string $addressStreet = null; + + /** + * The extra field of the address. + */ + public ?string $addressExtra = null; + public ?string $addressStreetNumber = null; + public ?string $addressPostcode = null; + public ?string $addressCity = null; public ?string $center = null; public function hasAddressInfo(): bool { - return null !== $this->address || null !== $this->postcode || null !== $this->streetNumber; + return null !== $this->addressStreet || null !== $this->addressPostcode || null !== $this->addressStreetNumber; } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Action/Upsert/Handler/PersonUpsertHandlerTest.php b/src/Bundle/ChillPersonBundle/Tests/Action/Upsert/Handler/PersonUpsertHandlerTest.php index c1b7e66dd..2c5042833 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Action/Upsert/Handler/PersonUpsertHandlerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Action/Upsert/Handler/PersonUpsertHandlerTest.php @@ -27,6 +27,10 @@ use Doctrine\ORM\EntityManagerInterface; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Clock\ClockInterface; +use Psr\Log\LoggerInterface; +use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; /** * @internal @@ -40,11 +44,16 @@ class PersonUpsertHandlerTest extends TestCase private function createMembersEditorFactoryMock(): MembersEditorFactory { $membersEditor = $this->prophesize(MembersEditor::class); - $membersEditor->validate()->willReturn($this->prophesize(\Doctrine\Common\Collections\Collection::class)->reveal()); + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $membersEditor->validate()->willReturn($violations->reveal()); + $membersEditor->addMovement(Argument::type(\DateTimeImmutable::class), Argument::type(Person::class), null, true)->willReturn($membersEditor->reveal()); $membersEditor->getPersistable()->willReturn([]); + $membersEditor->postMove(); $factory = $this->prophesize(MembersEditorFactory::class); $factory->createEditor()->willReturn($membersEditor->reveal()); + $factory->createEditor(Argument::any())->willReturn($membersEditor->reveal()); return $factory->reveal(); } @@ -59,6 +68,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository = $this->prophesize(CenterRepositoryInterface::class); $genderRepository = $this->prophesize(GenderRepository::class); $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); @@ -72,9 +85,24 @@ class PersonUpsertHandlerTest extends TestCase // Mock center repository - no center found $centerRepository->findBy(['name' => null])->willReturn([]); + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager - create real worker with mocked engine + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); $handler = new PersonUpsertHandler( $personIdentifierDefinitionRepository->reveal(), @@ -85,6 +113,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository->reveal(), $genderRepository->reveal(), $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), ); $message = new UpsertMessage(); @@ -106,6 +138,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository = $this->prophesize(CenterRepositoryInterface::class); $genderRepository = $this->prophesize(GenderRepository::class); $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); $personIdentifierDefinitionRepository->find(2)->willReturn($definition->reveal()); @@ -131,7 +167,13 @@ class PersonUpsertHandlerTest extends TestCase // Mock center repository - no center found $centerRepository->findBy(['name' => null])->willReturn([]); + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); $handler = new PersonUpsertHandler( $personIdentifierDefinitionRepository->reveal(), @@ -142,6 +184,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository->reveal(), $genderRepository->reveal(), $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), ); $message = new UpsertMessage(); @@ -163,6 +209,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository = $this->prophesize(CenterRepositoryInterface::class); $genderRepository = $this->prophesize(GenderRepository::class); $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); @@ -176,9 +226,24 @@ class PersonUpsertHandlerTest extends TestCase // Mock center repository $centerRepository->findBy(['name' => null])->willReturn([]); + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager - create real worker with mocked engine + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); $handler = new PersonUpsertHandler( $personIdentifierDefinitionRepository->reveal(), @@ -189,6 +254,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository->reveal(), $genderRepository->reveal(), $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), ); $message = new UpsertMessage(); @@ -211,6 +280,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository = $this->prophesize(CenterRepositoryInterface::class); $genderRepository = $this->prophesize(GenderRepository::class); $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); @@ -225,9 +298,24 @@ class PersonUpsertHandlerTest extends TestCase $center = $this->prophesize(\Chill\MainBundle\Entity\Center::class); $centerRepository->findBy(['name' => 'Main Center'])->willReturn([$center->reveal()]); + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager - create real worker with mocked engine + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); $handler = new PersonUpsertHandler( $personIdentifierDefinitionRepository->reveal(), @@ -238,6 +326,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository->reveal(), $genderRepository->reveal(), $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), ); $message = new UpsertMessage(); @@ -260,6 +352,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository = $this->prophesize(CenterRepositoryInterface::class); $genderRepository = $this->prophesize(GenderRepository::class); $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); @@ -274,9 +370,24 @@ class PersonUpsertHandlerTest extends TestCase $center = $this->prophesize(\Chill\MainBundle\Entity\Center::class); $centerRepository->findBy(['name' => 'Secondary Center'])->willReturn([$center->reveal()]); + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager - create real worker with mocked engine + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->persist(Argument::any())->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); $handler = new PersonUpsertHandler( $personIdentifierDefinitionRepository->reveal(), @@ -287,6 +398,10 @@ class PersonUpsertHandlerTest extends TestCase $centerRepository->reveal(), $genderRepository->reveal(), $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), ); $message = new UpsertMessage(); @@ -299,4 +414,403 @@ class PersonUpsertHandlerTest extends TestCase $handler->__invoke($message); } + + public function testInvokeWithBirthdate(): void + { + $personIdentifierDefinitionRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository::class); + $identifierRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $membersEditorFactory = $this->createMembersEditorFactoryMock(); + $postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class); + $centerRepository = $this->prophesize(CenterRepositoryInterface::class); + $genderRepository = $this->prophesize(GenderRepository::class); + $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); + + $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); + $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); + $identifierRepository->findByDefinitionAndCanonical($definition->reveal(), '123')->willReturn([]); + + // Mock gender repository + $gender = $this->prophesize(Gender::class); + $genderRepository->findByGenderTranslation(GenderEnum::NEUTRAL)->willReturn([$gender->reveal()]); + $genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender->reveal()]); + + // Mock center repository + $centerRepository->findBy(['name' => null])->willReturn([]); + + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); + + $handler = new PersonUpsertHandler( + $personIdentifierDefinitionRepository->reveal(), + $identifierRepository->reveal(), + $entityManager->reveal(), + $membersEditorFactory, + $postalCodeRepository->reveal(), + $centerRepository->reveal(), + $genderRepository->reveal(), + $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), + ); + + $message = new UpsertMessage(); + $message->externalId = '123'; + $message->personIdentifierDefinitionId = 1; + $message->firstName = 'John'; + $message->lastName = 'Doe'; + $message->birthdate = '1990-01-15'; + + $handler->__invoke($message); + } + + public function testInvokeWithPhoneNumber(): void + { + $personIdentifierDefinitionRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository::class); + $identifierRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $membersEditorFactory = $this->createMembersEditorFactoryMock(); + $postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class); + $centerRepository = $this->prophesize(CenterRepositoryInterface::class); + $genderRepository = $this->prophesize(GenderRepository::class); + $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); + + $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); + $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); + $identifierRepository->findByDefinitionAndCanonical($definition->reveal(), '123')->willReturn([]); + + // Mock gender repository + $gender = $this->prophesize(Gender::class); + $genderRepository->findByGenderTranslation(GenderEnum::NEUTRAL)->willReturn([$gender->reveal()]); + $genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender->reveal()]); + + // Mock center repository + $centerRepository->findBy(['name' => null])->willReturn([]); + + // Mock phone number parsing + $phoneNumber = $this->prophesize(\libphonenumber\PhoneNumber::class); + $phoneNumberUtil->parse('+32123456789')->willReturn($phoneNumber->reveal()); + + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); + + $handler = new PersonUpsertHandler( + $personIdentifierDefinitionRepository->reveal(), + $identifierRepository->reveal(), + $entityManager->reveal(), + $membersEditorFactory, + $postalCodeRepository->reveal(), + $centerRepository->reveal(), + $genderRepository->reveal(), + $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), + ); + + $message = new UpsertMessage(); + $message->externalId = '123'; + $message->personIdentifierDefinitionId = 1; + $message->firstName = 'John'; + $message->lastName = 'Doe'; + $message->phoneNumber = '+32123456789'; + + $handler->__invoke($message); + } + + public function testInvokeWithMobileNumber(): void + { + $personIdentifierDefinitionRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository::class); + $identifierRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $membersEditorFactory = $this->createMembersEditorFactoryMock(); + $postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class); + $centerRepository = $this->prophesize(CenterRepositoryInterface::class); + $genderRepository = $this->prophesize(GenderRepository::class); + $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); + + $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); + $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); + $identifierRepository->findByDefinitionAndCanonical($definition->reveal(), '123')->willReturn([]); + + // Mock gender repository + $gender = $this->prophesize(Gender::class); + $genderRepository->findByGenderTranslation(GenderEnum::NEUTRAL)->willReturn([$gender->reveal()]); + $genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender->reveal()]); + + // Mock center repository + $centerRepository->findBy(['name' => null])->willReturn([]); + + // Mock mobile number parsing + $mobileNumber = $this->prophesize(\libphonenumber\PhoneNumber::class); + $phoneNumberUtil->parse('+32987654321')->willReturn($mobileNumber->reveal()); + + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); + + $handler = new PersonUpsertHandler( + $personIdentifierDefinitionRepository->reveal(), + $identifierRepository->reveal(), + $entityManager->reveal(), + $membersEditorFactory, + $postalCodeRepository->reveal(), + $centerRepository->reveal(), + $genderRepository->reveal(), + $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), + ); + + $message = new UpsertMessage(); + $message->externalId = '123'; + $message->personIdentifierDefinitionId = 1; + $message->firstName = 'John'; + $message->lastName = 'Doe'; + $message->mobileNumber = '+32987654321'; + + $handler->__invoke($message); + } + + public function testInvokeWithAddressFields(): void + { + $personIdentifierDefinitionRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository::class); + $identifierRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $membersEditorFactory = $this->createMembersEditorFactoryMock(); + $postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class); + $centerRepository = $this->prophesize(CenterRepositoryInterface::class); + $genderRepository = $this->prophesize(GenderRepository::class); + $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); + + $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); + $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); + $identifierRepository->findByDefinitionAndCanonical($definition->reveal(), '123')->willReturn([]); + + // Mock gender repository + $gender = $this->prophesize(Gender::class); + $genderRepository->findByGenderTranslation(GenderEnum::NEUTRAL)->willReturn([$gender->reveal()]); + $genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender->reveal()]); + + // Mock center repository + $centerRepository->findBy(['name' => null])->willReturn([]); + + // Mock postal code repository + $postalCode = $this->prophesize(\Chill\MainBundle\Entity\PostalCode::class); + $postalCodeRepository->findOneBy(['code' => '1000'])->willReturn($postalCode->reveal()); + + // Mock clock for address valid from + $now = new \DateTimeImmutable('2026-03-09'); + $clock->now()->willReturn($now); + + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); + + $handler = new PersonUpsertHandler( + $personIdentifierDefinitionRepository->reveal(), + $identifierRepository->reveal(), + $entityManager->reveal(), + $membersEditorFactory, + $postalCodeRepository->reveal(), + $centerRepository->reveal(), + $genderRepository->reveal(), + $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), + ); + + $message = new UpsertMessage(); + $message->externalId = '123'; + $message->personIdentifierDefinitionId = 1; + $message->firstName = 'John'; + $message->lastName = 'Doe'; + $message->addressStreet = 'Main Street'; + $message->addressStreetNumber = '42'; + $message->addressPostcode = '1000'; + $message->addressExtra = 'Apartment 3B'; + $message->addressCity = 'Brussels'; + + $handler->__invoke($message); + } + + public function testInvokeWithAllFields(): void + { + $personIdentifierDefinitionRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository::class); + $identifierRepository = $this->prophesize(\Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $membersEditorFactory = $this->createMembersEditorFactoryMock(); + $postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class); + $centerRepository = $this->prophesize(CenterRepositoryInterface::class); + $genderRepository = $this->prophesize(GenderRepository::class); + $clock = $this->prophesize(ClockInterface::class); + $phoneNumberUtil = $this->prophesize(\libphonenumber\PhoneNumberUtil::class); + $logger = $this->prophesize(LoggerInterface::class); + $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class); + $validator = $this->prophesize(ValidatorInterface::class); + + $definition = $this->prophesize(\Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition::class); + $personIdentifierDefinitionRepository->find(1)->willReturn($definition->reveal()); + $identifierRepository->findByDefinitionAndCanonical($definition->reveal(), '123')->willReturn([]); + + // Mock gender repository + $femaleGender = $this->prophesize(Gender::class); + $genderRepository->findByGenderTranslation(GenderEnum::FEMALE)->willReturn([$femaleGender->reveal()]); + $genderRepository->findByGenderTranslation(Argument::any())->willReturn([$femaleGender->reveal()]); + + // Mock center repository + $center = $this->prophesize(\Chill\MainBundle\Entity\Center::class); + $centerRepository->findBy(['name' => 'Main Center'])->willReturn([$center->reveal()]); + + // Mock postal code repository + $postalCode = $this->prophesize(\Chill\MainBundle\Entity\PostalCode::class); + $postalCodeRepository->findOneBy(['code' => '1000'])->willReturn($postalCode->reveal()); + + // Mock clock for address valid from + $now = new \DateTimeImmutable('2026-03-09'); + $clock->now()->willReturn($now); + + // Mock phone number parsing + $phoneNumber = $this->prophesize(\libphonenumber\PhoneNumber::class); + $mobileNumber = $this->prophesize(\libphonenumber\PhoneNumber::class); + $phoneNumberUtil->parse('+32123456789')->willReturn($phoneNumber->reveal()); + $phoneNumberUtil->parse('+32987654321')->willReturn($mobileNumber->reveal()); + + // Mock validator + $violations = $this->prophesize(ConstraintViolationListInterface::class); + $violations->count()->willReturn(0); + $validator->validate(Argument::any())->willReturn($violations->reveal()); + + // Mock person identifier manager + $identifierEngine = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class); + $identifierEngine->canonicalizeValue(Argument::any(), Argument::any())->willReturn('123'); + $worker = new \Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker( + $identifierEngine->reveal(), + $definition->reveal() + ); + $personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker); + + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->persist(Argument::any())->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + $entityManager->clear()->shouldBeCalled(); + + $handler = new PersonUpsertHandler( + $personIdentifierDefinitionRepository->reveal(), + $identifierRepository->reveal(), + $entityManager->reveal(), + $membersEditorFactory, + $postalCodeRepository->reveal(), + $centerRepository->reveal(), + $genderRepository->reveal(), + $clock->reveal(), + $phoneNumberUtil->reveal(), + $logger->reveal(), + $personIdentifierManager->reveal(), + $validator->reveal(), + ); + + $message = new UpsertMessage(); + $message->externalId = '123'; + $message->personIdentifierDefinitionId = 1; + $message->firstName = 'Jane'; + $message->lastName = 'Smith'; + $message->gender = 'female'; + $message->birthdate = '1985-05-20'; + $message->phoneNumber = '+32123456789'; + $message->mobileNumber = '+32987654321'; + $message->addressStreet = 'Main Street'; + $message->addressStreetNumber = '42'; + $message->addressPostcode = '1000'; + $message->addressExtra = 'Apartment 3B'; + $message->addressCity = 'Brussels'; + $message->center = 'Main Center'; + + $handler->__invoke($message); + } }