Merge branch '497-import-des-usagers-depuis-une-source-externe' into 'ticket-app-master'

Resolve "Import des usagers depuis une source externe"

See merge request Chill-Projet/chill-bundles!962
This commit is contained in:
2026-03-13 14:40:43 +00:00
7 changed files with 1290 additions and 6 deletions

View File

@@ -98,7 +98,7 @@
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13",
"friendsofphp/php-cs-fixer": "3.93.0",
"friendsofphp/php-cs-fixer": "^3.94",
"jangregor/phpstan-prophecy": "^1.0",
"nelmio/alice": "^3.8",
"nikic/php-parser": "^4.15",
@@ -113,13 +113,13 @@
"symfony/debug-bundle": "^5.4",
"symfony/dotenv": "^5.4",
"symfony/flex": "^2.4",
"symfony/loco-translation-provider": "^6.0",
"symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^7.1",
"symfony/runtime": "^5.4",
"symfony/stopwatch": "^5.4",
"symfony/var-dumper": "^5.4",
"symfony/web-profiler-bundle": "^5.4",
"symfony/loco-translation-provider": "^6.0"
"symfony/web-profiler-bundle": "^5.4"
},
"conflict": {
"symfony/symfony": "*"

View File

@@ -11,6 +11,7 @@
"@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",

View File

@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Actions\Upsert\Handler;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
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,
private EntityManagerInterface $entityManager,
private MembersEditorFactory $membersEditorFactory,
private PostalCodeRepositoryInterface $postalCodeRepository,
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->addressStreet) {
$newAddress->setStreet($message->addressStreet);
}
if (null !== $message->addressStreetNumber) {
$newAddress->setStreetNumber($message->addressStreetNumber);
}
if (null !== $message->addressPostcode) {
$postalCode = $this->postalCodeRepository->findOneBy(['code' => $message->addressPostcode]);
if (null !== $postalCode) {
$newAddress->setPostcode($postalCode);
}
}
if (null !== $message->addressExtra) {
$newAddress->setExtra($message->addressExtra);
}
$newAddress->setValidFrom(\DateTime::createFromImmutable($this->clock->now()));
return $newAddress;
}
private function isMessageAddressMatch(Address $existingAddress, UpsertMessage $message): bool
{
$streetMatches = $message->addressStreet === $existingAddress->getStreet();
$streetNumberMatches = $message->addressStreetNumber === $existingAddress->getStreetNumber();
$postcodeMatches = null !== $existingAddress->getPostcode()
&& $message->addressPostcode === $existingAddress->getPostcode()->getCode();
return $streetMatches && $streetNumberMatches && $postcodeMatches;
}
private function findGenderByValue(?string $genderValue): ?Gender
{
if (null === $genderValue) {
return null;
}
foreach (GenderEnum::cases() as $case) {
if ($case->value === $genderValue) {
$genders = $this->genderRepository->findByGenderTranslation($case);
return count($genders) > 0 ? $genders[0] : null;
}
}
return null;
}
private function findNeutralGender(): ?Gender
{
$genders = $this->genderRepository->findByGenderTranslation(GenderEnum::NEUTRAL);
return count($genders) > 0 ? $genders[0] : null;
}
private function findCenterByName(?string $centerName): ?Center
{
if (null === $centerName) {
return null;
}
$centers = $this->centerRepository->findBy(['name' => $centerName]);
return count($centers) > 0 ? $centers[0] : null;
}
private function handlePersonMessage(UpsertMessage $message, Person $person): Person
{
$currentHousehold = $person->getCurrentHousehold();
// Check if address information is provided in the message
$hasAddressInfo = $message->hasAddressInfo();
if (null !== $currentHousehold && $hasAddressInfo) {
$lastCurrentAddress = $currentHousehold->getCurrentAddress();
if (null !== $lastCurrentAddress) {
$messageAddressMatch = $this->isMessageAddressMatch($lastCurrentAddress, $message);
if (!$messageAddressMatch) {
$newAddress = $this->createAddressWithMessage($message);
$currentHousehold->addAddress($newAddress);
}
}
} elseif (null === $currentHousehold && $hasAddressInfo) {
// Create a new household with a new address
$newHousehold = new Household();
// Create the new address
$newAddress = $this->createAddressWithMessage($message);
$newHousehold->addAddress($newAddress);
// Create a MembersEditor with the new household and add the person
$membersEditor = $this->membersEditorFactory->createEditor($newHousehold);
$membersEditor->addMovement(
$this->clock->now(),
$person,
null,
true
);
// Validate the membership
$violations = $membersEditor->validate();
if (0 === $violations->count()) {
// Persist the household and trigger events
$this->entityManager->persist($newHousehold);
foreach ($membersEditor->getPersistable() as $persistable) {
$this->entityManager->persist($persistable);
}
$membersEditor->postMove();
}
}
// Update person information
if (null !== $message->firstName) {
$person->setFirstName($message->firstName);
}
if (null !== $message->lastName) {
$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) {
// If no gender found or provided, use neutral as default
$gender = $this->findNeutralGender();
}
if (null !== $gender) {
$person->setGender($gender);
}
// Handle center
$center = $this->findCenterByName($message->center);
if (null !== $center) {
$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;
}
public function __invoke(UpsertMessage $message): void
{
// 1. Retrieve definition
$definition = $this->personIdentifierDefinitionRepository->find($message->personIdentifierDefinitionId);
if (null === $definition) {
$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) {
$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)) {
// 3. Create new entity Person
$person = new Person();
$person = $this->handlePersonMessage($message, $person);
$this->entityManager->persist($person);
// Create and bound identifier
$identifier = new PersonIdentifier($definition);
$identifier->setPerson($person);
$identifier->setValue($value = ['content' => $message->externalId]);
$identifier->setCanonical(
$this->personIdentifierManager
->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition())
->canonicalizeValue($value)
);
$this->entityManager->persist($identifier);
} else {
// 4. Update existing person
/** @var PersonIdentifier $identifier */
$identifier = $identifiers[0];
$person = $identifier->getPerson();
$this->handlePersonMessage($message, $person);
}
$this->entityManager->flush();
$this->entityManager->clear();
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Actions\Upsert;
class UpsertMessage
{
public string $externalId;
public int $personIdentifierDefinitionId;
public ?string $firstName = null;
public ?string $lastName = null;
public ?string $gender = null;
public ?string $birthdate = null;
public ?string $mobileNumber = null;
public ?string $phoneNumber = 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->addressStreet || null !== $this->addressPostcode || null !== $this->addressStreetNumber;
}
}

