|null>(null);
+
+ function violationTitles(property: P): string[] {
+ if (null === violationsList.value) {
+ return [];
+ }
+ const r = violationsList.value.violationsByNormalizedProperty(property).map((v) => v.title);
+
+
+ return r;
+
+ }
+ function violationTitlesWithParameter<
+ P extends ViolationKey,
+ Param extends Extract
+ >(
+ property: P,
+ with_parameter: Param,
+ with_parameter_value: T[P][Param],
+ ): string[] {
+ if (violationsList.value === null) {
+ return [];
+ }
+ return violationsList.value.violationsByNormalizedPropertyAndParams(property, with_parameter, with_parameter_value)
+ .map((v) => v.title);
+ }
+
+
+ function hasViolation(property: P): boolean {
+ return violationTitles(property).length > 0;
+ }
+ function hasViolationWithParameter<
+ P extends ViolationKey,
+ Param extends Extract
+ >(
+ property: P,
+ with_parameter: Param,
+ with_parameter_value: T[P][Param],
+ ): boolean {
+ return violationTitlesWithParameter(property, with_parameter, with_parameter_value).length > 0;
+ }
+
+ function setValidationException>(validationException: V): void {
+ violationsList.value = validationException;
+ }
+
+ function cleanException(): void {
+ violationsList.value = null;
+ }
+
+ return {violationTitles, violationTitlesWithParameter, setValidationException, cleanException, hasViolationWithParameter, hasViolation};
+}
diff --git a/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php b/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php
index 2be3d54db..9f36c0a4d 100644
--- a/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php
+++ b/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php
@@ -80,9 +80,7 @@ class ExtractPhonenumberFromPattern
}
if (5 < $length) {
- $filtered = \trim(\strtr($subject, [$matches[0] => '']));
-
- return new SearchExtractionResult($filtered, [\implode('', $phonenumber)]);
+ return new SearchExtractionResult($subject, [\implode('', $phonenumber)]);
}
}
diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php
index 2493b1794..187199432 100644
--- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php
+++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php
@@ -22,7 +22,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
{
public function __construct(private readonly RequestStack $requestStack, private readonly ParameterBagInterface $parameterBag) {}
- public function denormalize($data, $type, $format = null, array $context = [])
+ public function denormalize($data, $type, $format = null, array $context = []): ?\DateTimeInterface
{
if (null === $data) {
return null;
@@ -51,7 +51,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
return $result;
}
- public function normalize($date, $format = null, array $context = [])
+ public function normalize($date, $format = null, array $context = []): array
{
/* @var DateTimeInterface $date */
switch ($format) {
diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/PhonenumberNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/PhonenumberNormalizer.php
index 28d7c623d..33ba52c44 100644
--- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/PhonenumberNormalizer.php
+++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/PhonenumberNormalizer.php
@@ -45,8 +45,11 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
try {
return $this->phoneNumberUtil->parse($data, $this->defaultCarrierCode);
- } catch (NumberParseException $e) {
- throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
+ } catch (NumberParseException) {
+ $phonenumber = new PhoneNumber();
+ $phonenumber->setRawInput($data);
+
+ return $phonenumber;
}
}
diff --git a/src/Bundle/ChillMainBundle/Tests/Search/Utils/ExtractPhonenumberFromPatternTest.php b/src/Bundle/ChillMainBundle/Tests/Search/Utils/ExtractPhonenumberFromPatternTest.php
index 3f5950a4c..1cb66738e 100644
--- a/src/Bundle/ChillMainBundle/Tests/Search/Utils/ExtractPhonenumberFromPatternTest.php
+++ b/src/Bundle/ChillMainBundle/Tests/Search/Utils/ExtractPhonenumberFromPatternTest.php
@@ -43,20 +43,20 @@ final class ExtractPhonenumberFromPatternTest extends KernelTestCase
yield ['BE', 'Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', 'no phonenumber and a date'];
- yield ['BE', 'Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
+ yield ['BE', 'Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo 0486 123 456', 'a phonenumber and a name'];
- yield ['BE', 'Diallo 123 456', 1, ['123456'], 'Diallo', 'a number and a name, without leadiing 0'];
+ yield ['BE', 'Diallo 123 456', 1, ['123456'], 'Diallo 123 456', 'a number and a name, without leadiing 0'];
- yield ['BE', '123 456', 1, ['123456'], '', 'only phonenumber'];
+ yield ['BE', '123 456', 1, ['123456'], '123 456', 'only phonenumber'];
- yield ['BE', '0123 456', 1, ['+32123456'], '', 'only phonenumber with a leading 0'];
+ yield ['BE', '0123 456', 1, ['+32123456'], '0123 456', 'only phonenumber with a leading 0'];
- yield ['FR', '123 456', 1, ['123456'], '', 'only phonenumber'];
+ yield ['FR', '123 456', 1, ['123456'], '123 456', 'only phonenumber'];
- yield ['FR', '0123 456', 1, ['+33123456'], '', 'only phonenumber with a leading 0'];
+ yield ['FR', '0123 456', 1, ['+33123456'], '0123 456', 'only phonenumber with a leading 0'];
- yield ['FR', 'Diallo 0486 123 456', 1, ['+33486123456'], 'Diallo', 'a phonenumber and a name'];
+ yield ['FR', 'Diallo 0486 123 456', 1, ['+33486123456'], 'Diallo 0486 123 456', 'a phonenumber and a name'];
- yield ['FR', 'Diallo +32486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
+ yield ['FR', 'Diallo +32486 123 456', 1, ['+32486123456'], 'Diallo +32486 123 456', 'a phonenumber and a name'];
}
}
diff --git a/src/Bundle/ChillMainBundle/Validation/Constraint/PhonenumberConstraint.php b/src/Bundle/ChillMainBundle/Validation/Constraint/PhonenumberConstraint.php
index 2c20de854..6b5be58b8 100644
--- a/src/Bundle/ChillMainBundle/Validation/Constraint/PhonenumberConstraint.php
+++ b/src/Bundle/ChillMainBundle/Validation/Constraint/PhonenumberConstraint.php
@@ -13,6 +13,9 @@ namespace Chill\MainBundle\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
+/**
+ * @deprecated use odolbeau/phonenumber validator instead
+ */
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
class PhonenumberConstraint extends Constraint
{
diff --git a/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php b/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php
index 0c02885ad..594429b15 100644
--- a/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php
+++ b/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php
@@ -16,6 +16,9 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
+/**
+ * @deprecated use https://github.com/odolbeau/phone-number-bundle/blob/master/src/Validator/Constraints/PhoneNumberValidator.php instead
+ */
final class ValidPhonenumber extends ConstraintValidator
{
public function __construct(private readonly LoggerInterface $logger, private readonly PhoneNumberHelperInterface $phonenumberHelper) {}
diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml
index f411f3289..fc8c7a48e 100644
--- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml
+++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml
@@ -150,34 +150,6 @@ filter_order:
Search: Chercher dans la liste
By date: Filtrer par date
search_box: Filtrer par contenu
-renderbox:
- person: "Usager"
- birthday:
- man: "Né le"
- woman: "Née le"
- neutral: "Né·e le"
- unknown: "Né·e le"
- deathdate: "Date de décès"
- household_without_address: "Le ménage de l'usager est sans adresse"
- no_data: "Aucune information renseignée"
- type:
- thirdparty: "Tiers"
- person: "Usager"
- holder: "Titulaire"
- years_old: >-
- {n, plural,
- =0 {0 an}
- one {1 an}
- other {# ans}
- }
- residential_address: "Adresse de résidence"
- located_at: "réside chez"
- household_number: "Ménage n°{number}"
- current_members: "Membres actuels"
- no_current_address: "Sans adresse actuellement"
- new_household: "Nouveau ménage"
- no_members_yet: "Aucun membre actuellement"
-
pick_entity:
add: "Ajouter"
modal_title: >-
diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml
index fd582f108..f79b93500 100644
--- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml
@@ -974,11 +974,12 @@ onthefly:
thirdparty: Détails du tiers
file_person: Ouvrir la fiche de l'usager
file_thirdparty: Voir le Tiers
+ file_default: Voir
edit:
person: Modifier un usager
thirdparty: Modifier un tiers
create:
- button: Créer {q}
+ button: Créer "q"
title:
default: Création d'un nouvel usager ou d'un tiers professionnel
person: Création d'un nouvel usager
diff --git a/src/Bundle/ChillPersonBundle/Actions/PersonCreate/PersonCreateDTO.php b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/PersonCreateDTO.php
new file mode 100644
index 000000000..bb8d4f79c
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/PersonCreateDTO.php
@@ -0,0 +1,70 @@
+ where the key is the altname's key
+ */
+ public array $altNames = [];
+
+ /**
+ * @var array
+ */
+ #[Assert\Valid(traverse: true)]
+ public array $identifiers = [];
+}
diff --git a/src/Bundle/ChillPersonBundle/Actions/PersonCreate/Service/PersonCreateDTOFactory.php b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/Service/PersonCreateDTOFactory.php
new file mode 100644
index 000000000..2259e345a
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/Service/PersonCreateDTOFactory.php
@@ -0,0 +1,96 @@
+firstName = $person->getFirstName();
+ $dto->lastName = $person->getLastName();
+ $dto->birthdate = $person->getBirthdate();
+ $dto->gender = $person->getGender();
+ $dto->civility = $person->getCivility();
+ $dto->phonenumber = $person->getPhonenumber();
+ $dto->mobilenumber = $person->getMobilenumber();
+ $dto->email = $person->getEmail();
+ $dto->center = $person->getCenter();
+ // address/addressForm are not mapped on Person entity; left to defaults
+
+ foreach ($this->configPersonAltNamesHelper->getChoices() as $key => $labels) {
+ $altName = $person->getAltNames()->findFirst(fn (int $k, PersonAltName $altName) => $altName->getKey() === $key);
+ if (null === $altName) {
+ $altName = new PersonAltName();
+ $altName->setKey($key);
+ }
+ $dto->altNames[$key] = $altName;
+ }
+
+ foreach ($this->personIdentifierManager->getWorkers() as $worker) {
+ $identifier = $person
+ ->getIdentifiers()
+ ->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition() === $identifier->getDefinition());
+ if (null === $identifier) {
+ $identifier = new PersonIdentifier($worker->getDefinition());
+ $person->addIdentifier($identifier);
+ }
+ $dto->identifiers['identifier_'.$worker->getDefinition()->getId()] = $identifier;
+ }
+
+ return $dto;
+ }
+
+ public function mapPersonCreateDTOtoPerson(PersonCreateDTO $personCreateDTO, Person $person): void
+ {
+ $person
+ ->setFirstName($personCreateDTO->firstName)
+ ->setLastName($personCreateDTO->lastName)
+ ->setBirthdate($personCreateDTO->birthdate)
+ ->setGender($personCreateDTO->gender)
+ ->setCivility($personCreateDTO->civility)
+ ->setPhonenumber($personCreateDTO->phonenumber)
+ ->setMobilenumber($personCreateDTO->mobilenumber)
+ ->setEmail($personCreateDTO->email)
+ ->setCenter($personCreateDTO->center);
+
+ foreach ($personCreateDTO->altNames as $altName) {
+ if ('' === $altName->getLabel()) {
+ $person->removeAltName($altName);
+ } else {
+ $person->addAltName($altName);
+ }
+ }
+
+ foreach ($personCreateDTO->identifiers as $identifier) {
+ $worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition());
+ if ($worker->isEmpty($identifier)) {
+ $person->removeIdentifier($identifier);
+ } else {
+ $person->addIdentifier($identifier);
+ }
+ }
+ // Note: address and addressForm are handled by controller/form during creation, not mapped here
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Actions/PersonEdit/PersonEditDTO.php b/src/Bundle/ChillPersonBundle/Actions/PersonEdit/PersonEditDTO.php
new file mode 100644
index 000000000..ed19e581d
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Actions/PersonEdit/PersonEditDTO.php
@@ -0,0 +1,108 @@
+ where the key is the altname's key
+ */
+ public array $altNames = [];
+
+ public string $memo = '';
+
+ public ?EmploymentStatus $employmentStatus = null;
+
+ public ?AdministrativeStatus $administrativeStatus = null;
+
+ public string $placeOfBirth = '';
+
+ public ?string $contactInfo = '';
+
+ #[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::FIXED_LINE, MisdPhoneNumberConstraint::VOIP, MisdPhoneNumberConstraint::PERSONAL_NUMBER])]
+ public ?PhoneNumber $phonenumber = null;
+
+ #[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::MOBILE])]
+ public ?PhoneNumber $mobilenumber = null;
+
+ public ?bool $acceptSms = false;
+
+ #[Assert\Valid(traverse: true)]
+ public Collection $otherPhonenumbers; // Collection
+
+ #[Assert\Email]
+ public ?string $email = '';
+
+ public ?bool $acceptEmail = false;
+
+ public ?Country $countryOfBirth = null;
+
+ public ?Country $nationality = null;
+
+ public Collection $spokenLanguages; // Collection
+
+ public ?Civility $civility = null;
+
+ public ?MaritalStatus $maritalStatus = null;
+
+ public ?\DateTimeInterface $maritalStatusDate = null;
+
+ #[Assert\Valid]
+ public CommentEmbeddable $maritalStatusComment;
+
+ public ?array $cFData = null;
+
+ /**
+ * @var array
+ */
+ #[Assert\Valid(traverse: true)]
+ public array $identifiers = [];
+}
diff --git a/src/Bundle/ChillPersonBundle/Actions/PersonEdit/Service/PersonEditDTOFactory.php b/src/Bundle/ChillPersonBundle/Actions/PersonEdit/Service/PersonEditDTOFactory.php
new file mode 100644
index 000000000..e25cd0ad2
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Actions/PersonEdit/Service/PersonEditDTOFactory.php
@@ -0,0 +1,130 @@
+firstName = $person->getFirstName();
+ $dto->lastName = $person->getLastName();
+ $dto->birthdate = $person->getBirthdate();
+ $dto->deathdate = (null !== $deathDate = $person->getDeathdate()) ? \DateTimeImmutable::createFromInterface($deathDate) : null;
+ $dto->gender = $person->getGender();
+ $dto->genderComment = $person->getGenderComment();
+ $dto->numberOfChildren = $person->getNumberOfChildren();
+ $dto->memo = $person->getMemo() ?? '';
+ $dto->employmentStatus = $person->getEmploymentStatus();
+ $dto->administrativeStatus = $person->getAdministrativeStatus();
+ $dto->placeOfBirth = $person->getPlaceOfBirth() ?? '';
+ $dto->contactInfo = $person->getcontactInfo();
+ $dto->phonenumber = $person->getPhonenumber();
+ $dto->mobilenumber = $person->getMobilenumber();
+ $dto->acceptSms = $person->getAcceptSMS();
+ $dto->otherPhonenumbers = $person->getOtherPhoneNumbers();
+ $dto->email = $person->getEmail();
+ $dto->acceptEmail = $person->getAcceptEmail();
+ $dto->countryOfBirth = $person->getCountryOfBirth();
+ $dto->nationality = $person->getNationality();
+ $dto->spokenLanguages = $person->getSpokenLanguages();
+ $dto->civility = $person->getCivility();
+ $dto->maritalStatus = $person->getMaritalStatus();
+ $dto->maritalStatusDate = $person->getMaritalStatusDate();
+ $dto->maritalStatusComment = $person->getMaritalStatusComment();
+ $dto->cFData = $person->getCFData();
+
+
+ foreach ($this->configPersonAltNamesHelper->getChoices() as $key => $labels) {
+ $altName = $person->getAltNames()->findFirst(fn (int $k, PersonAltName $altName) => $altName->getKey() === $key);
+ if (null === $altName) {
+ $altName = new PersonAltName();
+ $altName->setKey($key);
+ }
+ $dto->altNames[$key] = $altName;
+ }
+
+ foreach ($this->personIdentifierManager->getWorkers() as $worker) {
+ $identifier = $person
+ ->getIdentifiers()
+ ->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition() === $identifier->getDefinition());
+ if (null === $identifier) {
+ $identifier = new PersonIdentifier($worker->getDefinition());
+ $person->addIdentifier($identifier);
+ }
+ $dto->identifiers['identifier_'.$worker->getDefinition()->getId()] = $identifier;
+ }
+
+ return $dto;
+ }
+
+ public function mapPersonEditDTOtoPerson(PersonEditDTO $personEditDTO, Person $person): void
+ {
+ // Copy all editable fields from the DTO back to the Person entity
+ $person
+ ->setFirstName($personEditDTO->firstName)
+ ->setLastName($personEditDTO->lastName)
+ ->setBirthdate($personEditDTO->birthdate)
+ ->setDeathdate($personEditDTO->deathdate)
+ ->setGender($personEditDTO->gender)
+ ->setGenderComment($personEditDTO->genderComment)
+ ->setNumberOfChildren($personEditDTO->numberOfChildren)
+ ->setMemo($personEditDTO->memo)
+ ->setEmploymentStatus($personEditDTO->employmentStatus)
+ ->setAdministrativeStatus($personEditDTO->administrativeStatus)
+ ->setPlaceOfBirth($personEditDTO->placeOfBirth)
+ ->setcontactInfo($personEditDTO->contactInfo)
+ ->setPhonenumber($personEditDTO->phonenumber)
+ ->setMobilenumber($personEditDTO->mobilenumber)
+ ->setAcceptSMS($personEditDTO->acceptSms ?? false)
+ ->setOtherPhoneNumbers($personEditDTO->otherPhonenumbers)
+ ->setEmail($personEditDTO->email)
+ ->setAcceptEmail($personEditDTO->acceptEmail ?? false)
+ ->setCountryOfBirth($personEditDTO->countryOfBirth)
+ ->setNationality($personEditDTO->nationality)
+ ->setSpokenLanguages($personEditDTO->spokenLanguages)
+ ->setCivility($personEditDTO->civility)
+ ->setMaritalStatus($personEditDTO->maritalStatus)
+ ->setMaritalStatusDate($personEditDTO->maritalStatusDate)
+ ->setMaritalStatusComment($personEditDTO->maritalStatusComment)
+ ->setCFData($personEditDTO->cFData);
+
+ foreach ($personEditDTO->altNames as $altName) {
+ if ('' === $altName->getLabel()) {
+ $person->removeAltName($altName);
+ } else {
+ $person->addAltName($altName);
+ }
+ }
+
+ foreach ($personEditDTO->identifiers as $identifier) {
+ $worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition());
+ if ($worker->isEmpty($identifier)) {
+ $person->removeIdentifier($identifier);
+ } else {
+ $person->addIdentifier($identifier);
+ }
+ }
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Config/ConfigPersonAltNamesHelper.php b/src/Bundle/ChillPersonBundle/Config/ConfigPersonAltNamesHelper.php
index 4be480442..bac53aa23 100644
--- a/src/Bundle/ChillPersonBundle/Config/ConfigPersonAltNamesHelper.php
+++ b/src/Bundle/ChillPersonBundle/Config/ConfigPersonAltNamesHelper.php
@@ -28,6 +28,8 @@ class ConfigPersonAltNamesHelper
/**
* get the choices as key => values.
+ *
+ * @return array> where the key is the altName's key, and the value is an array of TranslatableString
*/
public function getChoices(): array
{
diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php
index 4698373f1..3b5cabd95 100644
--- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php
+++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php
@@ -11,36 +11,22 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
-use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
-use Chill\PersonBundle\Entity\Household\Household;
-use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
-use Chill\PersonBundle\Form\CreationPersonType;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\PersonBundle\Repository\PersonRepository;
-use Chill\PersonBundle\Search\SimilarPersonMatcher;
-use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\Form\Extension\Core\Type\SubmitType;
-use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
-use Symfony\Contracts\Translation\TranslatorInterface;
-use function hash;
final class PersonController extends AbstractController
{
public function __construct(
- private readonly AuthorizationHelperInterface $authorizationHelper,
- private readonly SimilarPersonMatcher $similarPersonMatcher,
- private readonly TranslatorInterface $translator,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly PersonRepository $personRepository,
private readonly ConfigPersonAltNamesHelper $configPersonAltNameHelper,
@@ -85,110 +71,6 @@ final class PersonController extends AbstractController
);
}
- /**
- * Method for creating a new person.
- *
- * The controller register data from a previous post on the form, and
- * register it in the session.
- *
- * The next post compare the data with previous one and, if yes, show a
- * review page if there are "alternate persons".
- */
- #[Route(path: '/{_locale}/person/new', name: 'chill_person_new')]
- public function newAction(Request $request): Response
- {
- $person = new Person();
-
- $authorizedCenters = $this->authorizationHelper->getReachableCenters($this->getUser(), PersonVoter::CREATE);
-
- if (1 === \count($authorizedCenters)) {
- $person->setCenter($authorizedCenters[0]);
- }
-
- $form = $this->createForm(CreationPersonType::class, $person)
- ->add('editPerson', SubmitType::class, [
- 'label' => 'Add the person',
- ])->add('createPeriod', SubmitType::class, [
- 'label' => 'Add the person and create an accompanying period',
- ])->add('createHousehold', SubmitType::class, [
- 'label' => 'Add the person and create a household',
- ]);
-
- $form->handleRequest($request);
-
- if (Request::METHOD_GET === $request->getMethod()) {
- $this->lastPostDataReset();
- } elseif (
- Request::METHOD_POST === $request->getMethod()
- && $form->isValid()
- ) {
- $alternatePersons = $this->similarPersonMatcher
- ->matchPerson($person);
-
- if (
- false === $this->isLastPostDataChanges($form, $request, true)
- || 0 === \count($alternatePersons)
- ) {
- $this->em->persist($person);
-
- $this->em->flush();
- $this->lastPostDataReset();
-
- $address = $form->get('address')->getData();
- $addressForm = (bool) $form->get('addressForm')->getData();
-
- if (null !== $address && $addressForm) {
- $household = new Household();
-
- $member = new HouseholdMember();
- $member->setPerson($person);
- $member->setStartDate(new \DateTimeImmutable());
-
- $household->addMember($member);
- $household->setForceAddress($address);
-
- $this->em->persist($member);
- $this->em->persist($household);
- $this->em->flush();
-
- if ($form->get('createHousehold')->isClicked()) {
- return $this->redirectToRoute('chill_person_household_members_editor', [
- 'persons' => [$person->getId()],
- 'household' => $household->getId(),
- ]);
- }
- }
-
- if ($form->get('createPeriod')->isClicked()) {
- return $this->redirectToRoute('chill_person_accompanying_course_new', [
- 'person_id' => [$person->getId()],
- ]);
- }
-
- if ($form->get('createHousehold')->isClicked()) {
- return $this->redirectToRoute('chill_person_household_members_editor', [
- 'persons' => [$person->getId()],
- ]);
- }
-
- return $this->redirectToRoute(
- 'chill_person_general_edit',
- ['person_id' => $person->getId()]
- );
- }
- } elseif (Request::METHOD_POST === $request->getMethod() && !$form->isValid()) {
- $this->addFlash('error', $this->translator->trans('This form contains errors'));
- }
-
- return $this->render(
- '@ChillPerson/Person/create.html.twig',
- [
- 'form' => $form->createView(),
- 'alternatePersons' => $alternatePersons ?? [],
- ]
- );
- }
-
#[Route(path: '/{_locale}/person/{person_id}/general', name: 'chill_person_view')]
public function viewAction(int $person_id)
{
@@ -250,51 +132,4 @@ final class PersonController extends AbstractController
return $errors;
}
-
- private function isLastPostDataChanges(Form $form, Request $request, bool $replace = false): bool
- {
- /** @var SessionInterface $session */
- $session = $this->get('session');
-
- if (!$session->has('last_person_data')) {
- return true;
- }
-
- $newPost = $this->lastPostDataBuildHash($form, $request);
-
- $isChanged = $session->get('last_person_data') !== $newPost;
-
- if ($replace) {
- $session->set('last_person_data', $newPost);
- }
-
- return $isChanged;
- }
-
- /**
- * build the hash for posted data.
- *
- * For privacy reasons, the data are hashed using sha512
- */
- private function lastPostDataBuildHash(Form $form, Request $request): string
- {
- $fields = [];
- $ignoredFields = ['form_status', '_token'];
-
- foreach ($request->request->all()[$form->getName()] as $field => $value) {
- if (\in_array($field, $ignoredFields, true)) {
- continue;
- }
- $fields[$field] = \is_array($value) ?
- \implode(',', $value) : $value;
- }
- ksort($fields);
-
- return \hash('sha512', \implode('&', $fields));
- }
-
- private function lastPostDataReset(): void
- {
- $this->get('session')->set('last_person_data', '');
- }
}
diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonCreateController.php b/src/Bundle/ChillPersonBundle/Controller/PersonCreateController.php
new file mode 100644
index 000000000..7ddd883d7
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Controller/PersonCreateController.php
@@ -0,0 +1,205 @@
+authorizationHelper->getReachableCenters($this->getUser(), PersonVoter::CREATE);
+
+ $dto = $this->personCreateDTOFactory->createPersonCreateDTO($person);
+
+ if (1 === \count($authorizedCenters)) {
+ $dto->center = $authorizedCenters[0];
+ }
+
+ $form = $this->createForm(CreationPersonType::class, $dto)
+ ->add('editPerson', SubmitType::class, [
+ 'label' => 'Add the person',
+ ])->add('createPeriod', SubmitType::class, [
+ 'label' => 'Add the person and create an accompanying period',
+ ])->add('createHousehold', SubmitType::class, [
+ 'label' => 'Add the person and create a household',
+ ]);
+
+ $form->handleRequest($request);
+
+ if (Request::METHOD_GET === $request->getMethod()) {
+ $this->lastPostDataReset($session);
+ } elseif (
+ Request::METHOD_POST === $request->getMethod()
+ && $form->isValid()
+ ) {
+ $alternatePersons = $this->similarPersonMatcher
+ ->matchPerson($person);
+
+ $createHouseholdButton = $form->get('createHousehold');
+ $createPeriodButton = $form->get('createPeriod');
+ $editPersonButton = $form->get('editPerson');
+
+ if (!$createHouseholdButton instanceof ClickableInterface) {
+ throw new \UnexpectedValueException();
+ }
+ if (!$createPeriodButton instanceof ClickableInterface) {
+ throw new \UnexpectedValueException();
+ }
+ if (!$editPersonButton instanceof ClickableInterface) {
+ throw new \UnexpectedValueException();
+ }
+
+ if (
+ false === $this->isLastPostDataChanges($form, $request, $session)
+ || 0 === \count($alternatePersons)
+ ) {
+ $this->personCreateDTOFactory->mapPersonCreateDTOtoPerson($dto, $person);
+ $this->em->persist($person);
+
+ $this->em->flush();
+ $this->lastPostDataReset($session);
+
+ $address = $dto->address;
+ $addressForm = $dto->addressForm;
+
+ if (null !== $address && $addressForm) {
+ $household = new Household();
+
+ $member = new HouseholdMember();
+ $member->setPerson($person);
+ $member->setStartDate(new \DateTimeImmutable());
+
+ $household->addMember($member);
+ $household->setForceAddress($address);
+
+ $this->em->persist($member);
+ $this->em->persist($household);
+ $this->em->flush();
+
+ if ($createHouseholdButton->isClicked()) {
+ return $this->redirectToRoute('chill_person_household_members_editor', [
+ 'persons' => [$person->getId()],
+ 'household' => $household->getId(),
+ ]);
+ }
+ }
+
+ if ($createPeriodButton->isClicked()) {
+ return $this->redirectToRoute('chill_person_accompanying_course_new', [
+ 'person_id' => [$person->getId()],
+ ]);
+ }
+
+ if ($createHouseholdButton->isClicked()) {
+ return $this->redirectToRoute('chill_person_household_members_editor', [
+ 'persons' => [$person->getId()],
+ ]);
+ }
+
+ return $this->redirectToRoute(
+ 'chill_person_general_edit',
+ ['person_id' => $person->getId()]
+ );
+ }
+ } elseif (Request::METHOD_POST === $request->getMethod() && !$form->isValid()) {
+ $this->addFlash('error', $this->translator->trans('This form contains errors'));
+ }
+
+ return $this->render(
+ '@ChillPerson/Person/create.html.twig',
+ [
+ 'form' => $form->createView(),
+ 'alternatePersons' => $alternatePersons ?? [],
+ ]
+ );
+ }
+
+ private function isLastPostDataChanges(FormInterface $form, Request $request, SessionInterface $session): bool
+ {
+ if (!$session->has('last_person_data')) {
+ return true;
+ }
+
+ $newPost = $this->lastPostDataBuildHash($form, $request);
+
+ $isChanged = $session->get('last_person_data') !== $newPost;
+ $session->set('last_person_data', $newPost);
+
+ return $isChanged;
+ }
+
+ /**
+ * build the hash for posted data.
+ *
+ * For privacy reasons, the data are hashed using sha512
+ */
+ private function lastPostDataBuildHash(FormInterface $form, Request $request): string
+ {
+ $fields = [];
+ $ignoredFields = ['form_status', '_token', 'identifiers'];
+
+ foreach ($request->request->all()[$form->getName()] as $field => $value) {
+ if (\in_array($field, $ignoredFields, true)) {
+ continue;
+ }
+ $fields[$field] = \is_array($value) ?
+ \implode(',', $value) : $value;
+ }
+ ksort($fields);
+
+ return \hash('sha512', \implode('&', $fields));
+ }
+
+ private function lastPostDataReset(SessionInterface $session): void
+ {
+ $session->set('last_person_data', '');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php b/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php
index be5b87bc3..e09bb97b6 100644
--- a/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php
+++ b/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository;
+use Chill\PersonBundle\Actions\PersonEdit\Service\PersonEditDTOFactory;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\PersonType;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
@@ -38,6 +39,7 @@ final readonly class PersonEditController
private EntityManagerInterface $entityManager,
private UrlGeneratorInterface $urlGenerator,
private Environment $twig,
+ private PersonEditDTOFactory $personEditDTOFactory,
) {}
/**
@@ -50,9 +52,11 @@ final readonly class PersonEditController
throw new AccessDeniedHttpException('You are not allowed to edit this person.');
}
+ $dto = $this->personEditDTOFactory->createPersonEditDTO($person);
+
$form = $this->formFactory->create(
PersonType::class,
- $person,
+ $dto,
['cFGroup' => $this->customFieldsDefaultGroupRepository->findOneByEntity(Person::class)?->getCustomFieldsGroup()]
);
@@ -62,6 +66,7 @@ final readonly class PersonEditController
$session
->getFlashBag()->add('error', new TranslatableMessage('This form contains errors'));
} elseif ($form->isSubmitted() && $form->isValid()) {
+ $this->personEditDTOFactory->mapPersonEditDTOtoPerson($dto, $person);
$this->entityManager->flush();
$session->getFlashBag()->add('success', new TranslatableMessage('The person data has been updated'));
diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonIdentifierListApiController.php b/src/Bundle/ChillPersonBundle/Controller/PersonIdentifierListApiController.php
new file mode 100644
index 000000000..2f0b3af28
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Controller/PersonIdentifierListApiController.php
@@ -0,0 +1,47 @@
+security->isGranted('ROLE_USER')) {
+ throw new AccessDeniedHttpException();
+ }
+
+ $workers = $this->personIdentifierManager->getWorkers();
+ $paginator = $this->paginatorFactory->create(count($workers));
+ $paginator->setItemsPerPage(count($workers));
+ $collection = new Collection($workers, $paginator);
+
+ return new JsonResponse($this->serializer->serialize($collection, 'json'), json: true);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php
index ffff789c2..095566b91 100644
--- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php
+++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php
@@ -96,7 +96,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
// We can get rid of this file when the service 'chill.person.repository.person' is no more used.
// We should use the PersonRepository service instead of a custom service name.
$loader->load('services/repository.yaml');
- $loader->load('services/serializer.yaml');
$loader->load('services/security.yaml');
$loader->load('services/doctrineEventListener.yaml');
$loader->load('services/accompanyingPeriodConsistency.yaml');
diff --git a/src/Bundle/ChillPersonBundle/Entity/Identifier/IdentifierPresenceEnum.php b/src/Bundle/ChillPersonBundle/Entity/Identifier/IdentifierPresenceEnum.php
new file mode 100644
index 000000000..120fed3f0
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Entity/Identifier/IdentifierPresenceEnum.php
@@ -0,0 +1,42 @@
+ PersonIdentifier::class])]
class PersonIdentifier
{
#[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\GeneratedValue]
+ #[Serializer\Groups(['read'])]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Person::class)]
@@ -28,14 +37,16 @@ class PersonIdentifier
private ?Person $person = null;
#[ORM\Column(name: 'value', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
+ #[Serializer\Groups(['read'])]
private array $value = [];
- #[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '[]'])]
+ #[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $canonical = '';
public function __construct(
#[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)]
- #[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
+ #[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
+ #[Serializer\Groups(['read'])]
private PersonIdentifierDefinition $definition,
) {}
diff --git a/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php b/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php
index 6d6112569..8b60c7814 100644
--- a/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php
+++ b/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php
@@ -11,30 +11,35 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Identifier;
+use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation as Serializer;
#[ORM\Entity]
#[ORM\Table(name: 'chill_person_identifier_definition')]
+#[Serializer\DiscriminatorMap('type', ['person_identifier_definition' => PersonIdentifierDefinition::class])]
class PersonIdentifierDefinition
{
#[ORM\Id]
- #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
+ #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\GeneratedValue]
+ #[Serializer\Groups(['read'])]
private ?int $id = null;
- #[ORM\Column(name: 'active', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
+ #[ORM\Column(name: 'active', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
private bool $active = true;
public function __construct(
- #[ORM\Column(name: 'label', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
+ #[ORM\Column(name: 'label', type: Types::JSON, nullable: false, options: ['default' => '[]'])]
private array $label,
- #[ORM\Column(name: 'engine', type: \Doctrine\DBAL\Types\Types::STRING, length: 100)]
+ #[ORM\Column(name: 'engine', type: Types::STRING, length: 100)]
+ #[Serializer\Groups(['read'])]
private string $engine,
- #[ORM\Column(name: 'is_searchable', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
+ #[ORM\Column(name: 'is_searchable', type: Types::BOOLEAN, options: ['default' => false])]
private bool $isSearchable = false,
- #[ORM\Column(name: 'is_editable_by_users', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => false])]
- private bool $isEditableByUsers = false,
- #[ORM\Column(name: 'data', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
+ #[ORM\Column(name: 'presence', type: Types::STRING, nullable: false, enumType: IdentifierPresenceEnum::class, options: ['default' => IdentifierPresenceEnum::ON_EDIT])]
+ private IdentifierPresenceEnum $presence = IdentifierPresenceEnum::ON_EDIT,
+ #[ORM\Column(name: 'data', type: Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private array $data = [],
) {}
@@ -58,11 +63,6 @@ class PersonIdentifierDefinition
return $this->engine;
}
- public function setEngine(string $engine): void
- {
- $this->engine = $engine;
- }
-
public function isSearchable(): bool
{
return $this->isSearchable;
@@ -75,12 +75,7 @@ class PersonIdentifierDefinition
public function isEditableByUsers(): bool
{
- return $this->isEditableByUsers;
- }
-
- public function setIsEditableByUsers(bool $isEditableByUsers): void
- {
- $this->isEditableByUsers = $isEditableByUsers;
+ return $this->presence->isEditableByUser();
}
public function isActive(): bool
@@ -104,4 +99,16 @@ class PersonIdentifierDefinition
{
$this->data = $data;
}
+
+ public function getPresence(): IdentifierPresenceEnum
+ {
+ return $this->presence;
+ }
+
+ public function setPresence(IdentifierPresenceEnum $presence): self
+ {
+ $this->presence = $presence;
+
+ return $this;
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php
index aa78774b6..af9135177 100644
--- a/src/Bundle/ChillPersonBundle/Entity/Person.php
+++ b/src/Bundle/ChillPersonBundle/Entity/Person.php
@@ -27,7 +27,6 @@ use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
-use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
@@ -36,6 +35,7 @@ use Chill\PersonBundle\Entity\Person\PersonCenterCurrent;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
use Chill\PersonBundle\Entity\Person\PersonResource;
+use Chill\PersonBundle\PersonIdentifier\Validator\RequiredIdentifierConstraint;
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
@@ -47,6 +47,7 @@ use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
+use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -273,6 +274,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
+ #[RequiredIdentifierConstraint]
+ #[Assert\Valid]
private Collection $identifiers;
/**
@@ -319,7 +322,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* The person's mobile phone number.
*/
#[ORM\Column(type: 'phone_number', nullable: true)]
- #[PhonenumberConstraint(type: 'mobile')]
+ #[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::MOBILE])]
private ?PhoneNumber $mobilenumber = null;
/**
@@ -359,7 +362,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* The person's phonenumber.
*/
#[ORM\Column(type: 'phone_number', nullable: true)]
- #[PhonenumberConstraint(type: 'landline')]
+ #[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::FIXED_LINE, MisdPhoneNumberConstraint::VOIP, MisdPhoneNumberConstraint::PERSONAL_NUMBER])]
private ?PhoneNumber $phonenumber = null;
/**
diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php
index bb40c6c35..7be6a50b1 100644
--- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php
+++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php
@@ -11,15 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Form;
-use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\MainBundle\Form\Type\PickCivilityType;
+use Chill\PersonBundle\Actions\PersonCreate\PersonCreateDTO;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
-use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Form\Type\PickGenderType;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
@@ -33,6 +32,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class CreationPersonType extends AbstractType
{
@@ -80,15 +80,18 @@ final class CreationPersonType extends AbstractType
->add('addressForm', CheckboxType::class, [
'label' => 'Create a household and add an address',
'required' => false,
- 'mapped' => false,
'help' => 'A new household will be created. The person will be member of this household.',
])
->add('address', PickAddressType::class, [
'required' => false,
- 'mapped' => false,
'label' => false,
]);
+ $builder->add('identifiers', PersonIdentifiersType::class, [
+ 'by_reference' => false,
+ 'step' => 'on_create',
+ ]);
+
if ($this->askCenters) {
$builder
->add('center', PickCenterType::class, [
@@ -112,7 +115,7 @@ final class CreationPersonType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
- 'data_class' => Person::class,
+ 'data_class' => PersonCreateDTO::class,
'constraints' => [
new Callback($this->validateCheckedAddress(...)),
],
@@ -129,10 +132,12 @@ final class CreationPersonType extends AbstractType
public function validateCheckedAddress($data, ExecutionContextInterface $context, $payload): void
{
- /** @var bool $addressFrom */
- $addressFrom = $context->getObject()->get('addressForm')->getData();
- /** @var ?Address $address */
- $address = $context->getObject()->get('address')->getData();
+ if (!$data instanceof PersonCreateDTO) {
+ throw new UnexpectedTypeException($data, PersonCreateDTO::class);
+ }
+
+ $addressFrom = $data->addressForm;
+ $address = $data->address;
if ($addressFrom && null === $address) {
$context->buildViolation('person_creation.If you want to create an household, an address is required')
diff --git a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonAltNameDataMapper.php b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonAltNameDataMapper.php
index 3a2a61cad..00986bddd 100644
--- a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonAltNameDataMapper.php
+++ b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonAltNameDataMapper.php
@@ -12,10 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Form\DataMapper;
use Chill\PersonBundle\Entity\PersonAltName;
-use Doctrine\Common\Collections\ArrayCollection;
-use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\DataMapperInterface;
-use Symfony\Component\Form\Exception\UnexpectedTypeException;
class PersonAltNameDataMapper implements DataMapperInterface
{
@@ -25,62 +22,24 @@ class PersonAltNameDataMapper implements DataMapperInterface
return;
}
- if (!$viewData instanceof Collection) {
- throw new UnexpectedTypeException($viewData, Collection::class);
- }
-
- $mapIndexToKey = [];
-
- foreach ($viewData->getIterator() as $key => $altName) {
- /* @var PersonAltName $altName */
- $mapIndexToKey[$altName->getKey()] = $key;
+ if (!is_array($viewData)) {
+ throw new \InvalidArgumentException('View data must be an array');
}
foreach ($forms as $key => $form) {
- if (\array_key_exists($key, $mapIndexToKey)) {
- $form->setData($viewData->get($mapIndexToKey[$key])->getLabel());
+ $personAltName = $viewData[$key];
+ if (!$personAltName instanceof PersonAltName) {
+ throw new \InvalidArgumentException('PersonAltName must be an instance of PersonAltName');
}
+ $form->setData($personAltName->getLabel());
}
}
public function mapFormsToData(\Traversable $forms, &$viewData): void
{
- $mapIndexToKey = [];
-
- if (\is_array($viewData)) {
- $dataIterator = $viewData;
- } else {
- $dataIterator = $viewData instanceof ArrayCollection ?
- $viewData->toArray() : $viewData->getIterator();
- }
-
- foreach ($dataIterator as $key => $altName) {
- /* @var PersonAltName $altName */
- $mapIndexToKey[$altName->getKey()] = $key;
- }
-
foreach ($forms as $key => $form) {
- $isEmpty = empty($form->getData());
-
- if (\array_key_exists($key, $mapIndexToKey)) {
- if ($isEmpty) {
- $viewData->remove($mapIndexToKey[$key]);
- } else {
- $viewData->get($mapIndexToKey[$key])->setLabel($form->getData());
- }
- } else {
- if (!$isEmpty) {
- $altName = (new PersonAltName())
- ->setKey($key)
- ->setLabel($form->getData());
-
- if (\is_array($viewData)) {
- $viewData[] = $altName;
- } else {
- $viewData->add($altName);
- }
- }
- }
+ $personAltName = array_find($viewData, fn (PersonAltName $altName) => $altName->getKey() === $key);
+ $personAltName->setLabel($form->getData());
}
}
}
diff --git a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php
index eea151865..bae359e34 100644
--- a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php
+++ b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php
@@ -14,60 +14,51 @@ namespace Chill\PersonBundle\Form\DataMapper;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\PersonIdentifier\Exception\UnexpectedTypeException;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
-use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
-use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\DataMapperInterface;
-use Symfony\Component\Form\FormInterface;
final readonly class PersonIdentifiersDataMapper implements DataMapperInterface
{
public function __construct(
private PersonIdentifierManagerInterface $identifierManager,
- private PersonIdentifierDefinitionRepository $identifierDefinitionRepository,
) {}
+ /**
+ * @pure
+ */
public function mapDataToForms($viewData, \Traversable $forms): void
{
- if (!$viewData instanceof Collection) {
- throw new UnexpectedTypeException($viewData, Collection::class);
+ if (!$viewData instanceof PersonIdentifier) {
+ throw new UnexpectedTypeException($viewData, PersonIdentifier::class);
}
- /** @var array $formsByKey */
- $formsByKey = iterator_to_array($forms);
- foreach ($this->identifierManager->getWorkers() as $worker) {
- if (!$worker->getDefinition()->isEditableByUsers()) {
- continue;
- }
- $form = $formsByKey['identifier_'.$worker->getDefinition()->getId()];
- $identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition()->getId() === $identifier->getId());
- if (null === $identifier) {
- $identifier = new PersonIdentifier($worker->getDefinition());
- }
- $form->setData($identifier->getValue());
+ $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($viewData->getDefinition());
+ if (!$worker->getDefinition()->isEditableByUsers()) {
+ return;
+ }
+ foreach ($forms as $key => $form) {
+ $form->setData($viewData->getValue()[$key] ?? $worker->getDefaultValue()[$key] ?? '');
}
}
+ /**
+ * @pure
+ */
public function mapFormsToData(\Traversable $forms, &$viewData): void
{
- if (!$viewData instanceof Collection) {
- throw new UnexpectedTypeException($viewData, Collection::class);
+ if (!$viewData instanceof PersonIdentifier) {
+ throw new UnexpectedTypeException($viewData, PersonIdentifier::class);
+ }
+ $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($viewData->getDefinition());
+ if (!$worker->getDefinition()->isEditableByUsers()) {
+ return;
}
+ $values = [];
foreach ($forms as $name => $form) {
- $identifierId = (int) substr((string) $name, 11);
- $identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getId() === $identifierId);
- $definition = $this->identifierDefinitionRepository->find($identifierId);
- if (null === $identifier) {
- $identifier = new PersonIdentifier($definition);
- $viewData->add($identifier);
- }
- if (!$identifier->getDefinition()->isEditableByUsers()) {
- continue;
- }
-
- $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($definition);
- $identifier->setValue($form->getData());
- $identifier->setCanonical($worker->canonicalizeValue($identifier->getValue()));
+ $values[$name] = $form->getData();
}
+
+ $viewData->setValue($values);
+ $viewData->setCanonical($worker->canonicalizeValue($viewData->getValue()));
}
}
diff --git a/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php b/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
index ea077f626..199b89bc8 100644
--- a/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
+++ b/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
@@ -12,10 +12,12 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
+use Chill\PersonBundle\Entity\Identifier\IdentifierPresenceEnum;
use Chill\PersonBundle\Form\DataMapper\PersonIdentifiersDataMapper;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
final class PersonIdentifiersType extends AbstractType
{
@@ -27,22 +29,34 @@ final class PersonIdentifiersType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
- foreach ($this->identifierManager->getWorkers() as $worker) {
+ foreach ($this->identifierManager->getWorkers() as $k => $worker) {
if (!$worker->getDefinition()->isEditableByUsers()) {
continue;
}
+ // skip some on creation
+ if ('on_create' === $options['step']
+ && IdentifierPresenceEnum::ON_EDIT === $worker->getDefinition()->getPresence()) {
+ continue;
+ }
+
$subBuilder = $builder->create(
'identifier_'.$worker->getDefinition()->getId(),
options: [
'compound' => true,
'label' => $this->translatableStringHelper->localize($worker->getDefinition()->getLabel()),
+ 'error_bubbling' => false,
]
);
+ $subBuilder->setDataMapper($this->identifiersDataMapper);
$worker->buildForm($subBuilder);
$builder->add($subBuilder);
}
+ }
- $builder->setDataMapper($this->identifiersDataMapper);
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefault('step', 'on_edit')
+ ->setAllowedValues('step', ['on_edit', 'on_create']);
}
}
diff --git a/src/Bundle/ChillPersonBundle/Form/PersonType.php b/src/Bundle/ChillPersonBundle/Form/PersonType.php
index 09ab04b01..60b75f325 100644
--- a/src/Bundle/ChillPersonBundle/Form/PersonType.php
+++ b/src/Bundle/ChillPersonBundle/Form/PersonType.php
@@ -21,8 +21,8 @@ use Chill\MainBundle\Form\Type\PickCivilityType;
use Chill\MainBundle\Form\Type\Select2CountryType;
use Chill\MainBundle\Form\Type\Select2LanguageType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
+use Chill\PersonBundle\Actions\PersonEdit\PersonEditDTO;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
-use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Form\Type\PersonPhoneType;
@@ -242,7 +242,7 @@ class PersonType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
- 'data_class' => Person::class,
+ 'data_class' => PersonEditDTO::class,
]);
$resolver->setRequired([
diff --git a/src/Bundle/ChillPersonBundle/Form/Type/PersonAltNameType.php b/src/Bundle/ChillPersonBundle/Form/Type/PersonAltNameType.php
index 6d983be37..5ee8830e0 100644
--- a/src/Bundle/ChillPersonBundle/Form/Type/PersonAltNameType.php
+++ b/src/Bundle/ChillPersonBundle/Form/Type/PersonAltNameType.php
@@ -32,6 +32,7 @@ class PersonAltNameType extends AbstractType
[
'label' => $label,
'required' => false,
+ 'empty_data' => '',
]
);
}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php
index 0801d4947..68620d869 100644
--- a/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php
@@ -13,20 +13,26 @@ namespace Chill\PersonBundle\PersonIdentifier\Identifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
+use Chill\PersonBundle\PersonIdentifier\IdentifierViolationDTO;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class StringIdentifier implements PersonIdentifierEngineInterface
{
+ public const NAME = 'chill-person-bundle.string-identifier';
+
+ private const ONLY_NUMBERS = 'only_numbers';
+ private const FIXED_LENGTH = 'fixed_length';
+
public static function getName(): string
{
- return 'chill-person-bundle.string-identifier';
+ return self::NAME;
}
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
{
- return $value['content'] ?? '';
+ return trim($value['content'] ?? '');
}
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void
@@ -36,6 +42,37 @@ final readonly class StringIdentifier implements PersonIdentifierEngineInterface
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
{
- return $identifier?->getValue()['content'] ?? '';
+ return trim($identifier?->getValue()['content'] ?? '');
+ }
+
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ return '' === trim($identifier->getValue()['content'] ?? '');
+ }
+
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ $config = $definition->getData();
+ $content = (string) ($identifier->getValue()['content'] ?? '');
+ $violations = [];
+
+ if (($config[self::ONLY_NUMBERS] ?? false) && !preg_match('/^[0-9]+$/', $content)) {
+ $violations[] = new IdentifierViolationDTO('person_identifier.only_number', '2a3352c0-a2b9-11f0-a767-b7a3f80e52f1');
+ }
+
+ if (null !== ($config[self::FIXED_LENGTH] ?? null) && strlen($content) !== $config[self::FIXED_LENGTH]) {
+ $violations[] = new IdentifierViolationDTO(
+ 'person_identifier.fixed_length',
+ '2b02a8fe-a2b9-11f0-bfe5-033300972783',
+ ['limit' => (string) $config[self::FIXED_LENGTH]]
+ );
+ }
+
+ return $violations;
+ }
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array
+ {
+ return ['content' => ''];
}
}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/IdentifierViolationDTO.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/IdentifierViolationDTO.php
new file mode 100644
index 000000000..3a74a4b34
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/IdentifierViolationDTO.php
@@ -0,0 +1,31 @@
+
+ */
+ public array $parameters = [],
+ public string $messageDomain = 'validators',
+ ) {}
+}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Normalizer/PersonIdentifierWorkerNormalizer.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Normalizer/PersonIdentifierWorkerNormalizer.php
new file mode 100644
index 000000000..432dea50c
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Normalizer/PersonIdentifierWorkerNormalizer.php
@@ -0,0 +1,39 @@
+ 'person_identifier_worker',
+ 'definition_id' => $object->getDefinition()->getId(),
+ 'engine' => $object->getDefinition()->getEngine(),
+ 'label' => $object->getDefinition()->getLabel(),
+ 'isActive' => $object->getDefinition()->isActive(),
+ 'presence' => $object->getDefinition()->getPresence()->value,
+ ];
+ }
+
+ public function supportsNormalization($data, ?string $format = null): bool
+ {
+ return $data instanceof PersonIdentifierWorker;
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php
index 6c75f263e..a29172958 100644
--- a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php
@@ -19,9 +19,29 @@ interface PersonIdentifierEngineInterface
{
public static function getName(): string;
+ /**
+ * @phpstan-pure
+ */
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string;
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void;
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string;
+
+ /**
+ * Return true if the identifier must be considered as empty.
+ *
+ * This is in use when the identifier is validated and must be required. If the identifier is empty and is required
+ * by the definition, the validation will fails.
+ */
+ public function isEmpty(PersonIdentifier $identifier): bool;
+
+ /**
+ * Return a list of @see{IdentifierViolationDTO} to generatie violation errors.
+ *
+ * @return list
+ */
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array;
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array;
}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManager.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManager.php
index 3cbc2c621..83e883fe4 100644
--- a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManager.php
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManager.php
@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\PersonIdentifier\Exception\EngineNotFoundException;
+use Chill\PersonBundle\PersonIdentifier\Exception\PersonIdentifierDefinitionNotFoundException;
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
final readonly class PersonIdentifierManager implements PersonIdentifierManagerInterface
@@ -44,8 +45,16 @@ final readonly class PersonIdentifierManager implements PersonIdentifierManagerI
return $workers;
}
- public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
+ public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
{
+ if (is_int($personIdentifierDefinition)) {
+ $id = $personIdentifierDefinition;
+ $personIdentifierDefinition = $this->personIdentifierDefinitionRepository->find($id);
+ if (null === $personIdentifierDefinition) {
+ throw new PersonIdentifierDefinitionNotFoundException($id);
+ }
+ }
+
return new PersonIdentifierWorker($this->getEngine($personIdentifierDefinition->getEngine()), $personIdentifierDefinition);
}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php
index 9bec7d1fd..b28fba7b6 100644
--- a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php
@@ -18,9 +18,16 @@ interface PersonIdentifierManagerInterface
/**
* Build PersonIdentifierWorker's for all active definition.
*
+ * Only active definition are returned.
+ *
* @return list
*/
public function getWorkers(): array;
- public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
+ /**
+ * @param int|PersonIdentifierDefinition $personIdentifierDefinition an instance of PersonIdentifierDefinition, or his id
+ *
+ * @throw PersonIdentifierNotFoundException
+ */
+ public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php
index 94702b8eb..529a98e84 100644
--- a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php
@@ -15,7 +15,7 @@ use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Symfony\Component\Form\FormBuilderInterface;
-final readonly class PersonIdentifierWorker
+readonly class PersonIdentifierWorker
{
public function __construct(
private PersonIdentifierEngineInterface $identifierEngine,
@@ -46,4 +46,25 @@ final readonly class PersonIdentifierWorker
{
return $this->identifierEngine->renderAsString($identifier, $this->definition);
}
+
+ /**
+ * Return true if the identifier must be considered as empty.
+ */
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ return $this->identifierEngine->isEmpty($identifier);
+ }
+
+ /**
+ * @return list
+ */
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ return $this->identifierEngine->validate($identifier, $definition);
+ }
+
+ public function getDefaultValue(): array
+ {
+ return $this->identifierEngine->getDefaultValue($this->definition);
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php
new file mode 100644
index 000000000..68a75c7cf
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php
@@ -0,0 +1,28 @@
+identifierManager->getWorkers() as $worker) {
+ if (IdentifierPresenceEnum::REQUIRED !== $worker->getDefinition()->getPresence()) {
+ continue;
+ }
+
+ $identifier = $value->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getDefinition() === $worker->getDefinition());
+
+ if (null === $identifier || $worker->isEmpty($identifier)) {
+ $this->context->buildViolation($constraint->message)
+ ->setParameter('{{ value }}', $worker->renderAsString($identifier))
+ ->setParameter('definition_id', (string) $worker->getDefinition()->getId())
+ ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05')
+ ->addViolation();
+ }
+ }
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/UniqueIdentifierConstraint.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/UniqueIdentifierConstraint.php
new file mode 100644
index 000000000..b15c4a8b2
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/UniqueIdentifierConstraint.php
@@ -0,0 +1,25 @@
+personIdentifierRepository->findByDefinitionAndCanonical($value->getDefinition(), $value->getValue());
+
+ if (count($identifiers) > 0) {
+ if (count($identifiers) > 1 || $identifiers[0]->getPerson() !== $value->getPerson()) {
+ $persons = array_map(fn (PersonIdentifier $idf): string => $this->personRender->renderString($idf->getPerson(), []), $identifiers);
+
+ $this->context->buildViolation($constraint->message)
+ ->setParameter('{{ persons }}', implode(', ', $persons))
+ ->setParameter('definition_id', (string) $value->getDefinition()->getId())
+ ->addViolation();
+
+ }
+ }
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/ValidIdentifierConstraint.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/ValidIdentifierConstraint.php
new file mode 100644
index 000000000..24b3551b1
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/ValidIdentifierConstraint.php
@@ -0,0 +1,23 @@
+personIdentifierManager->buildWorkerByPersonIdentifierDefinition($value->getDefinition());
+
+ $violations = $worker->validate($value, $value->getDefinition());
+
+ foreach ($violations as $violation) {
+ $this->context->buildViolation($violation->message)
+ ->setParameters($violation->parameters)
+ ->setParameter('{{ code }}', $violation->code)
+ ->setParameter('definition_id', (string) $value->getDefinition()->getId())
+ ->addViolation();
+ }
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierRepository.php b/src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierRepository.php
new file mode 100644
index 000000000..e3af66e31
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierRepository.php
@@ -0,0 +1,42 @@
+createQueryBuilder('p')
+ ->where('p.definition = :definition')
+ ->andWhere('p.canonical = :canonical')
+ ->setParameter('definition', $definition)
+ ->setParameter(
+ 'canonical',
+ is_string($valueOrCanonical) ?
+ $valueOrCanonical :
+ $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($valueOrCanonical),
+ )
+ ->getQuery()
+ ->getResult();
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php
index 0d972914d..0aab3faf3 100644
--- a/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php
+++ b/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php
@@ -17,6 +17,8 @@ use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person;
+use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
+use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
@@ -27,7 +29,13 @@ use Symfony\Component\Security\Core\Security;
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
{
- public function __construct(private Security $security, private EntityManagerInterface $em, private CountryRepository $countryRepository, private AuthorizationHelperInterface $authorizationHelper) {}
+ public function __construct(
+ private Security $security,
+ private EntityManagerInterface $em,
+ private CountryRepository $countryRepository,
+ private AuthorizationHelperInterface $authorizationHelper,
+ private PersonIdentifierManagerInterface $personIdentifierManager,
+ ) {}
public function buildAuthorizedQuery(
?string $default = null,
@@ -107,6 +115,15 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
$query
->setFromClause('chill_person_person AS person');
+ $idDefinitionWorkers = array_map(
+ fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->getId(),
+ array_filter(
+ $this->personIdentifierManager->getWorkers(),
+ fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->isSearchable()
+ )
+ );
+ $idDefinitionWorkerQuestionMarks = implode(', ', array_fill(0, count($idDefinitionWorkers), '?'));
+
$pertinence = [];
$pertinenceArgs = [];
$andWhereSearchClause = [];
@@ -124,20 +141,53 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
'(starts_with(LOWER(UNACCENT(lastname)), UNACCENT(LOWER(?))))::int';
\array_push($pertinenceArgs, $str, $str, $str, $str);
- $andWhereSearchClause[] =
- '(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR '.
- "person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
- \array_push($andWhereSearchClauseArgs, $str, $str);
+ $q = [
+ 'LOWER(UNACCENT(?)) <<% person.fullnamecanonical',
+ "person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ",
+ ];
+ $qArguments = [$str, $str];
+
+ if (count($idDefinitionWorkers) > 0) {
+ $q[] = $mq = "EXISTS (
+ SELECT 1 FROM chill_person_identifier AS identifier
+ WHERE identifier.canonical LIKE LOWER(UNACCENT(?)) || '%' AND identifier.definition_id IN ({$idDefinitionWorkerQuestionMarks})
+ AND person.id = identifier.person_id
+ )";
+ $pertinence[] = "({$mq})::int * 1000000";
+ $qArguments = [...$qArguments, $str, ...$idDefinitionWorkers];
+ $pertinenceArgs = [...$pertinenceArgs, $str, ...$idDefinitionWorkers];
+ }
+
+ $andWhereSearchClause[] = '('.implode(' OR ', $q).')';
+ $andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, ...$qArguments];
}
- $query->andWhereClause(
- \implode(' AND ', $andWhereSearchClause),
- $andWhereSearchClauseArgs
- );
} else {
$pertinence = ['1'];
$pertinenceArgs = [];
}
+
+ if (null !== $phonenumber) {
+ $personPhoneClause = "person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%')";
+ if (count($andWhereSearchClauseArgs) > 0) {
+ $initialSearchClause = '(('.\implode(' AND ', $andWhereSearchClause).') OR '.$personPhoneClause.')';
+ }
+ $andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, $phonenumber, $phonenumber, $phonenumber];
+
+ // drastically increase pertinence
+ $pertinence[] = "(person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%'))::int * 1000000";
+ $pertinenceArgs = [...$pertinenceArgs, $phonenumber, $phonenumber, $phonenumber];
+ } else {
+ $initialSearchClause = \implode(' AND ', $andWhereSearchClause);
+ }
+
+ if (isset($initialSearchClause)) {
+ $query->andWhereClause(
+ $initialSearchClause,
+ $andWhereSearchClauseArgs
+ );
+ }
+
$query
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs);
@@ -176,14 +226,6 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
);
}
- if (null !== $phonenumber) {
- $query->andWhereClause(
- "person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'",
- [$phonenumber, $phonenumber, $phonenumber]
- );
- $query->setFromClause($query->getFromClause().' LEFT JOIN chill_person_phone pp ON pp.person_id = person.id');
- }
-
if (null !== $city) {
$query->setFromClause($query->getFromClause().' '.
'JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id '.
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/types.ts b/src/Bundle/ChillPersonBundle/Resources/public/types.ts
index 6dfd732e2..4523325e8 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/types.ts
+++ b/src/Bundle/ChillPersonBundle/Resources/public/types.ts
@@ -10,16 +10,40 @@ import {
Scope,
Job,
PrivateCommentEmbeddable,
+ TranslatableString,
+ DateTimeWrite,
+ SetGender,
+ SetCenter,
+ SetCivility, Gender,
} from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
-import Person from "./vuejs/_components/OnTheFly/Person.vue";
+/**
+ * An alternative name, as configured locally
+ */
export interface AltName {
- label: string;
+ labels: TranslatableString;
key: string;
}
+
+export interface PersonAltNameWrite {
+ key: string;
+ value: string;
+}
+
+/**
+ * An altname for a person
+ */
+export interface PersonAltName {
+ label: string;
+ /**
+ * will match a key in @link{AltName}
+ */
+ key: string;
+}
+
export interface Person {
id: number;
type: "person";
@@ -27,7 +51,7 @@ export interface Person {
textAge: string;
firstName: string;
lastName: string;
- altNames: AltName[];
+ altNames: PersonAltName[];
suffixText: string;
current_household_address: Address | null;
birthdate: DateTime | null;
@@ -36,11 +60,63 @@ export interface Person {
phonenumber: string;
mobilenumber: string;
email: string;
- gender: "woman" | "man" | "other";
+ gender: Gender;
centers: Center[];
civility: Civility | null;
current_household_id: number;
- current_residential_addresses: Address[];
+ current_residential_addresses: ResidentialAddress[];
+ /**
+ * The person id as configured by the user
+ */
+ personId: string;
+ identifiers: PersonIdentifier[];
+}
+
+export interface PersonIdentifier {
+ id: number;
+ type: "person_identifier";
+ value: object;
+ definition: PersonIdentifierDefinition;
+}
+
+export interface PersonIdentifierDefinition {
+ id: number;
+ type: "person_identifier_definition";
+ engine: string;
+}
+
+export interface ResidentialAddress {
+ address: Address | null;
+ endDate: DateTime | null;
+ hostPerson: Person | null;
+ hostThirdParty: Thirdparty | null;
+ startDate: DateTime | null;
+}
+
+export interface PersonIdentifierWrite {
+ type: "person_identifier";
+ definition_id: number;
+ value: object;
+}
+
+/**
+ * Person representation to create or update a Person
+ */
+export interface PersonWrite {
+ type: "person";
+ firstName: string;
+ lastName: string;
+ altNames: PersonAltNameWrite[];
+ addressId: number | null;
+ birthdate: DateTimeWrite | null;
+ deathdate: DateTimeWrite | null;
+ phonenumber: string;
+ mobilenumber: string;
+ email: string;
+ gender: SetGender | null;
+ center: SetCenter | null;
+ civility: SetCivility | null;
+ identifiers: PersonIdentifierWrite[];
}
export interface AccompanyingPeriod {
@@ -342,22 +418,50 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
workflows: object[];
}
+/**
+ * Entity types that a user can create through AddPersons component
+ */
+export type CreatableEntityType = "person" | "thirdparty";
+
+/**
+ * Entities that can be search and selected by a user
+ */
export type EntityType =
+ | CreatableEntityType
| "user_group"
| "user"
- | "person"
- | "thirdparty"
| "household";
-export type Entities = (UserGroup | User | Person | Thirdparty | Household) & {
- address?: Address | null;
- kind?: string;
- text?: string;
- profession?: string;
-};
+export type Entities = (UserGroup | User | Person | Thirdparty | Household);
+
+export function isEntityHousehold(e: Entities): e is Household {
+ return e.type === "household";
+}
export type EntitiesOrMe = "me" | Entities;
+
+// Type guards to discriminate Suggestions by their result kind
+export function isSuggestionForUserGroup(s: Suggestion): s is Suggestion & { result: UserGroup } {
+ return (s as any)?.result?.type === "user_group";
+}
+
+export function isSuggestionForUser(s: Suggestion): s is Suggestion & { result: User } {
+ return (s as any)?.result?.type === "user";
+}
+
+export function isSuggestionForPerson(s: Suggestion): s is Suggestion & { result: Person } {
+ return (s as any)?.result?.type === "person";
+}
+
+export function isSuggestionForThirdParty(s: Suggestion): s is Suggestion & { result: Thirdparty } {
+ return (s as any)?.result?.type === "thirdparty";
+}
+
+export function isSuggestionForHousehold(s: Suggestion): s is Suggestion & { result: Household } {
+ return (s as any)?.result?.type === "household";
+}
+
export type AddPersonResult = Entities & {
parent?: Entities | null;
};
@@ -365,9 +469,8 @@ export type AddPersonResult = Entities & {
export interface Suggestion {
key: string;
relevance: number;
- result: AddPersonResult;
+ result: Entities;
}
-
export interface SearchPagination {
first: number;
items_per_page: number;
@@ -379,12 +482,13 @@ export interface SearchPagination {
export interface Search {
count: number;
pagination: SearchPagination;
- results: Suggestion[];
+ results: {relevance: number, result: Entities}[];
}
export interface SearchOptions {
uniq: boolean;
- type: string[];
+ /** @deprecated */
+ type: EntityType[];
priority: number | null;
button: {
size: string;
@@ -394,6 +498,17 @@ export interface SearchOptions {
};
}
+type PersonIdentifierPresence = 'NOT_EDITABLE' | 'ON_EDIT' | 'ON_CREATION' | 'REQUIRED';
+
+export interface PersonIdentifierWorker {
+ type: "person_identifier_worker";
+ definition_id: number;
+ engine: string;
+ label: TranslatableString;
+ isActive: boolean;
+ presence: PersonIdentifierPresence;
+}
+
export class MakeFetchException extends Error {
sta: number;
txt: string;
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue
index b4e0e70b8..e023c227b 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue
@@ -84,8 +84,8 @@
-
{
@@ -322,7 +325,7 @@ export default {
}
});
},
- addNewPersons({ selected, modal }) {
+ addNewPersons({ selected }) {
//console.log('@@@ CLICK button addNewPersons', selected);
this.$store
.dispatch("addRequestor", selected.shift())
@@ -337,7 +340,6 @@ export default {
});
this.$refs.addPersons.resetSearch(); // to cast child method
- modal.showModal = false;
},
saveFormOnTheFly(payload) {
console.log(
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue
index fa600bfdd..d09bc4fd7 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue
@@ -39,8 +39,8 @@
-
{
- const url = `/api/1.0/person/person/${id}.json`;
- return fetch(url).then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-};
-
-const getPersonAltNames = () =>
- fetch("/api/1.0/person/config/alt_names.json").then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-
-const getCivilities = () =>
- fetch("/api/1.0/main/civility.json").then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-
-const getGenders = () => makeFetch("GET", "/api/1.0/main/gender.json");
-// .then(response => {
-// console.log(response)
-// if (response.ok) { return response.json(); }
-// throw Error('Error with request resource response');
-// });
-
-const getCentersForPersonCreation = () =>
- makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);
-
-/*
- * POST a new person
- */
-const postPerson = (body) => {
- const url = `/api/1.0/person/person.json`;
- return fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json;charset=utf-8",
- },
- body: JSON.stringify(body),
- }).then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-};
-
-/*
- * PATCH an existing person
- */
-const patchPerson = (id, body) => {
- const url = `/api/1.0/person/person/${id}.json`;
- return fetch(url, {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json;charset=utf-8",
- },
- body: JSON.stringify(body),
- }).then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-};
-
-export {
- getCentersForPersonCreation,
- getPerson,
- getPersonAltNames,
- getCivilities,
- getGenders,
- postPerson,
- patchPerson,
-};
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts
new file mode 100644
index 000000000..44114a67f
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts
@@ -0,0 +1,116 @@
+import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
+import {Center, Civility, Gender, SetCenter} from "ChillMainAssets/types";
+import {
+ AltName,
+ Person, PersonIdentifier,
+ PersonIdentifierWorker,
+ PersonWrite,
+} from "ChillPersonAssets/types";
+import person from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
+
+/*
+ * GET a person by id
+ */
+export const getPerson = async (id: number): Promise => {
+ const url = `/api/1.0/person/person/${id}.json`;
+ return fetch(url).then((response) => {
+ if (response.ok) {
+ return response.json();
+ }
+ throw Error("Error with request resource response");
+ });
+};
+
+export const personToWritePerson = (person: Person): PersonWrite => {
+ return {
+ type: "person",
+ firstName: person.firstName,
+ lastName: person.lastName,
+ altNames: person.altNames.map((altName) => ({key: altName.key, value: altName.label})),
+ addressId: null,
+ birthdate: null === person.birthdate ? null : {datetime: person.birthdate.datetime8601},
+ deathdate: null === person.deathdate ? null : {datetime: person.deathdate.datetime8601},
+ phonenumber: person.phonenumber,
+ mobilenumber: person.mobilenumber,
+ center: null === person.centers ? null : person.centers
+ .map((center): SetCenter => ({id: center.id, type: "center"}))
+ .find(() => true) || null,
+ email: person.email,
+ civility: null === person.civility ? null : {id: person.civility.id, type: "chill_main_civility"},
+ gender: null === person.gender ? null : {id: person.gender.id, type: "chill_main_gender"},
+ identifiers: person.identifiers.map((identifier: PersonIdentifier) => ({type: "person_identifier", definition_id: identifier.definition.id, value: identifier.value})),
+ }
+}
+
+export const getPersonAltNames = async (): Promise =>
+ fetch("/api/1.0/person/config/alt_names.json").then((response) => {
+ if (response.ok) {
+ return response.json();
+ }
+ throw Error("Error with request resource response");
+ });
+
+export const getCivilities = async (): Promise =>
+ fetchResults("/api/1.0/main/civility.json");
+
+export const getGenders = async (): Promise =>
+ fetchResults("/api/1.0/main/gender.json");
+
+export const getCentersForPersonCreation = async (): Promise<{
+ showCenters: boolean;
+ centers: Center[];
+}> => makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);
+
+export const getPersonIdentifiers = async (): Promise<
+ PersonIdentifierWorker[]
+> => fetchResults("/api/1.0/person/identifiers/workers");
+
+export interface WritePersonViolationMap
+ extends Record> {
+ firstName: {
+ "{{ value }}": string
+ };
+ lastName: {
+ "{{ value }}": string;
+ };
+ gender: {
+ "{{ value }}": string;
+ };
+ mobilenumber: {
+ "{{ types }}": string; // ex: "mobile number"
+ "{{ value }}": string; // ex: "+33 1 02 03 04 05"
+ };
+ phonenumber: {
+ "{{ types }}": string; // ex: "mobile number"
+ "{{ value }}": string; // ex: "+33 1 02 03 04 05"
+ };
+ email: {
+ "{{ value }}": string;
+ };
+ center: {
+ "{{ value }}": string;
+ };
+ civility: {
+ "{{ value }}": string;
+ };
+ birthdate: {};
+ identifiers: {
+ "{{ value }}": string;
+ "definition_id": string;
+ };
+}
+export const createPerson = async (person: PersonWrite): Promise => {
+ return makeFetch(
+ "POST",
+ "/api/1.0/person/person.json",
+ person,
+ );
+};
+
+export const editPerson = async (person: PersonWrite, personId: number): Promise => {
+ return makeFetch(
+ "PATCH",
+ `/api/1.0/person/person/${personId}.json`,
+ person,
+ );
+}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
index 24cdca24e..417ef4b7a 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
@@ -3,487 +3,242 @@
class="btn"
:class="getClassButton"
:title="buttonTitle"
- @click="openModal"
+ @click="openModalChoose"
>
{{ buttonTitle }}
-
-
-
-
- {{ modalTitle }}
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonChooseModal.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonChooseModal.vue
new file mode 100644
index 000000000..8c0ddb802
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonChooseModal.vue
@@ -0,0 +1,345 @@
+
+
+ emit('close')"
+ :modal-dialog-class="modalDialogClass"
+ :hide-footer="false"
+ >
+
+ {{ modalTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
emit('updateSelected', payload)"
+ @trigger-add-contact="triggerAddContact"
+ />
+
+
+
+ {{
+ trans(ADD_PERSONS_SUGGESTED_COUNTER, {
+ count: suggestedCounter,
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue
index 4499b40c8..aeb8dcac3 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue
@@ -1,39 +1,38 @@
-
+
@@ -51,31 +50,38 @@ import SuggestionHousehold from "./TypeHousehold.vue";
import SuggestionUserGroup from "./TypeUserGroup.vue";
// Types
-import { Result, Suggestion } from "ChillPersonAssets/types";
+import {
+ isSuggestionForHousehold,
+ isSuggestionForPerson,
+ isSuggestionForThirdParty, isSuggestionForUser,
+ isSuggestionForUserGroup,
+ Suggestion
+} from "ChillPersonAssets/types";
+import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
const props = defineProps<{
item: Suggestion;
- search: { selected: Suggestion[] };
- type: string;
+ isSelected: boolean;
+ type: "radio"|"checkbox";
+}>();
+const emit = defineEmits<{
+ (e: "updateSelected", payload: {suggestion: Suggestion, isSelected: boolean}): void;
+ (e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
}>();
-const emit = defineEmits(["updateSelected", "newPriorSuggestion"]);
-// v-model for selected
-const selected = computed({
- get: () => props.search.selected,
- set: (value) => emit("updateSelected", value),
-});
+const isChecked = computed
(() => props.isSelected)
-const isChecked = computed(
- () => props.search.selected.indexOf(props.item) !== -1,
-);
-
-function setValueByType(value: Suggestion, type: string) {
- return type === "radio" ? [value] : value;
+const onUpdateValue = (event: Event) => {
+ const target = event?.target;
+ if (!(target instanceof HTMLInputElement)) {
+ console.error("the value of checked is not an HTMLInputElement");
+ return;
+ }
+ emit("updateSelected", {suggestion: props.item, isSelected: props.type === "radio" ? true : target.checked});
}
-function newPriorSuggestion(response: Result) {
- emit("newPriorSuggestion", response);
+function triggerAddContact(payload: {parent: ThirdpartyCompany}) {
+ emit("triggerAddContact", payload);
}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeHousehold.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeHousehold.vue
index a1e33c4de..4a99dcaac 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeHousehold.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeHousehold.vue
@@ -16,9 +16,10 @@ import { defineProps } from "vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import HouseholdRenderBox from "ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue";
import { Suggestion } from "ChillPersonAssets/types";
+import {Household} from "ChillMainAssets/types";
interface TypeHouseholdProps {
- item: Suggestion;
+ item: Suggestion & {result: Household};
}
defineProps();
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypePerson.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypePerson.vue
index 98119bd8e..adba7af03 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypePerson.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypePerson.vue
@@ -23,7 +23,7 @@ import { computed, defineProps } from "vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
-import { Person } from "ChillPersonAssets/types";
+import {Person, Suggestion} from "ChillPersonAssets/types";
function formatDate(dateString: string | undefined, format: string) {
if (!dateString) return "";
@@ -36,9 +36,7 @@ function formatDate(dateString: string | undefined, format: string) {
}
const props = defineProps<{
- item: {
- result: Person; // add other fields as needed
- };
+ item: Suggestion & { result: Person },
}>();
const hasBirthdate = computed(() => props.item.result.birthdate !== null);
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeThirdParty.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeThirdParty.vue
index 28be7c119..d665118cd 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeThirdParty.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeThirdParty.vue
@@ -1,7 +1,7 @@
- {{
+ {{
item.result.profession
}}
{{ item.result.text }}
@@ -12,20 +12,18 @@
-
-
> {{ item.result.parent?.text }}
+
+ > {{ item.result.parent.text }}
@@ -37,15 +35,19 @@ import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { useToast } from "vue-toast-notification";
import { Result, Suggestion } from "ChillPersonAssets/types";
-import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
+import {
+ isThirdpartyChild,
+ isThirdpartyContact,
+ Thirdparty, ThirdpartyCompany
+} from "./../../../../../../ChillThirdPartyBundle/Resources/public/types";
interface TypeThirdPartyProps {
- item: Suggestion;
+ item: Suggestion & {result: Thirdparty};
}
const props = defineProps
();
-const emit = defineEmits<(e: "newPriorSuggestion", payload: unknown) => void>();
+const emit = defineEmits<(e: "triggerAddContact", payload: {parent: ThirdpartyCompany}) => void>();
const onTheFly = ref | null>(null);
const toast = useToast();
@@ -54,47 +56,23 @@ const hasAddress = computed(() => {
if (props.item.result.address !== null) {
return true;
}
- if (props.item.result.parent !== null) {
- if (props.item.result.parent) {
- return props.item.result.parent.address !== null;
- }
+ if (isThirdpartyChild(props.item.result) && props.item.result.parent !== null) {
+ return props.item.result.parent.address !== null;
}
- return false;
-});
-const hasParent = computed(() => {
- return props.item.result.parent !== null;
+ return false;
});
const getAddress = computed(() => {
if (props.item.result.address !== null) {
return props.item.result.address;
}
- if (props.item.result.parent && props.item.result.parent.address !== null) {
+ if (isThirdpartyChild(props.item.result) && props.item.result.parent !== null && props.item.result.parent.address !== null) {
return props.item.result.parent.address;
}
return null;
});
-function saveFormOnTheFly({ data }: { data: Thirdparty }) {
- makeFetch("POST", "/api/1.0/thirdparty/thirdparty.json", data)
- .then((response: unknown) => {
- const result = response as Result;
- emit("newPriorSuggestion", result);
- if (onTheFly.value) onTheFly.value.closeModal();
- })
- .catch((error: unknown) => {
- const errorResponse = error as { name: string; violations: string[] };
- if (errorResponse.name === "ValidationException") {
- for (let v of errorResponse.violations) {
- if (toast) toast.open({ message: v });
- }
- } else {
- if (toast) toast.open({ message: "An error occurred" });
- }
- });
-}
-
// i18n config (if needed elsewhere)
const i18n = {
messages: {
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUser.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUser.vue
index 74f80c321..9daf3219f 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUser.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUser.vue
@@ -1,11 +1,11 @@
-
+
@@ -14,18 +14,14 @@ import { computed, defineProps } from "vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { Suggestion } from "ChillPersonAssets/types";
+import {User} from "ChillMainAssets/types";
interface TypeUserProps {
- item: Suggestion;
+ item: Suggestion & {result: User};
}
const props = defineProps();
-const hasParent = computed(() => props.item.result.parent !== null);
-
-defineExpose({
- hasParent,
-});
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig
index 1ceb84395..5a17acf18 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig
@@ -88,6 +88,15 @@
{% endfor %}
{% endif %}
+ {% if form.identifiers|length > 0 %}
+ {% for f in form.identifiers %}
+
+ {{ form_row(f) }}
+
+ {% endfor %}
+ {% else %}
+ {{ form_widget(form.identifiers) }}
+ {% endif %}
{{ form_row(form.gender, { 'label' : 'Gender'|trans }) }}
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig
index fe5dde242..169479854 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig
@@ -140,9 +140,7 @@
{% else %}
diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonDenormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonDenormalizer.php
new file mode 100644
index 000000000..9f0464a71
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonDenormalizer.php
@@ -0,0 +1,155 @@
+extractObjectToPopulate($type, $context);
+
+ if (null === $person) {
+ $person = new Person();
+ }
+
+ // Setters applied directly per known field for readability
+ if (\array_key_exists('firstName', $data)) {
+ $person->setFirstName($data['firstName']);
+ }
+
+ if (\array_key_exists('lastName', $data)) {
+ $person->setLastName($data['lastName']);
+ }
+
+ if (\array_key_exists('phonenumber', $data)) {
+ $person->setPhonenumber($this->denormalizer->denormalize($data['phonenumber'], PhoneNumber::class, $format, $context));
+ }
+
+ if (\array_key_exists('mobilenumber', $data)) {
+ $person->setMobilenumber($this->denormalizer->denormalize($data['mobilenumber'], PhoneNumber::class, $format, $context));
+ }
+
+ if (\array_key_exists('gender', $data) && null !== $data['gender']) {
+ $gender = $this->denormalizer->denormalize($data['gender'], Gender::class, $format, []);
+ $person->setGender($gender);
+ }
+
+ if (\array_key_exists('birthdate', $data)) {
+ $object = $this->denormalizer->denormalize($data['birthdate'], \DateTime::class, $format, $context);
+ $person->setBirthdate($object);
+ }
+
+ if (\array_key_exists('deathdate', $data)) {
+ $object = $this->denormalizer->denormalize($data['deathdate'], \DateTimeImmutable::class, $format, $context);
+ $person->setDeathdate($object);
+ }
+
+ if (\array_key_exists('center', $data)) {
+ $object = $this->denormalizer->denormalize($data['center'], Center::class, $format, $context);
+ $person->setCenter($object);
+ }
+
+ if (\array_key_exists('altNames', $data)) {
+ foreach ($data['altNames'] as $altNameData) {
+ if (!array_key_exists('key', $altNameData)
+ || !array_key_exists('value', $altNameData)
+ || '' === trim((string) $altNameData['key'])
+ ) {
+ throw new UnexpectedValueException('format for alt name is not correct');
+ }
+ $altNameKey = $altNameData['key'];
+ $altNameValue = $altNameData['value'];
+
+ $altName = $person->getAltNames()->findFirst(fn (int $key, PersonAltName $personAltName) => $personAltName->getKey() === $altNameKey);
+ if (null === $altName) {
+ $altName = new PersonAltName();
+ $person->addAltName($altName);
+ }
+ $altName->setKey($altNameKey)->setLabel($altNameValue);
+ }
+ }
+
+ if (\array_key_exists('identifiers', $data)) {
+ foreach ($data['identifiers'] as $identifierData) {
+ if (!array_key_exists('definition_id', $identifierData)
+ || !array_key_exists('value', $identifierData)
+ || !is_int($identifierData['definition_id'])
+ || !is_array($identifierData['value'])
+ ) {
+ throw new UnexpectedValueException('format for identifiers is not correct');
+ }
+
+ $definitionId = $identifierData['definition_id'];
+ $value = $identifierData['value'];
+
+ $worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definitionId);
+
+ if (!$worker->getDefinition()->isEditableByUsers()) {
+ continue;
+ }
+
+ $personIdentifier = $person->getIdentifiers()->findFirst(fn (int $key, PersonIdentifier $personIdentifier) => $personIdentifier->getDefinition()->getId() === $definitionId);
+ if (null === $personIdentifier) {
+ $personIdentifier = new PersonIdentifier($worker->getDefinition());
+ $person->addIdentifier($personIdentifier);
+ }
+
+ $personIdentifier->setValue($value);
+ $personIdentifier->setCanonical($worker->canonicalizeValue($value));
+
+ if ($worker->isEmpty($personIdentifier)) {
+ $person->removeIdentifier($personIdentifier);
+ }
+ }
+ }
+
+ if (\array_key_exists('email', $data)) {
+ $person->setEmail($data['email']);
+ }
+
+ if (\array_key_exists('civility', $data) && null !== $data['civility']) {
+ $civility = $this->denormalizer->denormalize($data['civility'], Civility::class, $format, []);
+ $person->setCivility($civility);
+ }
+
+ return $person;
+ }
+
+ public function supportsDenormalization($data, $type, $format = null): bool
+ {
+ return Person::class === $type && 'person' === ($data['type'] ?? null) && !isset($data['id']);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php
index 88c38cfbb..2dcc5bdd9 100644
--- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php
+++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php
@@ -11,169 +11,33 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer;
-use Chill\MainBundle\Entity\Center;
-use Chill\MainBundle\Entity\Civility;
-use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
-use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Doctrine\Common\Collections\Collection;
-use libphonenumber\PhoneNumber;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
-use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
-use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
-use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Serialize a Person entity.
*/
-class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwareInterface, PersonJsonNormalizerInterface
+class PersonJsonNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
- use DenormalizerAwareTrait;
-
use NormalizerAwareTrait;
- use ObjectToPopulateTrait;
-
public function __construct(
private readonly ChillEntityRenderExtension $render,
- /* TODO: replace by PersonRenderInterface, as sthis is the only one required */
- private readonly PersonRepository $repository,
private readonly CenterResolverManagerInterface $centerResolverManager,
private readonly ResidentialAddressRepository $residentialAddressRepository,
private readonly PhoneNumberHelperInterface $phoneNumberHelper,
+ private readonly \Chill\PersonBundle\PersonIdentifier\Rendering\PersonIdRenderingInterface $personIdRendering,
) {}
- public function denormalize($data, $type, $format = null, array $context = [])
- {
- $person = $this->extractObjectToPopulate($type, $context);
-
- if (\array_key_exists('id', $data) && null === $person) {
- $person = $this->repository->find($data['id']);
-
- if (null === $person) {
- throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
- }
-
- // currently, not allowed to update a person through api
- // if instantiated with id
- return $person;
- }
-
- if (null === $person) {
- $person = new Person();
- }
-
- $fields = [
- 'firstName',
- 'lastName',
- 'phonenumber',
- 'mobilenumber',
- 'gender',
- 'birthdate',
- 'deathdate',
- 'center',
- 'altNames',
- 'email',
- 'civility',
- ];
-
- $fields = array_filter(
- $fields,
- static fn (string $field): bool => \array_key_exists($field, $data)
- );
-
- foreach ($fields as $item) {
- switch ($item) {
- case 'firstName':
- $person->setFirstName($data[$item]);
-
- break;
-
- case 'lastName':
- $person->setLastName($data[$item]);
-
- break;
-
- case 'phonenumber':
- $person->setPhonenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
-
- break;
-
- case 'mobilenumber':
- $person->setMobilenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
-
- break;
-
- case 'gender':
- $gender = $this->denormalizer->denormalize($data[$item], Gender::class, $format, []);
-
- $person->setGender($gender);
-
- break;
-
- case 'birthdate':
- $object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
-
- $person->setBirthdate($object);
-
- break;
-
- case 'deathdate':
- $object = $this->denormalizer->denormalize($data[$item], \DateTimeImmutable::class, $format, $context);
-
- $person->setDeathdate($object);
-
- break;
-
- case 'center':
- $object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context);
- $person->setCenter($object);
-
- break;
-
- case 'altNames':
- foreach ($data[$item] as $altName) {
- $oldAltName = $person
- ->getAltNames()
- ->filter(static fn (PersonAltName $n): bool => $n->getKey() === $altName['key'])->first();
-
- if (false === $oldAltName) {
- $newAltName = new PersonAltName();
- $newAltName->setKey($altName['key']);
- $newAltName->setLabel($altName['label']);
- $person->addAltName($newAltName);
- } else {
- $oldAltName->setLabel($altName['label']);
- }
- }
-
- break;
-
- case 'email':
- $person->setEmail($data[$item]);
-
- break;
-
- case 'civility':
- $civility = $this->denormalizer->denormalize($data[$item], Civility::class, $format, []);
-
- $person->setCivility($civility);
-
- break;
- }
- }
-
- return $person;
- }
-
/**
* @param Person $person
* @param string|null $format
@@ -204,6 +68,8 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
'email' => $person->getEmail(),
'gender' => $this->normalizer->normalize($person->getGender(), $format, $context),
'civility' => $this->normalizer->normalize($person->getCivility(), $format, $context),
+ 'personId' => $this->personIdRendering->renderPersonId($person),
+ 'identifiers' => $this->normalizer->normalize($person->getIdentifiers(), $format, $context),
];
if (\in_array('minimal', $groups, true) && 1 === \count($groups)) {
@@ -215,11 +81,6 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
null];
}
- public function supportsDenormalization($data, $type, $format = null)
- {
- return Person::class === $type && 'person' === ($data['type'] ?? null);
- }
-
public function supportsNormalization($data, $format = null): bool
{
return $data instanceof Person && 'json' === $format;
diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonReadDenormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonReadDenormalizer.php
new file mode 100644
index 000000000..ce8e5ce48
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonReadDenormalizer.php
@@ -0,0 +1,51 @@
+repository->find($data['id']);
+
+ if (null === $person) {
+ throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
+ }
+
+ return $person;
+ }
+
+ throw new LogicException();
+ }
+
+ public function supportsDenormalization($data, string $type, ?string $format = null)
+ {
+ return is_array($data) && Person::class === $type && 'person' === ($data['type'] ?? null) && isset($data['id']);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Action/PersonEdit/Service/PersonEditDTOFactoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Action/PersonEdit/Service/PersonEditDTOFactoryTest.php
new file mode 100644
index 000000000..28241b14a
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Action/PersonEdit/Service/PersonEditDTOFactoryTest.php
@@ -0,0 +1,146 @@
+createMock(ConfigPersonAltNamesHelper::class);
+ $identifierManager = $this->createMock(PersonIdentifierManagerInterface::class);
+ $factory = new PersonEditDTOFactory($configHelper, $identifierManager);
+
+ $dto = new PersonEditDTO();
+ $dto->firstName = 'John';
+ $dto->lastName = 'Doe';
+ $dto->birthdate = new \DateTime('1980-05-10');
+ $dto->deathdate = new \DateTimeImmutable('2050-01-01');
+ $dto->gender = new Gender();
+ $dto->genderComment = new CommentEmbeddable('gender comment');
+ $dto->numberOfChildren = 2;
+ $dto->memo = 'Some memo';
+ $dto->employmentStatus = new EmploymentStatus();
+ $dto->administrativeStatus = new AdministrativeStatus();
+ $dto->placeOfBirth = 'Cityville';
+ $dto->contactInfo = 'Some contact info';
+ $phone = new PhoneNumber();
+ $dto->phonenumber = $phone;
+ $mobile = new PhoneNumber();
+ $dto->mobilenumber = $mobile;
+ $dto->acceptSms = true;
+ $dto->otherPhonenumbers = new ArrayCollection();
+ $dto->email = 'john.doe@example.org';
+ $dto->acceptEmail = true;
+ $dto->countryOfBirth = new Country();
+ $dto->nationality = new Country();
+ $dto->spokenLanguages = new ArrayCollection([new Language()]);
+ $dto->civility = new Civility();
+ $dto->maritalStatus = new MaritalStatus();
+ $dto->maritalStatusDate = new \DateTime('2010-01-01');
+ $dto->maritalStatusComment = new CommentEmbeddable('married');
+ $dto->cFData = ['foo' => 'bar'];
+
+ $person = new Person();
+
+ $factory->mapPersonEditDTOtoPerson($dto, $person);
+
+ self::assertSame('John', $person->getFirstName());
+ self::assertSame('Doe', $person->getLastName());
+ self::assertSame($dto->birthdate, $person->getBirthdate());
+ self::assertSame($dto->deathdate, $person->getDeathdate());
+ self::assertSame($dto->gender, $person->getGender());
+ self::assertSame($dto->genderComment, $person->getGenderComment());
+ self::assertSame($dto->numberOfChildren, $person->getNumberOfChildren());
+ self::assertSame('Some memo', $person->getMemo());
+ self::assertSame($dto->employmentStatus, $person->getEmploymentStatus());
+ self::assertSame($dto->administrativeStatus, $person->getAdministrativeStatus());
+ self::assertSame('Cityville', $person->getPlaceOfBirth());
+ self::assertSame('Some contact info', $person->getcontactInfo());
+ self::assertSame($phone, $person->getPhonenumber());
+ self::assertSame($mobile, $person->getMobilenumber());
+ self::assertTrue($person->getAcceptSMS());
+ self::assertSame($dto->otherPhonenumbers, $person->getOtherPhoneNumbers());
+ self::assertSame('john.doe@example.org', $person->getEmail());
+ self::assertTrue($person->getAcceptEmail());
+ self::assertSame($dto->countryOfBirth, $person->getCountryOfBirth());
+ self::assertSame($dto->nationality, $person->getNationality());
+ self::assertSame($dto->spokenLanguages, $person->getSpokenLanguages());
+ self::assertSame($dto->civility, $person->getCivility());
+ self::assertSame($dto->maritalStatus, $person->getMaritalStatus());
+ self::assertSame($dto->maritalStatusDate, $person->getMaritalStatusDate());
+ self::assertSame($dto->maritalStatusComment, $person->getMaritalStatusComment());
+ self::assertSame($dto->cFData, $person->getCFData());
+ }
+
+ public function testAltNamesHandlingWithConfigHelper(): void
+ {
+ $configHelper = $this->createMock(ConfigPersonAltNamesHelper::class);
+ $configHelper->method('getChoices')->willReturn([
+ 'aka' => ['en' => 'Also Known As'],
+ 'nickname' => ['en' => 'Nickname'],
+ ]);
+
+ $identifierManager = $this->createMock(PersonIdentifierManagerInterface::class);
+ $identifierManager->method('getWorkers')->willReturn([]);
+
+ $factory = new PersonEditDTOFactory($configHelper, $identifierManager);
+ $person = new Person();
+
+ $dto = $factory->createPersonEditDTO($person);
+
+ // Assert DTO has two altNames keys from helper
+ self::assertCount(2, $dto->altNames);
+ self::assertContainsOnlyInstancesOf(PersonAltName::class, $dto->altNames);
+ self::assertSame(['aka', 'nickname'], array_keys($dto->altNames));
+ self::assertSame(['aka' => 'aka', 'nickname' => 'nickname'], array_map(fn (PersonAltName $altName) => $altName->getKey(), $dto->altNames));
+
+ // Fill only one label and leave the other empty
+ $dto->altNames['aka']->setLabel('The Boss');
+ // 'nickname' remains empty by default
+
+ // Map DTO back to person
+ $factory->mapPersonEditDTOtoPerson($dto, $person);
+
+ // Assert only the filled alt name is persisted on the person
+ $altNames = $person->getAltNames();
+ self::assertCount(1, $altNames);
+ $altNameArray = $altNames->toArray();
+ self::assertSame('aka', $altNameArray[0]->getKey());
+ self::assertSame('The Boss', $altNameArray[0]->getLabel());
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonIdentifierListApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonIdentifierListApiControllerTest.php
new file mode 100644
index 000000000..e4df5446a
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonIdentifierListApiControllerTest.php
@@ -0,0 +1,157 @@
+prophesize(Security::class);
+ $security->isGranted('ROLE_USER')->willReturn(false)->shouldBeCalledOnce();
+
+ $serializer = new Serializer([new PersonIdentifierWorkerNormalizer(), new CollectionNormalizer()], [new JsonEncoder()]);
+
+ $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
+ $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
+
+ $controller = new PersonIdentifierListApiController(
+ $security->reveal(),
+ $serializer,
+ $personIdentifierManager->reveal(),
+ $paginatorFactory->reveal(),
+ );
+
+ $this->expectException(AccessDeniedHttpException::class);
+ $controller->list();
+ }
+
+ public function testListSuccess(): void
+ {
+ // Build 3 workers
+ $engine = new class () implements PersonIdentifierEngineInterface {
+ public static function getName(): string
+ {
+ return 'dummy';
+ }
+
+ public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
+ {
+ return null;
+ }
+
+ public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
+
+ public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
+ {
+ return '';
+ }
+
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ return false;
+ }
+
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+ };
+
+ $definition1 = new PersonIdentifierDefinition(['en' => 'Label 1'], 'dummy');
+ $definition2 = new PersonIdentifierDefinition(['en' => 'Label 2'], 'dummy');
+ $definition3 = new PersonIdentifierDefinition(['en' => 'Label 3'], 'dummy');
+ // simulate persisted ids
+ $r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
+ $r->setAccessible(true);
+ $r->setValue($definition1, 1);
+ $r->setValue($definition2, 2);
+ $r->setValue($definition3, 3);
+
+ $workers = [
+ new PersonIdentifierWorker($engine, $definition1),
+ new PersonIdentifierWorker($engine, $definition2),
+ new PersonIdentifierWorker($engine, $definition3),
+ ];
+
+ $security = $this->prophesize(Security::class);
+ $security->isGranted('ROLE_USER')->willReturn(true)->shouldBeCalledOnce();
+
+ $personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
+ $personIdentifierManager->getWorkers()->willReturn($workers)->shouldBeCalledOnce();
+
+ $paginator = $this->prophesize(\Chill\MainBundle\Pagination\PaginatorInterface::class);
+ $paginator->setItemsPerPage(3)->shouldBeCalledOnce();
+ $paginator->getCurrentPageFirstItemNumber()->willReturn(0);
+ $paginator->getItemsPerPage()->willReturn(count($workers));
+ $paginator->getTotalItems()->willReturn(count($workers));
+ $paginator->hasNextPage()->willReturn(false);
+ $paginator->hasPreviousPage()->willReturn(false);
+
+ $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
+ $paginatorFactory->create(3)->willReturn($paginator->reveal())->shouldBeCalledOnce();
+
+ $serializer = new Serializer([
+ new PersonIdentifierWorkerNormalizer(),
+ new CollectionNormalizer(),
+ ], [new JsonEncoder()]);
+
+ $controller = new PersonIdentifierListApiController(
+ $security->reveal(),
+ $serializer,
+ $personIdentifierManager->reveal(),
+ $paginatorFactory->reveal(),
+ );
+
+ $response = $controller->list();
+ self::assertSame(200, $response->getStatusCode());
+ $body = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
+ self::assertIsArray($body);
+ self::assertArrayHasKey('count', $body);
+ self::assertArrayHasKey('pagination', $body);
+ self::assertArrayHasKey('results', $body);
+ self::assertSame(3, $body['count']);
+ self::assertCount(3, $body['results']);
+ // spot check one item
+ self::assertSame('person_identifier_worker', $body['results'][0]['type']);
+ self::assertSame(1, $body['results'][0]['id']);
+ self::assertSame('dummy', $body['results'][0]['engine']);
+ self::assertSame(['en' => 'Label 1'], $body['results'][0]['label']);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Identifier/StringIdentifierValidationTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Identifier/StringIdentifierValidationTest.php
new file mode 100644
index 000000000..436c15589
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Identifier/StringIdentifierValidationTest.php
@@ -0,0 +1,111 @@
+ 'Test'], engine: StringIdentifier::NAME);
+ if ([] !== $data) {
+ $definition->setData($data);
+ }
+
+ return $definition;
+ }
+
+ private function makeIdentifier(PersonIdentifierDefinition $definition, ?string $content): PersonIdentifier
+ {
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setValue(['content' => $content]);
+
+ return $identifier;
+ }
+
+ public function testValidateWithoutOptionsHasNoViolations(): void
+ {
+ $definition = $this->makeDefinition();
+ $identifier = $this->makeIdentifier($definition, 'AB-123');
+
+ $engine = new StringIdentifier();
+ $violations = $engine->validate($identifier, $definition);
+
+ self::assertIsArray($violations);
+ self::assertCount(0, $violations);
+ }
+
+ public function testValidateOnlyNumbersOption(): void
+ {
+ $definition = $this->makeDefinition(['only_numbers' => true]);
+ $engine = new StringIdentifier();
+
+ // valid numeric content
+ $identifierOk = $this->makeIdentifier($definition, '123456');
+ $violationsOk = $engine->validate($identifierOk, $definition);
+ self::assertCount(0, $violationsOk);
+
+ // invalid alphanumeric content
+ $identifierBad = $this->makeIdentifier($definition, '12AB');
+ $violationsBad = $engine->validate($identifierBad, $definition);
+ self::assertCount(1, $violationsBad);
+ self::assertSame('person_identifier.only_number', $violationsBad[0]->message);
+ self::assertSame('2a3352c0-a2b9-11f0-a767-b7a3f80e52f1', $violationsBad[0]->code);
+ }
+
+ public function testValidateFixedLengthOption(): void
+ {
+ $definition = $this->makeDefinition(['fixed_length' => 5]);
+ $engine = new StringIdentifier();
+
+ // valid exact length
+ $identifierOk = $this->makeIdentifier($definition, 'ABCDE');
+ $violationsOk = $engine->validate($identifierOk, $definition);
+ self::assertCount(0, $violationsOk);
+
+ // invalid length (too short)
+ $identifierBad = $this->makeIdentifier($definition, 'AB');
+ $violationsBad = $engine->validate($identifierBad, $definition);
+ self::assertCount(1, $violationsBad);
+ self::assertSame('person_identifier.fixed_length', $violationsBad[0]->message);
+ self::assertSame('2b02a8fe-a2b9-11f0-bfe5-033300972783', $violationsBad[0]->code);
+ self::assertSame(['limit' => '5'], $violationsBad[0]->parameters);
+ }
+
+ public function testValidateOnlyNumbersAndFixedLengthTogether(): void
+ {
+ $definition = $this->makeDefinition(['only_numbers' => true, 'fixed_length' => 4]);
+ $engine = new StringIdentifier();
+
+ // valid: numeric and correct length
+ $identifierOk = $this->makeIdentifier($definition, '1234');
+ $violationsOk = $engine->validate($identifierOk, $definition);
+ self::assertCount(0, $violationsOk);
+
+ // invalid: non-numeric and wrong length -> two violations expected
+ $identifierBad = $this->makeIdentifier($definition, 'AB');
+ $violationsBad = $engine->validate($identifierBad, $definition);
+ self::assertCount(2, $violationsBad);
+ // Order is defined by implementation: numbers check first, then length
+ self::assertSame('person_identifier.only_number', $violationsBad[0]->message);
+ self::assertSame('person_identifier.fixed_length', $violationsBad[1]->message);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Normalizer/PersonIdentifierWorkerNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Normalizer/PersonIdentifierWorkerNormalizerTest.php
new file mode 100644
index 000000000..c54d360d3
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Normalizer/PersonIdentifierWorkerNormalizerTest.php
@@ -0,0 +1,133 @@
+ 'SSN'], engine: 'string');
+ $worker = new PersonIdentifierWorker($engine, $definition);
+
+ $normalizer = new PersonIdentifierWorkerNormalizer();
+
+ self::assertTrue($normalizer->supportsNormalization($worker));
+ self::assertFalse($normalizer->supportsNormalization(new \stdClass()));
+ }
+
+ public function testNormalizeReturnsExpectedArray(): void
+ {
+ $engine = new class () implements PersonIdentifierEngineInterface {
+ public static function getName(): string
+ {
+ return 'dummy';
+ }
+
+ public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
+ {
+ return null;
+ }
+
+ public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
+
+ public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
+ {
+ return '';
+ }
+
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ return false;
+ }
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+ };
+
+ $definition = new PersonIdentifierDefinition(label: ['en' => 'SSN'], engine: 'string');
+ $definition->setActive(false);
+ $worker = new PersonIdentifierWorker($engine, $definition);
+
+ $normalizer = new PersonIdentifierWorkerNormalizer();
+ $normalized = $normalizer->normalize($worker);
+
+ self::assertSame([
+ 'type' => 'person_identifier_worker',
+ 'definition_id' => null,
+ 'engine' => 'string',
+ 'label' => ['en' => 'SSN'],
+ 'isActive' => false,
+ 'presence' => 'ON_EDIT',
+ ], $normalized);
+ }
+
+ public function testNormalizeThrowsOnInvalidObject(): void
+ {
+ $normalizer = new PersonIdentifierWorkerNormalizer();
+ $this->expectException(UnexpectedValueException::class);
+ $normalizer->normalize(new \stdClass());
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Rendering/PersonIdRenderingTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Rendering/PersonIdRenderingTest.php
index ce1df3f9d..8f2592665 100644
--- a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Rendering/PersonIdRenderingTest.php
+++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Rendering/PersonIdRenderingTest.php
@@ -71,6 +71,21 @@ class PersonIdRenderingTest extends TestCase
// same behavior as StringIdentifier::renderAsString
return $identifier?->getValue()['content'] ?? '';
}
+
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ return false;
+ }
+
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
};
return new PersonIdentifierWorker($engine, $definition);
diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php
new file mode 100644
index 000000000..86201b6fb
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php
@@ -0,0 +1,131 @@
+requiredDefinition = new PersonIdentifierDefinition(
+ label: ['fr' => 'Identifiant requis'],
+ engine: 'test.engine',
+ );
+ $this->requiredDefinition->setPresence(IdentifierPresenceEnum::REQUIRED);
+ $reflection = new \ReflectionClass($this->requiredDefinition);
+ $id = $reflection->getProperty('id');
+ $id->setValue($this->requiredDefinition, 1);
+
+ // Mock only the required methods of the engine used by the validator through the worker
+ $engineProphecy = $this->prophesize(PersonIdentifierEngineInterface::class);
+ $engineProphecy->isEmpty(Argument::type(PersonIdentifier::class))
+ ->will(function (array $args): bool {
+ /** @var PersonIdentifier $identifier */
+ $identifier = $args[0];
+
+ return '' === trim($identifier->getValue()['content'] ?? '');
+ });
+ $engineProphecy->renderAsString(Argument::any(), Argument::any())
+ ->will(function (array $args): string {
+ /** @var PersonIdentifier|null $identifier */
+ $identifier = $args[0] ?? null;
+
+ return $identifier?->getValue()['content'] ?? '';
+ });
+
+ $worker = new PersonIdentifierWorker($engineProphecy->reveal(), $this->requiredDefinition);
+
+ // Mock only the required method used by the validator
+ $managerProphecy = $this->prophesize(PersonIdentifierManagerInterface::class);
+ $managerProphecy->getWorkers()->willReturn([$worker]);
+
+ return new RequiredIdentifierConstraintValidator($managerProphecy->reveal());
+ }
+
+ public function testThrowsOnNonCollectionValue(): void
+ {
+ $this->expectException(UnexpectedValueException::class);
+ $this->validator->validate(new \stdClass(), new RequiredIdentifierConstraint());
+ }
+
+ public function testThrowsOnInvalidConstraintType(): void
+ {
+ $this->expectException(UnexpectedTypeException::class);
+ // Provide a valid Collection value so the type check reaches the constraint check
+ $this->validator->validate(new ArrayCollection(), new NotBlank());
+ }
+
+ public function testNoViolationWhenRequiredIdentifierPresentAndNotEmpty(): void
+ {
+ $identifier = new PersonIdentifier($this->requiredDefinition);
+ $identifier->setValue(['content' => 'ABC']);
+
+ $collection = new ArrayCollection([$identifier]);
+
+ $this->validator->validate($collection, new RequiredIdentifierConstraint());
+
+ $this->assertNoViolation();
+ }
+
+ public function testViolationWhenRequiredIdentifierMissing(): void
+ {
+ $collection = new ArrayCollection();
+
+ $this->validator->validate($collection, new RequiredIdentifierConstraint());
+
+ $this->buildViolation('person_identifier.This identifier must be set')
+ ->setParameter('{{ value }}', '')
+ ->setParameter('definition_id', '1')
+ ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05')
+ ->assertRaised();
+ }
+
+ public function testViolationWhenRequiredIdentifierIsEmpty(): void
+ {
+ $identifier = new PersonIdentifier($this->requiredDefinition);
+ $identifier->setValue(['content' => ' ']);
+
+ $collection = new ArrayCollection([$identifier]);
+
+ $this->validator->validate($collection, new RequiredIdentifierConstraint());
+
+ $this->buildViolation('person_identifier.This identifier must be set')
+ ->setParameter('{{ value }}', ' ')
+ ->setParameter('definition_id', '1')
+ ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05')
+ ->assertRaised();
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/UniqueIdentifierConstraintValidatorTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/UniqueIdentifierConstraintValidatorTest.php
new file mode 100644
index 000000000..d872bfc56
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/UniqueIdentifierConstraintValidatorTest.php
@@ -0,0 +1,158 @@
+repository = $this->prophesize(PersonIdentifierRepository::class);
+ $this->personRender = $this->prophesize(PersonRenderInterface::class);
+ parent::setUp();
+ }
+
+ protected function createValidator(): UniqueIdentifierConstraintValidator
+ {
+ return new UniqueIdentifierConstraintValidator($this->repository->reveal(), $this->personRender->reveal());
+ }
+
+ public function testThrowsOnInvalidConstraintType(): void
+ {
+ $this->expectException(UnexpectedTypeException::class);
+
+ // Provide a valid value so execution reaches the constraint type check
+ $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'string');
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setValue(['value' => 'ABC']);
+
+ $this->validator->validate($identifier, new NotBlank());
+ }
+
+ public function testThrowsOnInvalidValueType(): void
+ {
+ $this->expectException(UnexpectedValueException::class);
+ $this->validator->validate(new \stdClass(), new UniqueIdentifierConstraint());
+ }
+
+ public function testNoViolationWhenNoDuplicate(): void
+ {
+ $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'string');
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setValue(['value' => 'UNIQ']);
+
+ // Configure repository mock to return empty array
+ $this->repository->findByDefinitionAndCanonical($definition, ['value' => 'UNIQ'])->willReturn([]);
+
+ $this->validator->validate($identifier, new UniqueIdentifierConstraint());
+ $this->assertNoViolation();
+ }
+
+ public function testViolationWhenDuplicateFound(): void
+ {
+ $definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
+ $reflectionClass = new \ReflectionClass($definition);
+ $reflectionId = $reflectionClass->getProperty('id');
+ $reflectionId->setValue($definition, 1);
+
+ $personA = new Person();
+ $personA->setFirstName('Alice')->setLastName('Anderson');
+ $personB = new Person();
+ $personB->setFirstName('Bob')->setLastName('Brown');
+
+ $dup1 = new PersonIdentifier($definition);
+ $dup1->setPerson($personA);
+ $dup1->setValue(['value' => '123']);
+ $dup2 = new PersonIdentifier($definition);
+ $dup2->setPerson($personB);
+ $dup2->setValue(['value' => '123']);
+
+ // Repository returns duplicates
+ $this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1, $dup2]);
+
+ // Person renderer returns names
+ $this->personRender->renderString($personA, Argument::type('array'))->willReturn('Alice Anderson');
+ $this->personRender->renderString($personB, Argument::type('array'))->willReturn('Bob Brown');
+
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setPerson(new Person());
+ $identifier->setValue(['value' => '123']);
+
+ $constraint = new UniqueIdentifierConstraint();
+
+ $this->validator->validate($identifier, $constraint);
+
+ $this->buildViolation($constraint->message)
+ ->setParameter('{{ persons }}', 'Alice Anderson, Bob Brown')
+ ->setParameter('definition_id', '1')
+ ->assertRaised();
+ }
+
+ public function testViolationWhenDuplicateFoundButForSamePerson(): void
+ {
+ $definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
+ $reflectionClass = new \ReflectionClass($definition);
+ $reflectionId = $reflectionClass->getProperty('id');
+ $reflectionId->setValue($definition, 1);
+
+ $personA = new Person();
+ $personA->setFirstName('Alice')->setLastName('Anderson');
+
+ $dup1 = new PersonIdentifier($definition);
+ $dup1->setPerson($personA);
+ $dup1->setValue(['value' => '123']);
+
+ // Repository returns duplicates
+ $this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1]);
+
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setPerson($personA);
+ $identifier->setValue(['value' => '123']);
+
+ $constraint = new UniqueIdentifierConstraint();
+
+ $this->validator->validate($identifier, $constraint);
+
+ $this->assertNoViolation();
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/ValidIdentifierConstraintValidatorTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/ValidIdentifierConstraintValidatorTest.php
new file mode 100644
index 000000000..9e4abff3b
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/ValidIdentifierConstraintValidatorTest.php
@@ -0,0 +1,115 @@
+manager = $this->prophesize(PersonIdentifierManagerInterface::class);
+ parent::setUp();
+ }
+
+ protected function createValidator(): ValidIdentifierConstraintValidator
+ {
+ return new ValidIdentifierConstraintValidator($this->manager->reveal());
+ }
+
+ public function testAddsViolationFromWorker(): void
+ {
+ $definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
+ // set definition id via reflection for definition_id parameter
+ $ref = new \ReflectionClass($definition);
+ $prop = $ref->getProperty('id');
+ $prop->setValue($definition, 1);
+
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setValue(['value' => 'bad']);
+
+ $violation = new IdentifierViolationDTO('Invalid Identifier', '0000-1111-2222-3333', ['{{ foo }}' => 'bar']);
+
+ // engine that returns one violation
+ $engine = new class ([$violation]) implements PersonIdentifierEngineInterface {
+ public function __construct(private readonly array $violations) {}
+
+ public static function getName(): string
+ {
+ return 'dummy';
+ }
+
+ public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
+ {
+ return null;
+ }
+
+ public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
+
+ public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
+ {
+ return '';
+ }
+
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ return false;
+ }
+
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ return $this->violations;
+ }
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+ };
+ $worker = new PersonIdentifierWorker($engine, $definition);
+
+ $this->manager
+ ->buildWorkerByPersonIdentifierDefinition($definition)
+ ->willReturn($worker);
+
+ $constraint = new ValidIdentifierConstraint();
+ $this->validator->validate($identifier, $constraint);
+
+ $this->buildViolation('Invalid Identifier')
+ ->setParameters(['{{ foo }}' => 'bar'])
+ ->setParameter('{{ code }}', '0000-1111-2222-3333')
+ ->setParameter('definition_id', '1')
+ ->assertRaised();
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/Identifier/PersonIdentifierRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/Identifier/PersonIdentifierRepositoryTest.php
new file mode 100644
index 000000000..ab36fd872
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Repository/Identifier/PersonIdentifierRepositoryTest.php
@@ -0,0 +1,76 @@
+get(PersonIdentifierManagerInterface::class);
+
+ /** @var EntityManagerInterface $em */
+ $em = $container->get(EntityManagerInterface::class);
+
+ // Get a random existing person from fixtures
+ /** @var Person|null $person */
+ $person = $em->getRepository(Person::class)->findOneBy([]);
+ self::assertNotNull($person, 'An existing Person is required for this integration test.');
+
+ // Create a definition
+ $definition = new PersonIdentifierDefinition(['en' => 'Test Identifier'], StringIdentifier::NAME);
+ $em->persist($definition);
+ $em->flush();
+
+ // Create an identifier attached to the person
+ $value = ['content' => 'ABC-'.bin2hex(random_bytes(4))];
+ $identifier = new PersonIdentifier($definition);
+ $identifier->setPerson($person);
+ $identifier->setValue($value);
+ $identifier->setCanonical($personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($identifier->getValue()));
+ $em->persist($identifier);
+ $em->flush();
+
+ // Use the repository to find by definition and value
+ /** @var PersonIdentifierRepository $repo */
+ $repo = $container->get(PersonIdentifierRepository::class);
+ $results = $repo->findByDefinitionAndCanonical($definition, $value);
+
+ self::assertNotEmpty($results, 'Repository should return at least one result.');
+ self::assertContainsOnlyInstancesOf(PersonIdentifier::class, $results);
+ self::assertTrue(in_array($identifier->getId(), array_map(static fn (PersonIdentifier $pi) => $pi->getId(), $results), true));
+
+ // Cleanup
+ foreach ($results as $res) {
+ $em->remove($res);
+ }
+ $em->flush();
+ $em->remove($definition);
+ $em->flush();
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/PersonACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/PersonACLAwareRepositoryTest.php
index 667242111..20401c1b1 100644
--- a/src/Bundle/ChillPersonBundle/Tests/Repository/PersonACLAwareRepositoryTest.php
+++ b/src/Bundle/ChillPersonBundle/Tests/Repository/PersonACLAwareRepositoryTest.php
@@ -18,6 +18,7 @@ use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
+use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\Repository\PersonACLAwareRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
@@ -42,6 +43,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
private EntityManagerInterface $entityManager;
+ private PersonIdentifierManagerInterface $personIdentifierManager;
+
protected function setUp(): void
{
self::bootKernel();
@@ -49,6 +52,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
$this->countryRepository = self::getContainer()->get(CountryRepository::class);
$this->centerRepository = self::getContainer()->get(CenterRepositoryInterface::class);
+ $this->personIdentifierManager = self::getContainer()->get(PersonIdentifierManagerInterface::class);
+
}
public function testCountByCriteria()
@@ -66,7 +71,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
$security->reveal(),
$this->entityManager,
$this->countryRepository,
- $authorizationHelper->reveal()
+ $authorizationHelper->reveal(),
+ $this->personIdentifierManager,
);
$number = $repository->countBySearchCriteria('diallo');
@@ -89,7 +95,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
$security->reveal(),
$this->entityManager,
$this->countryRepository,
- $authorizationHelper->reveal()
+ $authorizationHelper->reveal(),
+ $this->personIdentifierManager,
);
$results = $repository->findBySearchCriteria(0, 5, false, 'diallo');
@@ -120,7 +127,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
$security->reveal(),
$this->entityManager,
$this->countryRepository,
- $authorizationHelper->reveal()
+ $authorizationHelper->reveal(),
+ $this->personIdentifierManager,
);
$actual = $repository->findByPhone($phoneNumber, 0, 10);
diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonDenormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonDenormalizerTest.php
new file mode 100644
index 000000000..aef5ee584
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonDenormalizerTest.php
@@ -0,0 +1,312 @@
+ 'Test'], 'dummy');
+ // Force the id for testing purposes
+ $r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
+ $r->setAccessible(true);
+ $r->setValue($definition, $personIdentifierDefinition);
+ } else {
+ $definition = $personIdentifierDefinition;
+ }
+
+ $engine = new class () implements PersonIdentifierEngineInterface {
+ public static function getName(): string
+ {
+ return 'dummy';
+ }
+
+ public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
+ {
+ // trivial canonicalization for tests
+ return isset($value['content']) ? (string) $value['content'] : null;
+ }
+
+ public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
+
+ public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
+ {
+ return '';
+ }
+
+ public function isEmpty(PersonIdentifier $identifier): bool
+ {
+ $value = $identifier->getValue();
+ $content = isset($value['content']) ? trim((string) $value['content']) : '';
+
+ return '' === $content;
+ }
+
+ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+
+ public function getDefaultValue(PersonIdentifierDefinition $definition): array
+ {
+ return [];
+ }
+ };
+
+ return new PersonIdentifierWorker($engine, $definition);
+ }
+ };
+ }
+
+ public function testSupportsDenormalizationReturnsTrueForValidData(): void
+ {
+ $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
+
+ $data = [
+ 'type' => 'person',
+ // important: new Person (creation) must not contain an id
+ ];
+
+ self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
+ }
+
+ public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
+ {
+ $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
+
+ // not an array
+ self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));
+
+ // missing type
+ self::assertFalse($denormalizer->supportsDenormalization([], Person::class));
+
+ // wrong type value
+ self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person'], Person::class));
+
+ // id present means it's not a create payload for this denormalizer
+ self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 123], Person::class));
+
+ // wrong target class
+ self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], \stdClass::class));
+ }
+
+ public function testDenormalizeMapsPayloadToPersonProperties(): void
+ {
+ $json = <<<'JSON'
+ {
+ "type": "person",
+ "firstName": "Jérome",
+ "lastName": "diallo",
+ "altNames": [
+ {
+ "key": "jeune_fille",
+ "value": "FJ"
+ }
+ ],
+ "birthdate": null,
+ "deathdate": null,
+ "phonenumber": "",
+ "mobilenumber": "",
+ "email": "",
+ "gender": {
+ "id": 5,
+ "type": "chill_main_gender"
+ },
+ "center": {
+ "id": 1,
+ "type": "center"
+ },
+ "civility": null,
+ "identifiers": [
+ {
+ "type": "person_identifier",
+ "value": {
+ "content": "789456"
+ },
+ "definition_id": 5
+ }
+ ]
+ }
+ JSON;
+ $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
+
+ $inner = new class () implements DenormalizerInterface {
+ public ?Gender $gender = null;
+ public ?Center $center = null;
+
+ public function denormalize($data, $type, $format = null, array $context = [])
+ {
+ if (PhoneNumber::class === $type) {
+ return '' === $data ? null : new PhoneNumber();
+ }
+ if (\DateTime::class === $type || \DateTimeImmutable::class === $type) {
+ return null === $data ? null : new \DateTimeImmutable((string) $data);
+ }
+ if (Gender::class === $type) {
+ return $this->gender ??= new Gender();
+ }
+ if (Center::class === $type) {
+ return $this->center ??= new Center();
+ }
+ if (Civility::class === $type) {
+ return null; // input is null in our payload
+ }
+
+ return null;
+ }
+
+ public function supportsDenormalization($data, $type, $format = null)
+ {
+ return true;
+ }
+ };
+
+ $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
+ $denormalizer->setDenormalizer($inner);
+
+ $person = $denormalizer->denormalize($data, Person::class);
+
+ self::assertInstanceOf(Person::class, $person);
+ self::assertSame('Jérome', $person->getFirstName());
+ self::assertSame('diallo', $person->getLastName());
+
+ // phone numbers: empty strings map to null via the inner denormalizer stub
+ self::assertNull($person->getPhonenumber());
+ self::assertNull($person->getMobilenumber());
+
+ // email passes through as is
+ self::assertSame('', $person->getEmail());
+
+ // nested objects are provided by our inner denormalizer and must be set back on the Person
+ self::assertSame($inner->gender, $person->getGender());
+ self::assertSame($inner->center, $person->getCenter());
+
+ // dates are null in the provided payload
+ self::assertNull($person->getBirthdate());
+ self::assertNull($person->getDeathdate());
+
+ // civility is null as provided
+ self::assertNull($person->getCivility());
+
+ // altNames: make sure the alt name with key jeune_fille has label FJ
+ $found = false;
+ foreach ($person->getAltNames() as $altName) {
+ if ('jeune_fille' === $altName->getKey()) {
+ $found = true;
+ self::assertSame('FJ', $altName->getLabel());
+ }
+ }
+ self::assertTrue($found, 'Expected altNames to contain key "jeune_fille"');
+
+ $found = false;
+ foreach ($person->getIdentifiers() as $identifier) {
+ if (5 === $identifier->getDefinition()->getId()) {
+ $found = true;
+ self::assertSame(['content' => '789456'], $identifier->getValue());
+ }
+ }
+ self::assertTrue($found, 'Expected identifiers with definition id 5');
+ }
+
+ public function testDenormalizeRemovesEmptyIdentifier(): void
+ {
+ $data = [
+ 'type' => 'person',
+ 'firstName' => 'Alice',
+ 'lastName' => 'Smith',
+ 'identifiers' => [
+ [
+ 'type' => 'person_identifier',
+ 'value' => ['content' => ''],
+ 'definition_id' => 7,
+ ],
+ ],
+ ];
+
+ $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
+
+ $person = $denormalizer->denormalize($data, Person::class);
+
+ // The identifier with empty content must be considered empty and removed
+ self::assertSame(0, $person->getIdentifiers()->count(), 'Expected no identifiers to remain on the person');
+ }
+
+ public function testDenormalizeRemovesPreviouslyExistingIdentifierWhenIncomingValueIsEmpty(): void
+ {
+ // Prepare an existing Person with a pre-existing identifier (definition id = 9)
+ $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
+ $ref = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
+ $ref->setValue($definition, 9);
+
+ $existingIdentifier = new PersonIdentifier($definition);
+ $existingIdentifier->setValue(['content' => 'ABC']);
+
+ $person = new Person();
+ $person->addIdentifier($existingIdentifier);
+
+ // Also set the identifier's own id = 9 so that the denormalizer logic matches it
+ // (the current denormalizer matches by PersonIdentifier->getId() === definition_id)
+ $refId = new \ReflectionProperty(PersonIdentifier::class, 'id');
+ $refId->setValue($existingIdentifier, 9);
+
+ // Incoming payload sets the same definition id with an empty value
+ $data = [
+ 'type' => 'person',
+ 'identifiers' => [
+ [
+ 'type' => 'person_identifier',
+ 'value' => ['content' => ''],
+ 'definition_id' => 9,
+ ],
+ ],
+ ];
+
+ $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
+
+ // Use AbstractNormalizer::OBJECT_TO_POPULATE to update the existing person
+ $result = $denormalizer->denormalize($data, Person::class, null, [
+ AbstractNormalizer::OBJECT_TO_POPULATE => $person,
+ ]);
+
+ self::assertSame($person, $result, 'Denormalizer should update and return the provided Person instance');
+ self::assertSame(0, $person->getIdentifiers()->count(), 'The previously existing identifier should be removed when incoming value is empty');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerIntegrationTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerIntegrationTest.php
new file mode 100644
index 000000000..3fff8aa97
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerIntegrationTest.php
@@ -0,0 +1,63 @@
+get(PersonRepository::class);
+ $person = $repo->findOneBy([]);
+
+ if (!$person instanceof Person) {
+ self::markTestSkipped('No person found in test database. Load fixtures to enable this test.');
+ }
+
+ /** @var SerializerInterface $serializer */
+ $serializer = $container->get(SerializerInterface::class);
+
+ // Should not throw
+ $data = $serializer->normalize($person, 'json');
+ Assert::assertIsArray($data);
+
+ // Spot check some expected keys exist
+ foreach ([
+ 'type', 'id', 'text', 'textAge', 'firstName', 'lastName', 'birthdate', 'age', 'gender', 'civility',
+ ] as $key) {
+ Assert::assertArrayHasKey($key, $data, sprintf('Expected key %s in normalized payload', $key));
+ }
+
+ // Minimal group should also work
+ $minimal = $serializer->normalize($person, 'json', ['groups' => 'minimal']);
+ Assert::assertIsArray($minimal);
+ foreach ([
+ 'type', 'id', 'text', 'textAge', 'firstName', 'lastName',
+ ] as $key) {
+ Assert::assertArrayHasKey($key, $minimal, sprintf('Expected key %s in minimal normalized payload', $key));
+ }
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php
index af78e2475..41aadf4fb 100644
--- a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php
+++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php
@@ -11,74 +11,186 @@ declare(strict_types=1);
namespace Serializer\Normalizer;
+use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Chill\PersonBundle\Entity\Person;
-use Chill\PersonBundle\Repository\PersonRepository;
+use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer;
+use Chill\PersonBundle\PersonIdentifier\Rendering\PersonIdRenderingInterface;
+use Doctrine\Common\Collections\ArrayCollection;
+use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
-use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
*
- * @coversNothing
+ * @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer
*/
-final class PersonJsonNormalizerTest extends KernelTestCase
+final class PersonJsonNormalizerTest extends TestCase
{
use ProphecyTrait;
- private PersonJsonNormalizer $normalizer;
-
- protected function setUp(): void
+ public function testSupportsNormalization(): void
{
- self::bootKernel();
+ $normalizer = $this->createNormalizer();
- $residentialAddressRepository = $this->prophesize(ResidentialAddressRepository::class);
- $residentialAddressRepository
- ->findCurrentResidentialAddressByPerson(Argument::type(Person::class), Argument::any())
- ->willReturn([]);
-
- $this->normalizer = $this->buildPersonJsonNormalizer(
- self::getContainer()->get(ChillEntityRenderExtension::class),
- self::getContainer()->get(PersonRepository::class),
- self::getContainer()->get(CenterResolverManagerInterface::class),
- $residentialAddressRepository->reveal(),
- self::getContainer()->get(PhoneNumberHelperInterface::class),
- self::getContainer()->get(NormalizerInterface::class)
- );
+ self::assertTrue($normalizer->supportsNormalization(new Person(), 'json'));
+ self::assertFalse($normalizer->supportsNormalization(new \stdClass(), 'json'));
+ self::assertFalse($normalizer->supportsNormalization(new Person(), 'xml'));
}
- public function testNormalization()
+ public function testNormalizeWithMinimalGroupReturnsOnlyBaseKeys(): void
{
- $person = new Person();
- $result = $this->normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => ['read']]);
+ $person = $this->createSamplePerson();
- $this->assertIsArray($result);
+ $normalizer = $this->createNormalizer();
+ $data = $normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => 'minimal']);
+
+ // Expected base keys
+ $expectedKeys = [
+ 'type',
+ 'id',
+ 'text',
+ 'textAge',
+ 'firstName',
+ 'lastName',
+ 'current_household_address',
+ 'birthdate',
+ 'deathdate',
+ 'age',
+ 'phonenumber',
+ 'mobilenumber',
+ 'email',
+ 'gender',
+ 'civility',
+ 'personId',
+ ];
+
+ foreach ($expectedKeys as $key) {
+ self::assertArrayHasKey($key, $data, sprintf('Key %s should be present', $key));
+ }
+ self::assertSame('PERSON-ID-RENDER', $data['personId']);
+
+ // Ensure extended keys are not present in minimal mode
+ foreach (['centers', 'altNames', 'current_household_id', 'current_residential_addresses'] as $key) {
+ self::assertArrayNotHasKey($key, $data, sprintf('Key %s should NOT be present in minimal group', $key));
+ }
}
- private function buildPersonJsonNormalizer(
- ChillEntityRenderExtension $render,
- PersonRepository $repository,
- CenterResolverManagerInterface $centerResolverManager,
- ResidentialAddressRepository $residentialAddressRepository,
- PhoneNumberHelperInterface $phoneNumberHelper,
- NormalizerInterface $normalizer,
- ): PersonJsonNormalizer {
- $personJsonNormalizer = new PersonJsonNormalizer(
- $render,
- $repository,
- $centerResolverManager,
- $residentialAddressRepository,
- $phoneNumberHelper
- );
- $personJsonNormalizer->setNormalizer($normalizer);
+ public function testNormalizeWithoutGroupsIncludesExtendedKeys(): void
+ {
+ $person = $this->createSamplePerson(withAltNames: true);
- return $personJsonNormalizer;
+ $center1 = (new Center())->setName('c1');
+ $center2 = (new Center())->setName('c2');
+
+
+ $normalizer = $this->createNormalizer(
+ centers: [$center1, $center2],
+ currentResidentialAddresses: [['addr' => 1]],
+ );
+
+ $data = $normalizer->normalize($person, 'json');
+
+ // Base keys
+ $baseKeys = [
+ 'type', 'id', 'text', 'textAge', 'firstName', 'lastName', 'current_household_address', 'birthdate', 'deathdate', 'age', 'phonenumber', 'mobilenumber', 'email', 'gender', 'civility', 'personId',
+ ];
+ foreach ($baseKeys as $key) {
+ self::assertArrayHasKey($key, $data, sprintf('Key %s should be present', $key));
+ }
+
+ // Extended keys
+ foreach (['centers', 'altNames', 'current_household_id', 'current_residential_addresses'] as $key) {
+ self::assertArrayHasKey($key, $data, sprintf('Key %s should be present', $key));
+ }
+
+ self::assertSame(['c1', 'c2'], $data['centers']);
+ self::assertIsArray($data['altNames']);
+ self::assertSame([['key' => 'aka', 'label' => 'Johnny']], $data['altNames']);
+ self::assertNull($data['current_household_id'], 'No household set so id should be null');
+ self::assertSame([['addr' => 1]], $data['current_residential_addresses']);
+ }
+
+ private function createNormalizer(array $centers = [], array $currentResidentialAddresses = []): PersonJsonNormalizer
+ {
+ $render = $this->prophesize(ChillEntityRenderExtension::class);
+ $render->renderString(Argument::type(Person::class), ['addAge' => false])->willReturn('John Doe');
+ $render->renderString(Argument::type(Person::class), ['addAge' => true])->willReturn('John Doe (25)');
+
+ $centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
+ $centerResolver->resolveCenters(Argument::type(Person::class))->willReturn($centers);
+
+ $raRepo = $this->prophesize(ResidentialAddressRepository::class);
+ $raRepo->findCurrentResidentialAddressByPerson(Argument::type(Person::class))->willReturn($currentResidentialAddresses);
+
+ $phoneHelper = $this->prophesize(PhoneNumberHelperInterface::class);
+
+ $personIdRendering = $this->prophesize(PersonIdRenderingInterface::class);
+ $personIdRendering->renderPersonId(Argument::type(Person::class))->willReturn('PERSON-ID-RENDER');
+
+ $normalizer = new PersonJsonNormalizer(
+ $render->reveal(),
+ $centerResolver->reveal(),
+ $raRepo->reveal(),
+ $phoneHelper->reveal(),
+ $personIdRendering->reveal(),
+ );
+
+ // Inner normalizer that echoes values or simple conversions
+ $inner = new class () implements NormalizerInterface {
+ public function supportsNormalization($data, $format = null): bool
+ {
+ return true;
+ }
+
+ public function normalize($object, $format = null, array $context = [])
+ {
+ // For scalars and arrays, return as-is; for objects, return string or id when possible
+ if (\is_scalar($object) || null === $object) {
+ return $object;
+ }
+ if ($object instanceof \DateTimeInterface) {
+ return $object->format('Y-m-d');
+ }
+ if ($object instanceof Center) {
+ return $object->getName();
+ }
+ if (is_array($object)) {
+ return array_map(fn ($o) => $this->normalize($o, $format, $context), $object);
+ }
+
+ // default stub
+ return (string) (method_exists($object, 'getId') ? $object->getId() : 'normalized');
+ }
+ };
+
+ $normalizer->setNormalizer($inner);
+
+ return $normalizer;
+ }
+
+ private function createSamplePerson(bool $withAltNames = false): Person
+ {
+ $p = new Person();
+ $p->setFirstName('John');
+ $p->setLastName('Doe');
+ $p->setBirthdate(new \DateTime('2000-01-01'));
+ $p->setEmail('john@example.test');
+
+ if ($withAltNames) {
+ $alt = new PersonAltName();
+ $alt->setKey('aka');
+ $alt->setLabel('Johnny');
+ $p->setAltNames(new ArrayCollection([$alt]));
+ }
+
+ return $p;
}
}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonReadDenormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonReadDenormalizerTest.php
new file mode 100644
index 000000000..e019a41c6
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonReadDenormalizerTest.php
@@ -0,0 +1,86 @@
+getMockBuilder(PersonRepository::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $denormalizer = new PersonJsonReadDenormalizer($repository);
+
+ $data = [
+ 'type' => 'person',
+ 'id' => 123,
+ ];
+
+ self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
+ }
+
+ public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
+ {
+ $repository = $this->getMockBuilder(PersonRepository::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $denormalizer = new PersonJsonReadDenormalizer($repository);
+
+ // not an array
+ self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));
+
+ // missing type
+ self::assertFalse($denormalizer->supportsDenormalization(['id' => 1], Person::class));
+
+ // wrong type value
+ self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person', 'id' => 1], Person::class));
+
+ // missing id
+ self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], Person::class));
+
+ // wrong target class
+ self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 1], \stdClass::class));
+ }
+
+ public function testDenormalizeReturnsPersonFromRepository(): void
+ {
+ $person = new Person();
+
+ $repository = $this->getMockBuilder(PersonRepository::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['find'])
+ ->getMock();
+
+ $repository->expects(self::once())
+ ->method('find')
+ ->with(123)
+ ->willReturn($person);
+
+ $denormalizer = new PersonJsonReadDenormalizer($repository);
+
+ $result = $denormalizer->denormalize(['id' => 123], Person::class);
+
+ self::assertSame($person, $result);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml
index c5f244525..1d1a2b1e2 100644
--- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml
+++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml
@@ -1,1998 +1,1998 @@
components:
- schemas:
- # should go to main
- Date:
- type: object
- properties:
- datetime:
- type: string
- format: date-time
- Scope:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - scope
- name:
- type: object
- additionalProperties:
- type: string
- example:
- fr: Social
- ScopeById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - scope
- required:
- - id
- - scope
+ schemas:
+ # should go to main
+ Date:
+ type: object
+ properties:
+ datetime:
+ type: string
+ format: date-time
+ Scope:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - scope
+ name:
+ type: object
+ additionalProperties:
+ type: string
+ example:
+ fr: Social
+ ScopeById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - scope
+ required:
+ - id
+ - scope
- # ok to stay here
- Person:
- type: object
- properties:
- id:
- type: integer
- readOnly: true
- type:
- type: string
- enum:
- - "person"
- firstName:
- type: string
- lastName:
- type: string
- text:
- type: string
- description: a canonical representation for the person name
- readOnly: true
- birthdate:
- $ref: "#/components/schemas/Date"
- deathdate:
- $ref: "#/components/schemas/Date"
- phonenumber:
- type: string
- mobilenumber:
- type: string
- gender:
- type: string
- enum:
- - man
- - woman
- - both
- gender_numeric:
- type: integer
- description: a numerical representation of gender
- readOnly: true
- PersonById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "person"
- required:
- - id
- - type
- # should go to third party
- ThirdParty:
- type: object
- properties:
- text:
- type: string
- ThirdPartyById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "thirdparty"
- required:
- - id
- - type
+ # ok to stay here
+ Person:
+ type: object
+ properties:
+ id:
+ type: integer
+ readOnly: true
+ type:
+ type: string
+ enum:
+ - "person"
+ firstName:
+ type: string
+ lastName:
+ type: string
+ text:
+ type: string
+ description: a canonical representation for the person name
+ readOnly: true
+ birthdate:
+ $ref: "#/components/schemas/Date"
+ deathdate:
+ $ref: "#/components/schemas/Date"
+ phonenumber:
+ type: string
+ mobilenumber:
+ type: string
+ gender:
+ type: string
+ enum:
+ - man
+ - woman
+ - both
+ gender_numeric:
+ type: integer
+ description: a numerical representation of gender
+ readOnly: true
+ PersonById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "person"
+ required:
+ - id
+ - type
+ # should go to third party
+ ThirdParty:
+ type: object
+ properties:
+ text:
+ type: string
+ ThirdPartyById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "thirdparty"
+ required:
+ - id
+ - type
- # ok to stay here
- AccompanyingPeriod:
- type: object
- properties:
- type:
- type: string
- enum:
- - accompanying_period
- id:
- type: integer
- requestorAnonymous:
- type: boolean
- Resource:
- type: object
- properties:
- type:
- type: string
- enum:
- - "accompanying_period_resource"
- readOnly: true
- id:
- type: integer
- readOnly: true
- resource:
- anyOf:
- - $ref: "#/components/schemas/PersonById"
- - $ref: "#/components/schemas/ThirdPartyById"
- ResourceById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "accompanying_period_resource"
- required:
- - id
- - type
- Comment:
- type: object
- properties:
- type:
- type: string
- enum:
- - "accompanying_period_comment"
- readOnly: true
- id:
- type: integer
- readOnly: true
- content:
- type: string
- CommentById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "accompanying_period_comment"
- required:
- - id
- - type
- SocialIssue:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "social_issue"
- parent_id:
- type: integer
- readOnly: true
- children_ids:
- type: array
- items:
- type: integer
- readOnly: true
- title:
- type: object
- additionalProperties:
- type: string
- example:
- fr: Accompagnement Social Adulte
- readOnly: true
- text:
- type: string
- readOnly: true
- Household:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "household"
- HouseholdPosition:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "household_position"
- AccompanyingCourseWork:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "accompanying_period_work"
- note:
- type: string
- privateComment:
- type: string
- startDate:
- $ref: "#/components/schemas/Date"
- endDate:
- $ref: "#/components/schemas/Date"
- handlingThirdParty:
- $ref: "#/components/schemas/ThirdPartyById"
- goals:
- type: array
- items:
- $ref: "#/components/schemas/AccompanyingCourseWorkGoal"
- results:
- type: array
- items:
- $ref: "#/components/schemas/SocialWorkResultById"
- AccompanyingCourseWorkGoal:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "accompanying_period_work_goal"
- note:
- type: string
- goal:
- $ref: "#/components/schemas/SocialWorkGoalById"
- results:
- type: array
- items:
- $ref: "#/components/schemas/SocialWorkGoalById"
+ # ok to stay here
+ AccompanyingPeriod:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - accompanying_period
+ id:
+ type: integer
+ requestorAnonymous:
+ type: boolean
+ Resource:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "accompanying_period_resource"
+ readOnly: true
+ id:
+ type: integer
+ readOnly: true
+ resource:
+ anyOf:
+ - $ref: "#/components/schemas/PersonById"
+ - $ref: "#/components/schemas/ThirdPartyById"
+ ResourceById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "accompanying_period_resource"
+ required:
+ - id
+ - type
+ Comment:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "accompanying_period_comment"
+ readOnly: true
+ id:
+ type: integer
+ readOnly: true
+ content:
+ type: string
+ CommentById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "accompanying_period_comment"
+ required:
+ - id
+ - type
+ SocialIssue:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "social_issue"
+ parent_id:
+ type: integer
+ readOnly: true
+ children_ids:
+ type: array
+ items:
+ type: integer
+ readOnly: true
+ title:
+ type: object
+ additionalProperties:
+ type: string
+ example:
+ fr: Accompagnement Social Adulte
+ readOnly: true
+ text:
+ type: string
+ readOnly: true
+ Household:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "household"
+ HouseholdPosition:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "household_position"
+ AccompanyingCourseWork:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "accompanying_period_work"
+ note:
+ type: string
+ privateComment:
+ type: string
+ startDate:
+ $ref: "#/components/schemas/Date"
+ endDate:
+ $ref: "#/components/schemas/Date"
+ handlingThirdParty:
+ $ref: "#/components/schemas/ThirdPartyById"
+ goals:
+ type: array
+ items:
+ $ref: "#/components/schemas/AccompanyingCourseWorkGoal"
+ results:
+ type: array
+ items:
+ $ref: "#/components/schemas/SocialWorkResultById"
+ AccompanyingCourseWorkGoal:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "accompanying_period_work_goal"
+ note:
+ type: string
+ goal:
+ $ref: "#/components/schemas/SocialWorkGoalById"
+ results:
+ type: array
+ items:
+ $ref: "#/components/schemas/SocialWorkGoalById"
- SocialWorkResultById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "social_work_result"
- SocialWorkGoalById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "social_work_goal"
+ SocialWorkResultById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "social_work_result"
+ SocialWorkGoalById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "social_work_goal"
- RelationById:
- type: object
- properties:
- id:
- type: integer
- type:
- type: string
- enum:
- - "relation"
- required:
- - id
- - type
- Relationship:
- type: object
- properties:
- type:
- type: string
- enum:
- - "relationship"
- id:
- type: integer
- readOnly: true
- fromPerson:
- anyOf:
- - $ref: "#/components/schemas/PersonById"
- toPerson:
- anyOf:
- - $ref: "#/components/schemas/PersonById"
- relation:
- anyOf:
- - $ref: "#/components/schemas/RelationById"
- reverse:
- type: boolean
+ RelationById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "relation"
+ required:
+ - id
+ - type
+ Relationship:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "relationship"
+ id:
+ type: integer
+ readOnly: true
+ fromPerson:
+ anyOf:
+ - $ref: "#/components/schemas/PersonById"
+ toPerson:
+ anyOf:
+ - $ref: "#/components/schemas/PersonById"
+ relation:
+ anyOf:
+ - $ref: "#/components/schemas/RelationById"
+ reverse:
+ type: boolean
paths:
- /1.0/person/person/{id}.json:
- get:
- tags:
- - person
- summary: Get a single person
- parameters:
- - name: id
- in: path
- required: true
- description: The person's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "OK"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Person"
- 403:
- description: "Unauthorized"
- patch:
- tags:
- - person
- summary: "Alter a person"
- parameters:
- - name: id
- in: path
- required: true
- description: The person's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A person"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Person"
- examples:
- Update a person:
- value:
- type: "person"
- firstName: "string"
- lastName: "string"
- birthdate:
- datetime: "2016-06-01T00:00:00+02:00"
- deathdate:
- datetime: "2021-06-01T00:00:00+02:00"
- phonenumber: "string"
- mobilenumber: "string"
- gender: "male"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Object with validation errors"
+ /1.0/person/person/{id}.json:
+ get:
+ tags:
+ - person
+ summary: Get a single person
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The person's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Person"
+ 403:
+ description: "Unauthorized"
+ patch:
+ tags:
+ - person
+ summary: "Alter a person"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The person's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A person"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Person"
+ examples:
+ Update a person:
+ value:
+ type: "person"
+ firstName: "string"
+ lastName: "string"
+ birthdate:
+ datetime: "2016-06-01T00:00:00+02:00"
+ deathdate:
+ datetime: "2021-06-01T00:00:00+02:00"
+ phonenumber: "string"
+ mobilenumber: "string"
+ gender: "male"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Object with validation errors"
- /1.0/person/person.json:
- post:
- tags:
- - person
- summary: Create a single person
- requestBody:
- description: "A person"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Person"
- examples:
- Create a new person:
- value:
- type: "person"
- firstName: "string"
- lastName: "string"
- birthdate:
- datetime: "2016-06-01T00:00:00+02:00"
- deathdate:
- datetime: "2021-06-01T00:00:00+02:00"
- phonenumber: "string"
- mobilenumber: "string"
- gender: "male"
- responses:
- 200:
- description: "OK"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Person"
- 403:
- description: "Unauthorized"
- 422:
- description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
+ /1.0/person/person.json:
+ post:
+ tags:
+ - person
+ summary: Create a single person
+ requestBody:
+ description: "A person"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Person"
+ examples:
+ Create a new person:
+ value:
+ type: "person"
+ firstName: "string"
+ lastName: "string"
+ birthdate:
+ datetime: "2016-06-01T00:00:00+02:00"
+ deathdate:
+ datetime: "2021-06-01T00:00:00+02:00"
+ phonenumber: "string"
+ mobilenumber: "string"
+ gender: "male"
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Person"
+ 403:
+ description: "Unauthorized"
+ 422:
+ description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
- /1.0/person/person/{id}/address.json:
- post:
- tags:
- - person
- summary: post an address to a person
- parameters:
- - name: id
- in: path
- required: true
- description: The person id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- id:
- type: integer
- description: The address id to attach to the person
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Unprocessable entity (validation errors)"
+ /1.0/person/person/{id}/address.json:
+ post:
+ tags:
+ - person
+ summary: post an address to a person
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The person id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: integer
+ description: The address id to attach to the person
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Unprocessable entity (validation errors)"
- /1.0/person/address/suggest/by-person/{id}.json:
- get:
- tags:
- - address
- summary: get a list of suggested address for a person
- description: >
- The address are computed from various source. Currently:
+ /1.0/person/address/suggest/by-person/{id}.json:
+ get:
+ tags:
+ - address
+ summary: get a list of suggested address for a person
+ description: >
+ The address are computed from various source. Currently:
- - the address of course to which the person is participating
+ - the address of course to which the person is participating
- The current person's address is always ignored.
- parameters:
- - name: id
- in: path
- required: true
- description: The person id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
+ The current person's address is always ignored.
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The person id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
- /1.0/person/address/suggest/by-household/{id}.json:
- get:
- tags:
- - address
- summary: get a list of suggested address for a household
- description: >
- The address are computed from various source. Currently:
+ /1.0/person/address/suggest/by-household/{id}.json:
+ get:
+ tags:
+ - address
+ summary: get a list of suggested address for a household
+ description: >
+ The address are computed from various source. Currently:
- - the address of course to which the members is participating
+ - the address of course to which the members is participating
- The current household address is always ignored.
- parameters:
- - name: id
- in: path
- required: true
- description: The household id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
+ The current household address is always ignored.
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The household id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
- /1.0/person/accompanying-course/{id}.json:
- get:
- tags:
- - accompanying-course
- summary: "Return the description for an accompanying course (accompanying period)"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- patch:
- tags:
- - person
- summary: "Alter an accompanying course (accompanying period)"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "An accompanying period"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/AccompanyingPeriod"
- examples:
- Set the requestor as anonymous:
- value:
- type: accompanying_period
- id: 12345
- requestorAnonymous: true
- Adding an initial comment:
- value:
- type: accompanying_period
- id: 2668,
- pinnedComment:
- type: accompanying_period_comment
- content: >
- This is my an initial comment.
+ /1.0/person/accompanying-course/{id}.json:
+ get:
+ tags:
+ - accompanying-course
+ summary: "Return the description for an accompanying course (accompanying period)"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ patch:
+ tags:
+ - person
+ summary: "Alter an accompanying course (accompanying period)"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "An accompanying period"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AccompanyingPeriod"
+ examples:
+ Set the requestor as anonymous:
+ value:
+ type: accompanying_period
+ id: 12345
+ requestorAnonymous: true
+ Adding an initial comment:
+ value:
+ type: accompanying_period
+ id: 2668,
+ pinnedComment:
+ type: accompanying_period_comment
+ content: >
+ This is my an initial comment.
- Say hello to the new "parcours"!
- Setting person with id 8405 as locator:
- value:
- type: accompanying_period
- id: 0
- personLocation:
- type: person
- id: 8405
- Removing person location for both person and address:
- value:
- type: accompanying_period
- id: 0
- personLocation: null
- addressLocation: null
- Adding address with id 7960 as temporarily address:
- value:
- type: accompanying_period
- id: 0
- personLocation: null
- addressLocation:
- id: 7960
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ Say hello to the new "parcours"!
+ Setting person with id 8405 as locator:
+ value:
+ type: accompanying_period
+ id: 0
+ personLocation:
+ type: person
+ id: 8405
+ Removing person location for both person and address:
+ value:
+ type: accompanying_period
+ id: 0
+ personLocation: null
+ addressLocation: null
+ Adding address with id 7960 as temporarily address:
+ value:
+ type: accompanying_period
+ id: 0
+ personLocation: null
+ addressLocation:
+ id: 7960
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/requestor.json:
- post:
- tags:
- - accompanying-course
- summary: "Add a requestor to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A person or thirdparty"
- required: true
- content:
- application/json:
- schema:
- oneOf:
- - $ref: "#/components/schemas/PersonById"
- - $ref: "#/components/schemas/ThirdPartyById"
- examples:
- add person with id 50:
- summary: "a person with id 50"
- value:
- type: person
- id: 50
- add thirdparty with id 100:
- summary: "a third party with id 100"
- value:
- type: thirdparty
- id: 100
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- delete:
- tags:
- - accompanying-course
- summary: "Remove the requestor for the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/requestor.json:
+ post:
+ tags:
+ - accompanying-course
+ summary: "Add a requestor to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A person or thirdparty"
+ required: true
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - $ref: "#/components/schemas/PersonById"
+ - $ref: "#/components/schemas/ThirdPartyById"
+ examples:
+ add person with id 50:
+ summary: "a person with id 50"
+ value:
+ type: person
+ id: 50
+ add thirdparty with id 100:
+ summary: "a third party with id 100"
+ value:
+ type: thirdparty
+ id: 100
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ delete:
+ tags:
+ - accompanying-course
+ summary: "Remove the requestor for the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/participation.json:
- post:
- tags:
- - accompanying-course
- summary: "Add a participant to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A person"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/PersonById"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- delete:
- tags:
- - accompanying-course
- summary: "Remove the participant for the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A person"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/PersonById"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/participation.json:
+ post:
+ tags:
+ - accompanying-course
+ summary: "Add a participant to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A person"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PersonById"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ delete:
+ tags:
+ - accompanying-course
+ summary: "Remove the participant for the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A person"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PersonById"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/resource.json:
- post:
- tags:
- - accompanying-course
- summary: "Add a resource to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A resource"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Resource"
- examples:
- add person with id 50:
- summary: "a person with id 50"
- value:
- type: accompanying_period_resource
- resource:
- type: person
- id: 50
- add thirdparty with id 100:
- summary: "a third party with id 100"
- value:
- type: accompanying_period_resource
- resource:
- type: thirdparty
- id: 100
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- delete:
- tags:
- - accompanying-course
- summary: "Remove the resource"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A resource"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/ResourceById"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/resource.json:
+ post:
+ tags:
+ - accompanying-course
+ summary: "Add a resource to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A resource"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Resource"
+ examples:
+ add person with id 50:
+ summary: "a person with id 50"
+ value:
+ type: accompanying_period_resource
+ resource:
+ type: person
+ id: 50
+ add thirdparty with id 100:
+ summary: "a third party with id 100"
+ value:
+ type: accompanying_period_resource
+ resource:
+ type: thirdparty
+ id: 100
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ delete:
+ tags:
+ - accompanying-course
+ summary: "Remove the resource"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A resource"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ResourceById"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/comment.json:
- post:
- tags:
- - accompanying-course
- summary: "Add a comment to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A comment"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Comment"
- examples:
- a single comment:
- summary: "a simple comment"
- value:
- type: accompanying_period_comment
- content: |
- This is a funny comment I would like to share with you.
+ /1.0/person/accompanying-course/{id}/comment.json:
+ post:
+ tags:
+ - accompanying-course
+ summary: "Add a comment to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A comment"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Comment"
+ examples:
+ a single comment:
+ summary: "a simple comment"
+ value:
+ type: accompanying_period_comment
+ content: |
+ This is a funny comment I would like to share with you.
- Thank you for reading this !
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- delete:
- tags:
- - accompanying-course
- summary: "Remove the comment"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A comment"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/CommentById"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ Thank you for reading this !
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ delete:
+ tags:
+ - accompanying-course
+ summary: "Remove the comment"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A comment"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CommentById"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/scope.json:
- post:
- tags:
- - accompanying-course
- summary: "Add a scope to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A comment"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Scope"
- examples:
- add a scope:
- value:
- type: scope
- id: 5
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- delete:
- tags:
- - accompanying-course
- summary: "Remove the scope"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A scope with his id"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/ScopeById"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/scope.json:
+ post:
+ tags:
+ - accompanying-course
+ summary: "Add a scope to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A comment"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Scope"
+ examples:
+ add a scope:
+ value:
+ type: scope
+ id: 5
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ delete:
+ tags:
+ - accompanying-course
+ summary: "Remove the scope"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A scope with his id"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ScopeById"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/socialissue.json:
- post:
- tags:
- - accompanying-course
- summary: "Add a social issue to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A social issue by id"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/SocialIssue"
- examples:
- add a social issue:
- value:
- type: social_issue
- id: 5
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- delete:
- tags:
- - accompanying-course
- summary: "Remove the social issue"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A social issue with his id"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/SocialIssue"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/referrers-suggested.json:
- get:
- tags:
- - accompanying-course
- summary: "get a list of available referral for a given accompanying cours"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
+ /1.0/person/accompanying-course/{id}/socialissue.json:
+ post:
+ tags:
+ - accompanying-course
+ summary: "Add a social issue to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A social issue by id"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SocialIssue"
+ examples:
+ add a social issue:
+ value:
+ type: social_issue
+ id: 5
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ delete:
+ tags:
+ - accompanying-course
+ summary: "Remove the social issue"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A social issue with his id"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SocialIssue"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/referrers-suggested.json:
+ get:
+ tags:
+ - accompanying-course
+ summary: "get a list of available referral for a given accompanying cours"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
- /1.0/person/accompanying-course/{id}/works.json:
- get:
- tags:
- - accompanying-course
- summary: List of accompanying period works for an accompanying period
- description: Gets a list of accompanying period works for an accompanying period
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
+ /1.0/person/accompanying-course/{id}/works.json:
+ get:
+ tags:
+ - accompanying-course
+ summary: List of accompanying period works for an accompanying period
+ description: Gets a list of accompanying period works for an accompanying period
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
- /1.0/person/accompanying-course/{id}/work.json:
- post:
- tags:
- - accompanying-course-work
- summary: "Add a work (AccompanyingPeriodwork) to the accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A new work"
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- type:
- type: string
- enum:
- - "accompanying_period_work"
- startDate:
- $ref: "#/components/schemas/Date"
- endDate:
- $ref: "#/components/schemas/Date"
- examples:
- create a work:
- value:
- type: accompanying_period_work
- social_action:
- id: 0
- type: social_work_social_action
- startDate:
- datetime: 2021-06-20T15:00:00+0200
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/work.json:
+ post:
+ tags:
+ - accompanying-course-work
+ summary: "Add a work (AccompanyingPeriodwork) to the accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A new work"
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "accompanying_period_work"
+ startDate:
+ $ref: "#/components/schemas/Date"
+ endDate:
+ $ref: "#/components/schemas/Date"
+ examples:
+ create a work:
+ value:
+ type: accompanying_period_work
+ social_action:
+ id: 0
+ type: social_work_social_action
+ startDate:
+ datetime: 2021-06-20T15:00:00+0200
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/work/{id}.json:
- get:
- tags:
- - accompanying-course-work
- summary: edit an existing accompanying course work
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying course social work's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/accompanying-course/work/{id}.json:
+ get:
+ tags:
+ - accompanying-course-work
+ summary: edit an existing accompanying course work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying course social work's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- put:
- tags:
- - accompanying-course-work
- summary: edit an existing accompanying course work
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying course social work's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/AccompanyingCourseWork"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Unprocessable entity (validation errors)"
- 400:
- description: "Bad Request"
+ put:
+ tags:
+ - accompanying-course-work
+ summary: edit an existing accompanying course work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying course social work's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AccompanyingCourseWork"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Unprocessable entity (validation errors)"
+ 400:
+ description: "Bad Request"
- /1.0/person/accompanying-course/{id}/confirm.json:
- post:
- tags:
- - person
- summary: confirm an accompanying course
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "transition cannot be applied"
+ /1.0/person/accompanying-course/{id}/confirm.json:
+ post:
+ tags:
+ - person
+ summary: confirm an accompanying course
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "transition cannot be applied"
- /1.0/person/accompanying-course/{id}/confidential.json:
- post:
- tags:
- - person
- summary: "Toggle confidentiality of accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "Confidentiality toggle"
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- type:
- type: string
- enum:
- - "accompanying_period"
- confidential:
- type: boolean
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/confidential.json:
+ post:
+ tags:
+ - person
+ summary: "Toggle confidentiality of accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "Confidentiality toggle"
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "accompanying_period"
+ confidential:
+ type: boolean
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/{id}/intensity.json:
- post:
- tags:
- - person
- summary: "Toggle intensity status of accompanying course"
- parameters:
- - name: id
- in: path
- required: true
- description: The accompanying period's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "Intensity toggle"
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- type:
- type: string
- enum:
- - "accompanying_period"
- intensity:
- type: string
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-course/{id}/intensity.json:
+ post:
+ tags:
+ - person
+ summary: "Toggle intensity status of accompanying course"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The accompanying period's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "Intensity toggle"
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "accompanying_period"
+ intensity:
+ type: string
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/accompanying-course/by-person/{person_id}.json:
- get:
- tags:
- - accompanying period
- summary: get a list of accompanying periods for a person
- description: Returns a list of the current accompanying periods for a person
- parameters:
- - name: person_id
- in: path
- required: true
- description: The person id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
+ /1.0/person/accompanying-course/by-person/{person_id}.json:
+ get:
+ tags:
+ - accompanying period
+ summary: get a list of accompanying periods for a person
+ description: Returns a list of the current accompanying periods for a person
+ parameters:
+ - name: person_id
+ in: path
+ required: true
+ description: The person id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
- /1.0/person/accompanying-period/origin.json:
- get:
- tags:
- - person
- summary: Return a list of all origins
- responses:
- 200:
- description: "ok"
+ /1.0/person/accompanying-period/origin.json:
+ get:
+ tags:
+ - person
+ summary: Return a list of all origins
+ responses:
+ 200:
+ description: "ok"
- /1.0/person/accompanying-period/origin/{id}.json:
- get:
- tags:
- - person
- summary: Return an origin by id
- parameters:
- - name: id
- in: path
- required: true
- description: The origin id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "ok"
- 400:
- description: "Bad Request"
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
+ /1.0/person/accompanying-period/origin/{id}.json:
+ get:
+ tags:
+ - person
+ summary: Return an origin by id
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The origin id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "ok"
+ 400:
+ description: "Bad Request"
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
- /1.0/person/accompanying-period/resource/{id}.json:
- patch:
- tags:
- - accompanying-course-resource
- summary: "Alter the resource"
- parameters:
- - name: id
- in: path
- required: true
- description: The resource's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A resource"
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- type:
- type: string
- enum:
- - "accompanying_period_resource"
- #id:
- # type: integer
- comment:
- type: string
- required:
- - type
- examples:
- Set the resource comment:
- value:
- type: accompanying_period_resource
- #id: 0
- comment: my judicious comment
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/person/accompanying-period/resource/{id}.json:
+ patch:
+ tags:
+ - accompanying-course-resource
+ summary: "Alter the resource"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The resource's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A resource"
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "accompanying_period_resource"
+ #id:
+ # type: integer
+ comment:
+ type: string
+ required:
+ - type
+ examples:
+ Set the resource comment:
+ value:
+ type: accompanying_period_resource
+ #id: 0
+ comment: my judicious comment
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/person/household.json:
- get:
- tags:
- - household
- summary: Return a list of all household
- responses:
- 200:
- description: "ok"
- post:
- tags:
- - household
- requestBody:
- description: "A household"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Household"
- summary: Post a new household
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Unprocessable entity (validation errors)"
- 400:
- description: "transition cannot be applied"
+ /1.0/person/household.json:
+ get:
+ tags:
+ - household
+ summary: Return a list of all household
+ responses:
+ 200:
+ description: "ok"
+ post:
+ tags:
+ - household
+ requestBody:
+ description: "A household"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Household"
+ summary: Post a new household
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Unprocessable entity (validation errors)"
+ 400:
+ description: "transition cannot be applied"
- /1.0/person/household/{id}.json:
- get:
- tags:
- - household
- summary: Return a household by id
- parameters:
- - name: id
- in: path
- required: true
- description: The household id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "ok"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Household"
- 404:
- description: "not found"
- 401:
- description: "Unauthorized"
+ /1.0/person/household/{id}.json:
+ get:
+ tags:
+ - household
+ summary: Return a household by id
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The household id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "ok"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Household"
+ 404:
+ description: "not found"
+ 401:
+ description: "Unauthorized"
- /1.0/person/household/by-address-reference/{address_id}.json:
- get:
- tags:
- - household
- summary: Return a list of household which are sharing the same address reference
- parameters:
- - name: address_id
- in: path
- required: true
- description: the address reference id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "ok"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Household"
- 404:
- description: "not found"
- 401:
- description: "Unauthorized"
+ /1.0/person/household/by-address-reference/{address_id}.json:
+ get:
+ tags:
+ - household
+ summary: Return a list of household which are sharing the same address reference
+ parameters:
+ - name: address_id
+ in: path
+ required: true
+ description: the address reference id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "ok"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Household"
+ 404:
+ description: "not found"
+ 401:
+ description: "Unauthorized"
- /1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json:
- get:
- tags:
- - household
- summary: Return households associated with the given person through accompanying periods
- description: |
- Return households associated with the given person throught accompanying periods participation.
+ /1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json:
+ get:
+ tags:
+ - household
+ summary: Return households associated with the given person through accompanying periods
+ description: |
+ Return households associated with the given person throught accompanying periods participation.
- The current household of the given person is excluded.
- parameters:
- - name: person_id
- in: path
- required: true
- description: The person's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "ok"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Household"
- 404:
- description: "not found"
- 401:
- description: "Unauthorized"
+ The current household of the given person is excluded.
+ parameters:
+ - name: person_id
+ in: path
+ required: true
+ description: The person's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "ok"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Household"
+ 404:
+ description: "not found"
+ 401:
+ description: "Unauthorized"
- /1.0/person/household/members/move.json:
- post:
- tags:
- - household
- summary: move one or multiple person from a household to another
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- concerned:
- type: array
- items:
- type: object
- properties:
- person:
- $ref: "#/components/schemas/PersonById"
- start_date:
- $ref: "#/components/schemas/Date"
- position:
- $ref: "#/components/schemas/HouseholdPosition"
- holder:
- type: boolean
- comment:
- type: string
- destination:
- $ref: "#/components/schemas/Household"
- examples:
- Moving person to a new household:
- value:
- concerned:
- - person:
- id: 0
- type: person
- position:
- type: household_position
- id: 1
- start_date:
- datetime: "2021-06-01T00:00:00+02:00"
- comment: "This is my comment for moving"
- holder: false
- destination:
- type: household
- Moving person to a new household and set an address to this household:
- value:
- concerned:
- - person:
- id: 0
- type: person
- position:
- type: household_position
- id: 1
- start_date:
- datetime: "2021-06-01T00:00:00+02:00"
- comment: "This is my comment for moving"
- holder: false
- destination:
- type: household
- forceAddress:
- id: 0
- Moving person to an existing household:
- value:
- concerned:
- - person:
- id: 0
- type: person
- position:
- type: household_position
- id: 1
- start_date:
- datetime: 2021-06-01T00:00:00+02:00
- comment: "This is my comment for moving"
- holder: false
- destination:
- type: household
- id: 54
- Removing a person from any household:
- value:
- concerned:
- - person:
- id: 0
- type: person
- start_date:
- datetime: 2021-06-01T00:00:00+02:00
- destination: null
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Unprocessable entity (validation errors)"
- 400:
- description: "transition cannot be applied"
+ /1.0/person/household/members/move.json:
+ post:
+ tags:
+ - household
+ summary: move one or multiple person from a household to another
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ concerned:
+ type: array
+ items:
+ type: object
+ properties:
+ person:
+ $ref: "#/components/schemas/PersonById"
+ start_date:
+ $ref: "#/components/schemas/Date"
+ position:
+ $ref: "#/components/schemas/HouseholdPosition"
+ holder:
+ type: boolean
+ comment:
+ type: string
+ destination:
+ $ref: "#/components/schemas/Household"
+ examples:
+ Moving person to a new household:
+ value:
+ concerned:
+ - person:
+ id: 0
+ type: person
+ position:
+ type: household_position
+ id: 1
+ start_date:
+ datetime: "2021-06-01T00:00:00+02:00"
+ comment: "This is my comment for moving"
+ holder: false
+ destination:
+ type: household
+ Moving person to a new household and set an address to this household:
+ value:
+ concerned:
+ - person:
+ id: 0
+ type: person
+ position:
+ type: household_position
+ id: 1
+ start_date:
+ datetime: "2021-06-01T00:00:00+02:00"
+ comment: "This is my comment for moving"
+ holder: false
+ destination:
+ type: household
+ forceAddress:
+ id: 0
+ Moving person to an existing household:
+ value:
+ concerned:
+ - person:
+ id: 0
+ type: person
+ position:
+ type: household_position
+ id: 1
+ start_date:
+ datetime: 2021-06-01T00:00:00+02:00
+ comment: "This is my comment for moving"
+ holder: false
+ destination:
+ type: household
+ id: 54
+ Removing a person from any household:
+ value:
+ concerned:
+ - person:
+ id: 0
+ type: person
+ start_date:
+ datetime: 2021-06-01T00:00:00+02:00
+ destination: null
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Unprocessable entity (validation errors)"
+ 400:
+ description: "transition cannot be applied"
- /1.0/person/household/{id}/address.json:
- post:
- tags:
- - household
- summary: post an address to a household
- parameters:
- - name: id
- in: path
- required: true
- description: The household id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- id:
- type: integer
- description: The address id to attach to the household
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Unprocessable entity (validation errors)"
- 400:
- description: "transition cannot be applied"
+ /1.0/person/household/{id}/address.json:
+ post:
+ tags:
+ - household
+ summary: post an address to a household
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The household id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: integer
+ description: The address id to attach to the household
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Unprocessable entity (validation errors)"
+ 400:
+ description: "transition cannot be applied"
- /1.0/person/social/social-action.json:
- get:
- tags:
- - social-work-social-action
- summary: get a list of social action
- responses:
- 401:
- description: "Unauthorized"
- 200:
- description: "OK"
+ /1.0/person/social/social-action.json:
+ get:
+ tags:
+ - social-work-social-action
+ summary: get a list of social action
+ responses:
+ 401:
+ description: "Unauthorized"
+ 200:
+ description: "OK"
- /1.0/person/social/social-action/{id}.json:
- get:
- tags:
- - social-work-social-action
- parameters:
- - name: id
- in: path
- required: true
- description: The social action's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social/social-action/{id}.json:
+ get:
+ tags:
+ - social-work-social-action
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The social action's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/person/social/social-action/by-social-issue/{id}.json:
- get:
- tags:
- - social-work-social-action
- parameters:
- - name: id
- in: path
- required: true
- description: The social action's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social/social-action/by-social-issue/{id}.json:
+ get:
+ tags:
+ - social-work-social-action
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The social action's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/person/social-work/evaluation/by-social-action/{social_action_id}.json:
- get:
- tags:
- - social-work-evaluation
- summary: return a list of evaluation which are available for a given social action
- parameters:
- - name: social_action_id
- in: path
- required: true
- description: The social action's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: ok
- 404:
- description: not found
+ /1.0/person/social-work/evaluation/by-social-action/{social_action_id}.json:
+ get:
+ tags:
+ - social-work-evaluation
+ summary: return a list of evaluation which are available for a given social action
+ parameters:
+ - name: social_action_id
+ in: path
+ required: true
+ description: The social action's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: ok
+ 404:
+ description: not found
- /1.0/person/social-work/social-issue.json:
- get:
- tags:
- - social-issue
- summary: Return a list of social work
- responses:
- 200:
- description: "ok"
+ /1.0/person/social-work/social-issue.json:
+ get:
+ tags:
+ - social-issue
+ summary: Return a list of social work
+ responses:
+ 200:
+ description: "ok"
- /1.0/person/social-work/social-issue/{id}.json:
- get:
- tags:
- - social-issue
- summary: Return a social issue by id
- parameters:
- - name: id
- in: path
- required: true
- description: The social issue's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "ok"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/SocialIssue"
- 404:
- description: "not found"
- 401:
- description: "Unauthorized"
+ /1.0/person/social-work/social-issue/{id}.json:
+ get:
+ tags:
+ - social-issue
+ summary: Return a social issue by id
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The social issue's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "ok"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SocialIssue"
+ 404:
+ description: "not found"
+ 401:
+ description: "Unauthorized"
- /1.0/person/social-work/result.json:
- get:
- tags:
- - accompanying-course-work
- summary: get a list of social work result
- responses:
- 401:
- description: "Unauthorized"
- 200:
- description: "OK"
+ /1.0/person/social-work/result.json:
+ get:
+ tags:
+ - accompanying-course-work
+ summary: get a list of social work result
+ responses:
+ 401:
+ description: "Unauthorized"
+ 200:
+ description: "OK"
- /1.0/person/social-work/result/{id}.json:
- get:
- tags:
- - accompanying-course-work
- parameters:
- - name: id
- in: path
- required: true
- description: The result's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social-work/result/{id}.json:
+ get:
+ tags:
+ - accompanying-course-work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The result's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/person/social-work/result/by-goal/{id}.json:
- get:
- tags:
- - accompanying-course-work
- parameters:
- - name: id
- in: path
- required: true
- description: The goal's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social-work/result/by-goal/{id}.json:
+ get:
+ tags:
+ - accompanying-course-work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The goal's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/person/social-work/result/by-social-action/{id}.json:
- get:
- tags:
- - accompanying-course-work
- parameters:
- - name: id
- in: path
- required: true
- description: The social action's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social-work/result/by-social-action/{id}.json:
+ get:
+ tags:
+ - accompanying-course-work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The social action's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/person/social-work/goal.json:
- get:
- tags:
- - accompanying-course-work
- summary: get a list of social work goal
- responses:
- 401:
- description: "Unauthorized"
- 200:
- description: "OK"
+ /1.0/person/social-work/goal.json:
+ get:
+ tags:
+ - accompanying-course-work
+ summary: get a list of social work goal
+ responses:
+ 401:
+ description: "Unauthorized"
+ 200:
+ description: "OK"
- /1.0/person/social-work/goal/{id}.json:
- get:
- tags:
- - accompanying-course-work
- parameters:
- - name: id
- in: path
- required: true
- description: The goal's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social-work/goal/{id}.json:
+ get:
+ tags:
+ - accompanying-course-work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The goal's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/person/social-work/goal/by-social-action/{id}.json:
- get:
- tags:
- - accompanying-course-work
- parameters:
- - name: id
- in: path
- required: true
- description: The social action's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/person/social-work/goal/by-social-action/{id}.json:
+ get:
+ tags:
+ - accompanying-course-work
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The social action's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/relations/relationship/by-person/{id}.json:
- get:
- tags:
- - relationships
- parameters:
- - name: id
- in: path
- required: true
- description: The person's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 400:
- description: "Bad Request"
+ /1.0/relations/relationship/by-person/{id}.json:
+ get:
+ tags:
+ - relationships
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The person's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
- /1.0/relations/relationship.json:
- post:
- tags:
- - relationships
- summary: Create a new relationship
- requestBody:
- description: "A relationship"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Relationship"
- responses:
- 200:
- description: "OK"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Relationship"
- 403:
- description: "Unauthorized"
- 422:
- description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
+ /1.0/relations/relationship.json:
+ post:
+ tags:
+ - relationships
+ summary: Create a new relationship
+ requestBody:
+ description: "A relationship"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Relationship"
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Relationship"
+ 403:
+ description: "Unauthorized"
+ 422:
+ description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
- /1.0/relations/relationship/{id}.json:
- patch:
- tags:
- - relationships
- summary: "Alter a relationship"
- parameters:
- - name: id
- in: path
- required: true
- description: The relationship's id
- schema:
- type: integer
- format: integer
- minimum: 1
- requestBody:
- description: "A relationship"
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Relationship"
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "Object with validation errors"
- delete:
- tags:
- - relationships
- summary: "Remove the relationship"
- parameters:
- - name: id
- in: path
- required: true
- description: The relationship's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 401:
- description: "Unauthorized"
- 404:
- description: "Not found"
- 200:
- description: "OK"
- 422:
- description: "object with validation errors"
+ /1.0/relations/relationship/{id}.json:
+ patch:
+ tags:
+ - relationships
+ summary: "Alter a relationship"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The relationship's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A relationship"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Relationship"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Object with validation errors"
+ delete:
+ tags:
+ - relationships
+ summary: "Remove the relationship"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The relationship's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
- /1.0/relations/relation.json:
- get:
- tags:
- - relations
- summary: get a list of relations
- responses:
- 401:
- description: "Unauthorized"
- 200:
- description: "OK"
+ /1.0/relations/relation.json:
+ get:
+ tags:
+ - relations
+ summary: get a list of relations
+ responses:
+ 401:
+ description: "Unauthorized"
+ 200:
+ description: "OK"
- /1.0/person/config/alt_names.json:
- get:
- tags:
- - person
- summary: Return a list of possible altNames that are defined in the config
- responses:
- 200:
- description: "OK"
+ /1.0/person/config/alt_names.json:
+ get:
+ tags:
+ - person
+ summary: Return a list of possible altNames that are defined in the config
+ responses:
+ 200:
+ description: "OK"
- /1.0/person/creation/authorized-centers:
- get:
- tags:
- - person
- - permissions
- summary: Return a list of possible centers for person creation
- responses:
- 200:
- description: "OK"
+ /1.0/person/creation/authorized-centers:
+ get:
+ tags:
+ - person
+ - permissions
+ summary: Return a list of possible centers for person creation
+ responses:
+ 200:
+ description: "OK"
- /1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate:
- post:
- tags:
- - accompanying-course-work-evaluation-document
- summary: Dupliate an an accompanying period work evaluation document
- parameters:
- - in: path
- name: id
- required: true
- description: The document's id
- schema:
- type: integer
- format: integer
- minimum: 1
- responses:
- 200:
- description: "OK"
- content:
- application/json:
- schema:
- type: object
+ /1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate:
+ post:
+ tags:
+ - accompanying-course-work-evaluation-document
+ summary: Dupliate an an accompanying period work evaluation document
+ parameters:
+ - in: path
+ name: id
+ required: true
+ description: The document's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ type: object
/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate:
post:
@@ -2023,3 +2023,16 @@ paths:
application/json:
schema:
type: object
+
+ /1.0/person/identifiers/workers:
+ get:
+ tags:
+ - person
+ summary: List the person identifiers
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ type: object
diff --git a/src/Bundle/ChillPersonBundle/config/services.yaml b/src/Bundle/ChillPersonBundle/config/services.yaml
index 1b57721ff..5d258f029 100644
--- a/src/Bundle/ChillPersonBundle/config/services.yaml
+++ b/src/Bundle/ChillPersonBundle/config/services.yaml
@@ -108,3 +108,9 @@ services:
Chill\PersonBundle\PersonIdentifier\Rendering\:
resource: '../PersonIdentifier/Rendering'
+
+ Chill\PersonBundle\PersonIdentifier\Normalizer\:
+ resource: '../PersonIdentifier/Normalizer'
+
+ Chill\PersonBundle\PersonIdentifier\Validator\:
+ resource: '../PersonIdentifier/Validator'
diff --git a/src/Bundle/ChillPersonBundle/config/services/actions.yaml b/src/Bundle/ChillPersonBundle/config/services/actions.yaml
index d6e2c80a5..220a7483e 100644
--- a/src/Bundle/ChillPersonBundle/config/services/actions.yaml
+++ b/src/Bundle/ChillPersonBundle/config/services/actions.yaml
@@ -11,3 +11,9 @@ services:
Chill\PersonBundle\Actions\Remove\Handler\:
resource: '../../Actions/Remove/Handler'
+
+ Chill\PersonBundle\Actions\PersonEdit\Service\:
+ resource: '../../Actions/PersonEdit/Service'
+
+ Chill\PersonBundle\Actions\PersonCreate\Service\:
+ resource: '../../Actions/PersonCreate/Service'
diff --git a/src/Bundle/ChillPersonBundle/config/services/serializer.yaml b/src/Bundle/ChillPersonBundle/config/services/serializer.yaml
deleted file mode 100644
index 5a1e54400..000000000
--- a/src/Bundle/ChillPersonBundle/config/services/serializer.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
----
-services:
- # note: normalizers are loaded from ../services.yaml
-
- Chill\PersonBundle\Serializer\Normalizer\:
- autowire: true
- autoconfigure: true
- resource: '../../Serializer/Normalizer'
- tags:
- - { name: 'serializer.normalizer', priority: 64 }
-
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20250918095044.php b/src/Bundle/ChillPersonBundle/migrations/Version20250918095044.php
new file mode 100644
index 000000000..9ec97e337
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20250918095044.php
@@ -0,0 +1,37 @@
+addSql('ALTER TABLE chill_person_identifier_definition ADD presence VARCHAR(255) DEFAULT \'ON_EDIT\' NOT NULL');
+ $this->addSql('UPDATE chill_person_identifier_definition SET presence = \'NOT_EDITABLE\' WHERE is_editable_by_users IS FALSE');
+ $this->addSql('ALTER TABLE chill_person_identifier_definition DROP is_editable_by_users');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE chill_person_identifier_definition ADD is_editable_by_users BOOLEAN DEFAULT false NOT NULL');
+ $this->addSql('UPDATE chill_person_identifier_definition SET is_editable_by_users = true WHERE presence <> \'NOT_EDITABLE\' ');
+ $this->addSql('ALTER TABLE chill_person_identifier_definition DROP presence');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20250922151020.php b/src/Bundle/ChillPersonBundle/migrations/Version20250922151020.php
new file mode 100644
index 000000000..709ecbf54
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20250922151020.php
@@ -0,0 +1,33 @@
+addSql('CREATE UNIQUE INDEX chill_person_identifier_unique ON chill_person_identifier (definition_id, canonical)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP INDEX chill_person_identifier_unique');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20250924101621.php b/src/Bundle/ChillPersonBundle/migrations/Version20250924101621.php
new file mode 100644
index 000000000..e6f9fff54
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20250924101621.php
@@ -0,0 +1,53 @@
+addSql(<<<'SQL'
+ ALTER TABLE chill_person_identifier DROP CONSTRAINT fk_bca5a36bd11ea911
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE chill_person_identifier ADD CONSTRAINT fk_bca5a36bd11ea911
+ FOREIGN KEY (definition_id) REFERENCES chill_person_identifier_definition (id)
+ on delete restrict
+ SQL);
+
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql(<<<'SQL'
+ ALTER TABLE chill_person_identifier DROP CONSTRAINT fk_bca5a36bd11ea911
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE chill_person_identifier ADD CONSTRAINT fk_bca5a36bd11ea911
+ FOREIGN KEY (definition_id) REFERENCES chill_person_identifier_definition (id)
+ on delete cascade
+ SQL);
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20250926124024.php b/src/Bundle/ChillPersonBundle/migrations/Version20250926124024.php
new file mode 100644
index 000000000..71630d0ad
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20250926124024.php
@@ -0,0 +1,33 @@
+addSql('CREATE UNIQUE INDEX chill_person_identifier_unique_person_definition ON chill_person_identifier (definition_id, person_id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP INDEX chill_person_identifier_unique_person_definition');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml
index c5e8e097c..795d2ddf9 100644
--- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml
+++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml
@@ -265,3 +265,38 @@ add_persons:
title: "Centre"
error_only_one_person: "Une seule personne peut être sélectionnée !"
+
+renderbox:
+ person: "Usager"
+ birthday_statement: >-
+ {gender, select,
+ man {Né le {birthdate, date}}
+ woman {Née le {birthdate, date}}
+ other {Né·e le {birthdate, date}}
+ }
+ deathdate_statement: >-
+ {gender, select,
+ man {Décédé le {deathdate, date}}
+ woman {Décédée le {deathdate, date}}
+ other {Décédé·e le {deathdate, date}}
+ }
+ household_without_address: "Le ménage de l'usager est sans adresse"
+ no_data: "Aucune information renseignée"
+ type:
+ thirdparty: "Tiers"
+ person: "Usager"
+ holder: "Titulaire"
+ years_old: >-
+ {n, plural,
+ =0 {0 an}
+ one {1 an}
+ other {# ans}
+ }
+ residential_address: "Adresse de résidence"
+ located_at: "réside chez"
+ household_number: "Ménage n°{number}"
+ current_members: "Membres actuels"
+ no_current_address: "Sans adresse actuellement"
+ new_household: "Nouveau ménage"
+ no_members_yet: "Aucun membre actuellement"
+
diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
index 917d9e2cf..2159f32db 100644
--- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
@@ -105,6 +105,8 @@ Administrative status: Situation administrative
person:
Identifiers: Identifiants
+person_edit:
+ Error while saving: Erreur lors de l'enregistrement
# dédoublonnage
Old person: Doublon
@@ -885,6 +887,12 @@ accompanying_course:
administrative_location: Localisation administrative
comment is pinned: Le commentaire est épinglé
comment is unpinned: Le commentaire est désépinglé
+ requestor:
+ add: Ajouter un demandeur
+ persons_associated:
+ add_person: Ajouter des usagers
+ resources:
+ add_resources: Ajouter des interlocuteurs
show: Montrer
hide: Masquer
@@ -1583,7 +1591,7 @@ person_messages:
center_id: "Identifiant du centre"
center_type: "Type de centre"
center_name: "Territoire"
- phonenumber: "Téléphone"
+ phonenumber: "Téléphone fixe"
mobilenumber: "Mobile"
altnames: "Autres noms"
email: "Courriel"
diff --git a/src/Bundle/ChillPersonBundle/translations/validators+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/validators+intl-icu.fr.yaml
new file mode 100644
index 000000000..3b65462e1
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/translations/validators+intl-icu.fr.yaml
@@ -0,0 +1,7 @@
+person_identifier:
+ fixed_length: >-
+ {limit, plural,
+ =1 {L'identifier doit contenir exactement 1 caractère}
+ other {L'identifiant doit contenir exactement # caractères}
+ }
+ only_number: "L'identifiant ne doit contenir que des chiffres"
diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
index c6fe0f912..02ad95de7 100644
--- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
+++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
@@ -73,5 +73,9 @@ relationship:
person_creation:
If you want to create an household, an address is required: Pour la création d'un ménage, une adresse est requise
+person_identifier:
+ This identifier must be set: Cet identifiant doit être présent.
+ Identifier must be unique. The same identifier already exists for {{ persons }}: Identifiant déjà utilisé pour {{ persons }}
+
accompanying_course_work:
The endDate should be greater or equal than the start date: La date de fin doit être égale ou supérieure à la date de début
diff --git a/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php b/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php
index e65a06515..480e67cfc 100644
--- a/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php
+++ b/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php
@@ -17,7 +17,6 @@ use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\User;
-use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
@@ -70,6 +69,11 @@ use Symfony\Component\Validator\Constraints as Assert;
*
* The difference between categories and types is transparent for user: they choose the same fields into the UI, without
* noticing a difference.
+ *
+ * ## Validation
+ *
+ * When a validation is inserted / updated, do not forget to update the related ThirdPartyEdit.vue component and the associated
+ * list of possible violations.
*/
#[ORM\Entity]
#[ORM\Table(name: 'chill_3party.third_party')]
@@ -206,12 +210,12 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'telephone', type: 'phone_number', nullable: true)]
- #[PhonenumberConstraint(type: 'any')]
+ #[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
private ?PhoneNumber $telephone = null;
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'telephone2', type: 'phone_number', nullable: true)]
- #[PhonenumberConstraint(type: 'any')]
+ #[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
private ?PhoneNumber $telephone2 = null;
#[ORM\Column(name: 'types', type: \Doctrine\DBAL\Types\Types::JSON, nullable: true)]
diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/types.ts b/src/Bundle/ChillThirdPartyBundle/Resources/public/types.ts
index 2d2acc42b..a17797e6a 100644
--- a/src/Bundle/ChillThirdPartyBundle/Resources/public/types.ts
+++ b/src/Bundle/ChillThirdPartyBundle/Resources/public/types.ts
@@ -2,39 +2,123 @@ import {
Address,
Center,
Civility,
- DateTime,
+ DateTime, SetAddress, SetCivility,
User,
} from "ChillMainAssets/types";
-export interface Thirdparty {
+export type ThirdPartyKind = "contact" | "child" | "company";
+
+export interface BaseThirdParty {
type: "thirdparty";
+ kind: ""|ThirdPartyKind;
text: string;
acronym: string | null;
active: boolean;
address: Address | null;
- canonicalized: string | null;
- categories: ThirdpartyCategory[];
- centers: Center[];
- children: Thirdparty[];
- civility: Civility | null;
- comment: string | null;
contactDataAnonymous: boolean;
createdAt: DateTime;
createdBy: User | null;
email: string | null;
firstname: string | null;
- id: number | null;
- kind: string;
- name: string;
+ id: number;
nameCompany: string | null;
- parent: Thirdparty | null;
- profession: string;
telephone: string | null;
- thirdPartyTypes: ThirdpartyType[] | null;
+ telephone2: string | null;
updatedAt: DateTime | null;
updatedBy: User | null;
}
+function isBaseThirdParty(t: unknown): t is BaseThirdParty {
+ if (typeof t !== "object" || t === null) return false;
+ const o = t as Partial;
+ return (
+ (o as any).type === "thirdparty" &&
+ typeof o.id === "number" &&
+ typeof o.text === "string" &&
+ (o.kind === "" || o.kind === "contact" || o.kind === "child" || o.kind === "company") &&
+ typeof o.active === "boolean"
+ );
+}
+
+export interface ThirdpartyCompany extends BaseThirdParty {
+ kind: "company";
+ text: string;
+ acronym: string | null;
+ children: Thirdparty[];
+ category: ThirdpartyCategory[];
+ thirdPartyTypes: ThirdpartyType[] | null;
+ address: Address | null;
+}
+
+// Type guard to distinguish a ThirdpartyCompany
+export function isThirdpartyCompany(
+ t: BaseThirdParty
+): t is ThirdpartyCompany {
+ return (
+ t.type === "thirdparty" &&
+ t.kind === "company"
+ );
+}
+
+export interface ThirdpartyChild extends BaseThirdParty {
+ kind: "child";
+ civility: Civility | null;
+ contactDataAnonymous: boolean;
+ parent: ThirdpartyCompany;
+ profession: string;
+ firstname: string;
+ /**
+ * the lastname for "Contact" and "Child", the name
+ */
+ name: string;
+ comment: string | null;
+}
+
+// Type guard to distinguish a ThirdpartyChild
+export function isThirdpartyChild(
+ t: BaseThirdParty
+): t is ThirdpartyChild {
+ return (
+ t.type === "thirdparty" &&
+ t.kind === "child"
+ );
+}
+
+export interface ThirdpartyContact extends BaseThirdParty {
+ kind: "contact";
+ civility: Civility | null;
+ category: ThirdpartyCategory[];
+ thirdPartyTypes: ThirdpartyType[] | null;
+ profession: string;
+ firstname: string;
+ /**
+ * the lastname for "Contact" and "Child", the name
+ */
+ name: string;
+ address: Address | null;
+}
+
+// Type guard to distinguish a ThirdpartyContact
+export function isThirdpartyContact(
+ t: BaseThirdParty
+): t is ThirdpartyContact {
+ return (
+ t.type === "thirdparty" &&
+ t.kind === "contact"
+ );
+}
+
+export type Thirdparty = ThirdpartyCompany | ThirdpartyContact | ThirdpartyChild;
+
+
+export function isThirdparty(t: unknown): t is Thirdparty {
+ if (!isBaseThirdParty(t)) {
+ return false;
+ }
+
+ return (isThirdpartyCompany(t) || isThirdpartyContact(t) || isThirdpartyChild(t));
+}
+
interface ThirdpartyType {
key: string;
value: string;
@@ -47,3 +131,29 @@ export interface ThirdpartyCategory {
fr: string;
};
}
+
+/**
+ * Associate an existing ThirdParty during write operation.
+ */
+export interface SetThirdParty {
+ readonly type: "thirdparty";
+ id: number;
+}
+
+export interface ThirdPartyWrite {
+ readonly type: "thirdparty";
+ kind: ThirdPartyKind;
+ civility: SetCivility | null;
+ profession: string;
+ firstname: string;
+ /**
+ * the lastname
+ */
+ name: string;
+ email: string;
+ telephone: string;
+ telephone2: string;
+ address: null|SetAddress;
+ comment: string;
+ parent: SetThirdParty|null;
+}
diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_api/OnTheFly.js b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_api/OnTheFly.js
deleted file mode 100644
index eb8f11ef8..000000000
--- a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_api/OnTheFly.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * GET a thirdparty by id
- */
-const getThirdparty = (id) => {
- const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
- return fetch(url).then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-};
-
-/*
- * POST a new thirdparty
- */
-const postThirdparty = (body) => {
- const url = `/api/1.0/thirdparty/thirdparty.json`;
- return fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json;charset=utf-8",
- },
- body: JSON.stringify(body),
- }).then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-};
-
-/*
- * PATCH an existing thirdparty
- */
-const patchThirdparty = (id, body) => {
- const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
- return fetch(url, {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json;charset=utf-8",
- },
- body: JSON.stringify(body),
- }).then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw Error("Error with request resource response");
- });
-};
-
-export { getThirdparty, postThirdparty, patchThirdparty };
diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_api/OnTheFly.ts b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_api/OnTheFly.ts
new file mode 100644
index 000000000..2e9f40f00
--- /dev/null
+++ b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_api/OnTheFly.ts
@@ -0,0 +1,75 @@
+/*
+ * GET a thirdparty by id
+ */
+import {isThirdpartyChild, isThirdpartyCompany, isThirdpartyContact, Thirdparty, ThirdPartyWrite} from '../../types';
+import {makeFetch} from "ChillMainAssets/lib/api/apiMethods";
+
+export const getThirdparty = async (id: number) : Promise => {
+ const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
+ return fetch(url).then((response) => {
+ if (response.ok) {
+ return response.json();
+ }
+ throw Error("Error with request resource response");
+ });
+};
+
+export const thirdpartyToWriteThirdParty = (t: Thirdparty): ThirdPartyWrite => {
+ // Determine kind-specific fields using available type guards
+ const isCompany = isThirdpartyCompany(t);
+ const isContact = isThirdpartyContact(t);
+ const isChild = isThirdpartyChild(t);
+
+ return {
+ type: 'thirdparty',
+ kind: t.kind,
+ civility:
+ (isContact || isChild) && t.civility
+ ? { type: 'chill_main_civility', id: t.civility.id }
+ : null,
+ profession: (isContact || isChild) ? (t.profession ?? '') : '',
+ firstname: isCompany ? '' : (t.firstname ?? ''),
+ name: isCompany
+ ? (t.nameCompany ?? '')
+ : (t.name ?? ''),
+ email: t.email ?? '',
+ telephone: t.telephone ?? '',
+ telephone2: t.telephone2 ?? '',
+ address: null,
+ comment: isChild ? (t.comment ?? '') : '',
+ parent: isChild && t.parent ? { type: 'thirdparty', id: t.parent.id } : null,
+ };
+};
+
+export interface WriteThirdPartyViolationMap
+extends Record> {
+ email: {
+ "{{ value }}": string;
+ },
+ name: {
+ "{{ value }}": string;
+ },
+ telephone: {
+ "{{ value }}": string;
+ }
+ telephone2: {
+ "{{ value }}": string;
+ }
+}
+
+/*
+ * POST a new thirdparty
+ */
+export const createThirdParty = async (body: ThirdPartyWrite) => {
+ const url = `/api/1.0/thirdparty/thirdparty.json`;
+
+ return makeFetch('POST', url, body);
+};
+
+/*
+ * PATCH an existing thirdparty
+ */
+export const patchThirdparty = async (id: number, body: ThirdPartyWrite): Promise => {
+ const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
+ return makeFetch('PATCH', url, body);
+};
diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue
index 3413861ae..fb4bf2071 100644
--- a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue
+++ b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue
@@ -5,7 +5,7 @@
@@ -44,13 +44,13 @@
{{ getProfession[0] }}
- -
+
-
- {{ $t("child_of") }}
+ {{ trans(THIRDPARTY_MESSAGES_CHILD_OF)}}
@@ -128,65 +128,60 @@
-
diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml
index 806547a04..6e6e96d7a 100644
--- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml
@@ -68,6 +68,10 @@ Remove a contact: Supprimer
Contacts: Contacts
No contacts associated: Aucun contact
+thirdparty:
+ addcontact: Ajouter un contact
+ addcontact_title: Ajouter un contact
+
No nameCompany given: Aucune raison sociale renseignée
No acronym given: Aucun sigle renseigné
No phone given: Aucun téléphone renseigné
@@ -165,7 +169,7 @@ thirdpartyMessages:
comment: "Commentaire"
profession: "Qualité"
civility: "Civilité"
- child_of: "Contact de: "
+ child_of: "Contact d'une institution"
children: "Personnes de contact: "
thirdparty_duplicate:
diff --git a/src/Bundle/ChillTicketBundle/src/Controller/FindCallerController.php b/src/Bundle/ChillTicketBundle/src/Controller/FindCallerController.php
deleted file mode 100644
index c5d476361..000000000
--- a/src/Bundle/ChillTicketBundle/src/Controller/FindCallerController.php
+++ /dev/null
@@ -1,58 +0,0 @@
-query->get('caller', '');
-
- if ('' === $caller) {
- throw new BadRequestHttpException('Missing "caller" query parameter');
- }
-
- try {
- $phoneNumber = $this->phonenumberHelper->parse($caller);
- } catch (NumberParseException $e) {
- throw new BadRequestHttpException('Unable to parse number', $e);
- }
-
- $persons = $this->personRepository->findByPhoneNumber($phoneNumber, 0, 2);
-
- $asArray = match (count($persons)) {
- 0 => ['found' => false, 'name' => null],
- 1 => ['found' => true, 'name' => $this->personRender->renderString($persons[0], ['addAge' => false])],
- default => ['found' => true, 'name' => 'multiple'],
- };
-
- return new JsonResponse($asArray);
- }
-}
diff --git a/src/Bundle/ChillTicketBundle/src/Messenger/Handler/PostTicketUpdateMessageHandler.php b/src/Bundle/ChillTicketBundle/src/Messenger/Handler/PostTicketUpdateMessageHandler.php
index 9424203e9..88f082b58 100644
--- a/src/Bundle/ChillTicketBundle/src/Messenger/Handler/PostTicketUpdateMessageHandler.php
+++ b/src/Bundle/ChillTicketBundle/src/Messenger/Handler/PostTicketUpdateMessageHandler.php
@@ -14,12 +14,13 @@ namespace Chill\TicketBundle\Messenger\Handler;
use Chill\TicketBundle\Event\PostTicketUpdateEvent;
use Chill\TicketBundle\Messenger\PostTicketUpdateMessage;
use Chill\TicketBundle\Repository\TicketRepositoryInterface;
-use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
+use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsMessageHandler]
-final readonly class PostTicketUpdateMessageHandler
+final readonly class PostTicketUpdateMessageHandler implements MessageHandlerInterface
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
diff --git a/src/Bundle/ChillTicketBundle/src/Resources/public/vuejs/TicketApp/components/BannerComponent.vue b/src/Bundle/ChillTicketBundle/src/Resources/public/vuejs/TicketApp/components/BannerComponent.vue
index fd3c0af2b..43d12fdbd 100644
--- a/src/Bundle/ChillTicketBundle/src/Resources/public/vuejs/TicketApp/components/BannerComponent.vue
+++ b/src/Bundle/ChillTicketBundle/src/Resources/public/vuejs/TicketApp/components/BannerComponent.vue
@@ -55,7 +55,7 @@
})
}}
-
+
(() => {
+ if (null === props.ticket.caller) {
+ return [];
+ }
+
+ if (isThirdparty(props.ticket.caller)) {
+ return [props.ticket.caller];
+ } else {
+ return [props.ticket.caller];
+ }
+})
const since = computed(() => {
return store.getters.getSinceCreated(today.value);
diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/FindCallerControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/FindCallerControllerTest.php
deleted file mode 100644
index 997017e97..000000000
--- a/src/Bundle/ChillTicketBundle/tests/Controller/FindCallerControllerTest.php
+++ /dev/null
@@ -1,128 +0,0 @@
-buildController($persons);
-
- $request = new Request(query: ['caller' => $caller]);
-
- $response = $controller->findCaller($request);
-
- $actual = json_decode($response->getContent(), true);
-
- self::assertEqualsCanonicalizing($expected, $actual);
- }
-
- public static function provideFindCaller(): iterable
- {
- yield [
- '32486540600',
- [],
- ['found' => false, 'name' => null],
- ];
-
- yield [
- '32486540600',
- [new Person()],
- ['found' => true, 'name' => 'pppp'],
- ]
- ;
- yield [
- '32486540600',
- [new Person(), new Person()],
- ['found' => true, 'name' => 'multiple'],
- ];
- }
-
- public function testFindCallerWithoutCallerArgument(): void
- {
- self::expectException(BadRequestHttpException::class);
-
- $controller = $this->buildController([]);
-
- $request = new Request(query: []);
-
- $controller->findCaller($request);
- }
-
- public function testFindCallerWithEmptyCallerArgument(): void
- {
- self::expectException(BadRequestHttpException::class);
-
- $controller = $this->buildController([]);
-
- $request = new Request(query: ['caller' => '']);
-
- $controller->findCaller($request);
- }
-
- public function testFindCallerWithInvalidCaller(): void
- {
- self::expectException(BadRequestHttpException::class);
-
- $controller = $this->buildController([]);
-
- $request = new Request(query: ['caller' => 'abcde']);
-
- $controller->findCaller($request);
- }
-
- private function buildController(array $personsFound): FindCallerController
- {
- $phonenumberHelper =
- $subject = new PhonenumberHelper(
- new ArrayAdapter(),
- new ParameterBag([
- 'chill_main.phone_helper' => [
- 'default_carrier_code' => 'BE',
- ],
- ]),
- new NullLogger()
- );
-
- $personRepository = $this->prophesize(PersonRepository::class);
- $personRepository->findByPhoneNumber(Argument::any(), Argument::type('int'), Argument::type('int'))->willReturn($personsFound);
-
- $personRender = $this->prophesize(PersonRenderInterface::class);
- $personRender->renderString(Argument::type(Person::class), Argument::type('array'))->willReturn('pppp');
-
- return new FindCallerController($phonenumberHelper, $personRepository->reveal(), $personRender->reveal());
- }
-}