View File

@@ -73,6 +73,11 @@ class PersonRepository implements ObjectRepository
return $this->repository->findBy(['id' => $ids]);
}
public function findByExternalId(string $externalId): ?Person
{
return $this->repository->findOneBy(['externalId' => $externalId]);
}
/**
* @throws \Exception
*

View File

@@ -0,0 +1,949 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Action\Upsert\Handler;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Chill\PersonBundle\Actions\Upsert\Handler\PersonUpsertHandler;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Actions\Upsert\UpsertMessage;
use Chill\PersonBundle\Household\MembersEditor;
use Chill\PersonBundle\Household\MembersEditorFactory;
use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* @internal
*
* @coversNothing
*/
class PersonUpsertHandlerTest extends TestCase
{
use ProphecyTrait;
private function createMembersEditorFactoryMock(): MembersEditorFactory
{
$membersEditor = $this->prophesize(MembersEditor::class);
$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)->will(function ($args) use ($membersEditor) {
$date = $args[0];
$person = $args[1];
$household = $membersEditor->reveal()->getHousehold();
if ($household instanceof Household) {
$membership = (new HouseholdMember())
->setStartDate($date)
->setPerson($person);
$household->addMember($membership);
}
return $membersEditor->reveal();
});
$membersEditor->getPersistable()->willReturn([]);
$membersEditor->postMove();
$factory = $this->prophesize(MembersEditorFactory::class);
$factory->createEditor()->will(function ($args) use ($membersEditor) {
$membersEditor->getHousehold()->willReturn(new Household());
return $membersEditor->reveal();
});
$factory->createEditor(Argument::type(Household::class))->will(function ($args) use ($membersEditor) {
$membersEditor->getHousehold()->willReturn($args[0]);
return $membersEditor->reveal();
});
return $factory->reveal();
}
public function testInvokeCreatesNewPersonAndIdentifier(): 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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender]);
// Mock center repository - no center found
$centerRepository->findBy(['name' => null])->willReturn([]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->shouldBeCalled();
$entityManager->persist(Argument::that(fn ($arg) => $arg instanceof PersonIdentifier && '123' === $arg->getCanonical() && $arg->getValue() === ['content' => '123']))->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$entityManager->clear()->shouldBeCalled();
$handler = new PersonUpsertHandler(
$personIdentifierDefinitionRepository->reveal(),
$identifierRepository->reveal(),
$entityManager->reveal(),
$membersEditorFactory,
$postalCodeRepository->reveal(),
$centerRepository->reveal(),
$genderRepository->reveal(),
$clock,
$phoneNumberUtil->reveal(),
$logger,
$personIdentifierManager->reveal(),
$validator->reveal(),
);
$message = new UpsertMessage();
$message->externalId = '123';
$message->personIdentifierDefinitionId = 1;
$message->firstName = 'John';
$message->lastName = 'Doe';
$handler->__invoke($message);
self::assertInstanceOf(Person::class, $person);
self::assertEquals('John', $person->getFirstName());
self::assertEquals('Doe', $person->getLastName());
}
public function testInvokeUpdatesExistingPerson(): 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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(2)->willReturn($definition);
$person = new Person();
$identifier = new PersonIdentifier($definition);
$identifier->setPerson($person);
$identifierRepository->findByDefinitionAndCanonical($definition, '456')->willReturn([$identifier]);
// Mock gender repository - ensure it always returns an array
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender]);
// Mock center repository - no center found
$centerRepository->findBy(['name' => null])->willReturn([]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
$entityManager->flush()->shouldBeCalled();
$entityManager->clear()->shouldBeCalled();
$handler = new PersonUpsertHandler(
$personIdentifierDefinitionRepository->reveal(),
$identifierRepository->reveal(),
$entityManager->reveal(),
$membersEditorFactory,
$postalCodeRepository->reveal(),
$centerRepository->reveal(),
$genderRepository->reveal(),
$clock,
$phoneNumberUtil->reveal(),
$logger,
$personIdentifierManager->reveal(),
$validator->reveal(),
);
$message = new UpsertMessage();
$message->externalId = '456';
$message->personIdentifierDefinitionId = 2;
$message->firstName = 'Jane';
$message->lastName = 'Smith';
$handler->__invoke($message);
self::assertEquals('Jane', $person->getFirstName());
self::assertEquals('Smith', $person->getLastName());
}
public function testInvokeWithGenderHandling(): 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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository - configure to return male gender
$maleGender = new Gender();
$maleGender->setGenderTranslation(GenderEnum::MALE);
$genderRepository->findByGenderTranslation(GenderEnum::MALE)->willReturn([$maleGender]);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$maleGender]);
// Mock center repository
$centerRepository->findBy(['name' => null])->willReturn([]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$personIdentifierManager->reveal(),
$validator->reveal(),
);
$message = new UpsertMessage();
$message->externalId = '123';
$message->personIdentifierDefinitionId = 1;
$message->firstName = 'John';
$message->lastName = 'Doe';
$message->gender = 'male';
$handler->__invoke($message);
self::assertInstanceOf(Person::class, $person);
self::assertSame($maleGender, $person->getGender());
}
public function testInvokeWithCenterHandling(): 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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$neutralGender = new Gender();
$neutralGender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(GenderEnum::NEUTRAL)->willReturn([$neutralGender]);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$neutralGender]);
// Mock center repository - find center by name
$center = new \Chill\MainBundle\Entity\Center();
$center->setName('Main Center');
$centerRepository->findBy(['name' => 'Main Center'])->willReturn([$center]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$personIdentifierManager->reveal(),
$validator->reveal(),
);
$message = new UpsertMessage();
$message->externalId = '123';
$message->personIdentifierDefinitionId = 1;
$message->firstName = 'John';
$message->lastName = 'Doe';
$message->center = 'Main Center';
$handler->__invoke($message);
self::assertInstanceOf(Person::class, $person);
self::assertSame($center, $person->getCenter());
}
public function testInvokeWithGenderAndCenterHandling(): 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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository - configure to return female gender
$femaleGender = new Gender();
$femaleGender->setGenderTranslation(GenderEnum::FEMALE);
$genderRepository->findByGenderTranslation(GenderEnum::FEMALE)->willReturn([$femaleGender]);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$femaleGender]);
// Mock center repository - find center by name
$center = new \Chill\MainBundle\Entity\Center();
$center->setName('Secondary Center');
$centerRepository->findBy(['name' => 'Secondary Center'])->willReturn([$center]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$personIdentifierManager->reveal(),
$validator->reveal(),
);
$message = new UpsertMessage();
$message->externalId = '123';
$message->personIdentifierDefinitionId = 1;
$message->firstName = 'Jane';
$message->lastName = 'Smith';
$message->gender = 'female';
$message->center = 'Secondary Center';
$handler->__invoke($message);
self::assertInstanceOf(Person::class, $person);
self::assertSame($femaleGender, $person->getGender());
self::assertSame($center, $person->getCenter());
}
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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender]);
// Mock center repository
$centerRepository->findBy(['name' => null])->willReturn([]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$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);
self::assertInstanceOf(Person::class, $person);
self::assertEquals(new \DateTime('1990-01-15'), $person->getBirthdate());
}
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 = new MockClock();
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender]);
// 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
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$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);
self::assertInstanceOf(Person::class, $person);
self::assertSame($phoneNumber->reveal(), $person->getPhonenumber());
}
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 = new MockClock();
$phoneNumberUtil = PhoneNumberUtil::getInstance();
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender]);
// Mock center repository
$centerRepository->findBy(['name' => null])->willReturn([]);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList());
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil,
$logger,
$personIdentifierManager->reveal(),
$validator->reveal(),
);
$message = new UpsertMessage();
$message->externalId = '123';
$message->personIdentifierDefinitionId = 1;
$message->firstName = 'John';
$message->lastName = 'Doe';
$message->mobileNumber = '+32475123456';
$handler->__invoke($message);
self::assertInstanceOf(Person::class, $person);
self::assertEquals('475123456', (string) $person->getMobilenumber()->getNationalNumber());
self::assertEquals('32', $person->getMobilenumber()->getCountryCode());
}
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 = new MockClock('2026-03-09');
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::NEUTRAL);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$gender]);
// Mock center repository
$centerRepository->findBy(['name' => null])->willReturn([]);
// Mock postal code repository
$postalCode = new \Chill\MainBundle\Entity\PostalCode();
$postalCode->setCode('1000');
$postalCodeRepository->findOneBy(['code' => '1000'])->willReturn($postalCode);
// Mock validator
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$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);
self::assertInstanceOf(Person::class, $person);
$household = $person->getCurrentHousehold($clock->now());
self::assertNotNull($household);
$address = $household->getCurrentAddress(\DateTime::createFromImmutable($clock->now()));
self::assertNotNull($address);
self::assertEquals('Main Street', $address->getStreet());
self::assertEquals('42', $address->getStreetNumber());
self::assertSame($postalCode, $address->getPostcode());
self::assertEquals('Apartment 3B', $address->getExtra());
}
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 = new MockClock('2026-03-09');
$phoneNumberUtil = $this->prophesize(PhoneNumberUtil::class);
$logger = new NullLogger();
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
$validator = $this->prophesize(ValidatorInterface::class);
$definition = new PersonIdentifierDefinition(['fr' => 'dummy'], 'dummy');
$personIdentifierDefinitionRepository->find(1)->willReturn($definition);
$identifierRepository->findByDefinitionAndCanonical($definition, '123')->willReturn([]);
// Mock gender repository
$femaleGender = new Gender();
$femaleGender->setGenderTranslation(GenderEnum::FEMALE);
$genderRepository->findByGenderTranslation(GenderEnum::FEMALE)->willReturn([$femaleGender]);
$genderRepository->findByGenderTranslation(Argument::any())->willReturn([$femaleGender]);
// Mock center repository
$center = new \Chill\MainBundle\Entity\Center();
$center->setName('Main Center');
$centerRepository->findBy(['name' => 'Main Center'])->willReturn([$center]);
// Mock postal code repository
$postalCode = new \Chill\MainBundle\Entity\PostalCode();
$postalCode->setCode('1000');
$postalCodeRepository->findOneBy(['code' => '1000'])->willReturn($postalCode);
// 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
$validator->validate(Argument::any())->willReturn(new ConstraintViolationList([]));
// 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
);
$personIdentifierManager->buildWorkerByPersonIdentifierDefinition(Argument::any())->willReturn($worker);
$person = null;
$entityManager->persist(Argument::that(function ($arg) use (&$person) {
if ($arg instanceof Person) {
$person = $arg;
return true;
}
return false;
}))->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,
$phoneNumberUtil->reveal(),
$logger,
$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);
self::assertInstanceOf(Person::class, $person);
self::assertEquals('Jane', $person->getFirstName());
self::assertEquals('Smith', $person->getLastName());
self::assertEquals(new \DateTime('1985-05-20'), $person->getBirthdate());
self::assertSame($femaleGender, $person->getGender());
self::assertSame($center, $person->getCenter());
self::assertSame($phoneNumber->reveal(), $person->getPhonenumber());
self::assertSame($mobileNumber->reveal(), $person->getMobilenumber());
$household = $person->getCurrentHousehold($clock->now());
self::assertNotNull($household);
$address = $household->getCurrentAddress(\DateTime::createFromImmutable($clock->now()));
self::assertNotNull($address);
self::assertEquals('Main Street', $address->getStreet());
self::assertEquals('42', $address->getStreetNumber());
self::assertSame($postalCode, $address->getPostcode());
self::assertEquals('Apartment 3B', $address->getExtra());
}
}

View File

@@ -149,9 +149,8 @@ export type TicketHistoryLine =
| CallerStateEvent;
interface BaseTicket<
T extends
| "ticket_ticket:simple"
| "ticket_ticket:extended" = "ticket_ticket:simple",
T extends "ticket_ticket:simple" | "ticket_ticket:extended" =
"ticket_ticket:simple",
> {
type_extended: T;
type: "ticket_ticket";