From 4f9315087455ccdec8ff7785d08112163179ef89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 1 Jul 2025 12:38:02 +0000 Subject: [PATCH] =?UTF-8?q?Cr=C3=A9er=20un=20point=20d'api=20de=20suggesti?= =?UTF-8?q?on=20des=20usagers=20pour=20un=20ticket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Export/Formatter/CSVFormatter.php | 1 - .../Export/Formatter/CSVListFormatter.php | 1 - .../Formatter/SpreadsheetListFormatter.php | 1 - .../Repository/ThirdPartyRepository.php | 42 +++- .../ChillTicketBundle/chill.api.specs.yaml | 87 +++++++- .../AssociateByPhonenumberCommandHandler.php | 13 +- .../Handler/SetCallerCommandHandler.php | 50 +++++ .../src/Action/Ticket/SetCallerCommand.php | 29 +++ .../src/Controller/SetCallerApiController.php | 66 ++++++ .../SuggestPersonForTicketApiController.php | 45 ++++ .../src/Entity/CallerHistory.php | 148 +++++++++++++ .../ChillTicketBundle/src/Entity/Ticket.php | 39 ++++ .../PersonTicketACLAwareRepository.php | 60 ++++++ ...ersonTicketACLAwareRepositoryInterface.php | 29 +++ .../Normalizer/TicketNormalizer.php | 13 ++ .../Service/Ticket/SuggestPersonForTicket.php | 30 +++ .../SuggestPersonForTicketInterface.php | 26 +++ .../src/config/services.yaml | 3 + .../src/migrations/Version20250624105842.php | 93 ++++++++ ...sociateByPhonenumberCommandHandlerTest.php | 26 ++- .../Handler/SetCallerCommandHandlerTest.php | 178 ++++++++++++++++ .../Controller/SetCallerApiControllerTest.php | 200 ++++++++++++++++++ .../tests/Entity/CallerHistoryTest.php | 63 ++++++ .../tests/Entity/TicketCallerTest.php | 82 +++++++ .../PersonTicketACLAwareRepositoryTest.php | 64 ++++++ .../Normalizer/TicketNormalizerTest.php | 52 +++-- .../Ticket/SuggestPersonForTicketTest.php | 70 ++++++ 27 files changed, 1485 insertions(+), 26 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetCallerCommandHandler.php create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/SetCallerCommand.php create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/SetCallerApiController.php create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/SuggestPersonForTicketApiController.php create mode 100644 src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php create mode 100644 src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepository.php create mode 100644 src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepositoryInterface.php create mode 100644 src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicket.php create mode 100644 src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicketInterface.php create mode 100644 src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetCallerCommandHandlerTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Controller/SetCallerApiControllerTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Repository/PersonTicketACLAwareRepositoryTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Service/Ticket/SuggestPersonForTicketTest.php diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php index f0c2b9cee..c7ee03fe8 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php @@ -18,7 +18,6 @@ use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\Translation\TranslatorInterface; -use function count; /** * Command to get the report with curl: diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php index fda49f202..1a54eee5f 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php @@ -17,7 +17,6 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\Translation\TranslatorInterface; -use function count; // command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php index 3c5e90381..0a35e087f 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php @@ -21,7 +21,6 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\Translation\TranslatorInterface; -use function count; // command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff diff --git a/src/Bundle/ChillThirdPartyBundle/Repository/ThirdPartyRepository.php b/src/Bundle/ChillThirdPartyBundle/Repository/ThirdPartyRepository.php index 111be4089..de9cdd270 100644 --- a/src/Bundle/ChillThirdPartyBundle/Repository/ThirdPartyRepository.php +++ b/src/Bundle/ChillThirdPartyBundle/Repository/ThirdPartyRepository.php @@ -18,12 +18,15 @@ use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberFormat; +use libphonenumber\PhoneNumberUtil; class ThirdPartyRepository implements ObjectRepository { private readonly EntityRepository $repository; - public function __construct(EntityManagerInterface $em, private readonly Connection $connection) + public function __construct(EntityManagerInterface $em, private readonly Connection $connection, private readonly PhoneNumberUtil $phonenumberUtil) { $this->repository = $em->getRepository(ThirdParty::class); } @@ -122,6 +125,43 @@ class ThirdPartyRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + /** + * Finds third-party records by phone number. + * + * The search is performed agains every phonenumber field (there are two phonenumber on a thirdParty). + * + * @param string|PhoneNumber $phonenumber The phone number to search for. Can be a string or a PhoneNumber object. + * @param int $firstResult the index of the first result to retrieve (pagination start) + * @param int $maxResults the maximum number of results to retrieve (pagination limit) + * + * @return list the result set containing matching third-party records + */ + public function findByPhonenumber(string|PhoneNumber $phonenumber, int $firstResult = 0, int $maxResults = 20): array + { + if ('' === $phonenumber) { + return []; + } + + $qb = $this->createQueryBuilder('tp'); + $qb->select('tp'); + + $qb->where( + $qb->expr()->orX( + $qb->expr()->eq('t.telephone', ':phonenumber'), + $qb->expr()->eq('t.telephone2', ':phonenumber') + ) + ); + + $qb->setParameter( + 'phonenumber', + is_string($phonenumber) ? $phonenumber : $this->phonenumberUtil->format($phonenumber, PhoneNumberFormat::E164) + ); + + $qb->setFirstResult($firstResult)->setMaxResults($maxResults); + + return $qb->getQuery()->getResult(); + } + /** * Search amongst parties associated to $centers, with $terms parameters. * diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index 77bf2797d..3e40cab66 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -164,8 +164,8 @@ paths: type: array items: oneOf: - - $ref: '#/components/schemas/UserGroupById' - - $ref: '#/components/schemas/UserById' + - $ref: '#/components/schemas/UserGroupById' + - $ref: '#/components/schemas/UserById' responses: @@ -197,8 +197,8 @@ paths: properties: addressee: oneOf: - - $ref: '#/components/schemas/UserGroupById' - - $ref: '#/components/schemas/UserById' + - $ref: '#/components/schemas/UserGroupById' + - $ref: '#/components/schemas/UserById' responses: @@ -288,3 +288,82 @@ paths: description: "OK" 401: description: "UNAUTHORIZED" + + /1.0/ticket/ticket/{id}/set-caller: + post: + tags: + - ticket + summary: Set a caller for this ticket + description: | + Set a caller to the ticket. + + To remove the caller, set the caller field to null + parameters: + - name: id + in: path + required: true + description: The ticket id + schema: + type: integer + format: integer + minimum: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + caller: + nullable: true + oneOf: + - $ref: '#/components/schemas/PersonById' + - $ref: '#/components/schemas/ThirdPartyById' + examples: + add_user: + value: + caller: + type: person + id: 8 + summary: Set the person with id 8 + add_third_party: + value: + caller: + type: thirdparty + id: 10 + summary: Set the third party with id 10 + remove: + value: + caller: null + summary: Remove the caller (set the caller to null) + + responses: + 200: + description: "OK" + 401: + description: "UNAUTHORIZED" + /1.0/ticket/ticket/{id}/suggest-person: + get: + tags: + - ticket + summary: Get a list of person suggested for the given ticket + parameters: + - name: id + in: path + required: true + description: The ticket id + schema: + type: integer + format: integer + minimum: 1 + responses: + 401: + description: "UNAUTHORIZED" + 200: + description: "OK" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Person' diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/AssociateByPhonenumberCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/AssociateByPhonenumberCommandHandler.php index d794250c5..a5518504f 100644 --- a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/AssociateByPhonenumberCommandHandler.php +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/AssociateByPhonenumberCommandHandler.php @@ -13,8 +13,9 @@ namespace Chill\TicketBundle\Action\Ticket\Handler; use Chill\MainBundle\Phonenumber\PhonenumberHelper; use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface; +use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand; -use Chill\TicketBundle\Entity\PersonHistory; +use Chill\TicketBundle\Entity\CallerHistory; use Chill\TicketBundle\Entity\Ticket; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Clock\ClockInterface; @@ -23,6 +24,7 @@ class AssociateByPhonenumberCommandHandler { public function __construct( private readonly PersonACLAwareRepositoryInterface $personRepository, + private readonly ThirdPartyRepository $thirdPartyRepository, private readonly PhonenumberHelper $phonenumberHelper, private readonly ClockInterface $clock, private readonly EntityManagerInterface $entityManager, @@ -31,10 +33,13 @@ class AssociateByPhonenumberCommandHandler public function __invoke(Ticket $ticket, AssociateByPhonenumberCommand $command): void { $phone = $this->phonenumberHelper->parse($command->phonenumber); - $persons = $this->personRepository->findByPhone($phone); + $callers = [ + ...$this->personRepository->findByPhone($phone), + ...$this->thirdPartyRepository->findByPhonenumber($phone), + ]; - foreach ($persons as $person) { - $history = new PersonHistory($person, $ticket, $this->clock->now()); + if (count($callers) > 0) { + $history = new CallerHistory($callers[0], $ticket, $this->clock->now()); $this->entityManager->persist($history); } } diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetCallerCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetCallerCommandHandler.php new file mode 100644 index 000000000..ae37f3945 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetCallerCommandHandler.php @@ -0,0 +1,50 @@ +getCaller(); + if ($currentCaller === $command->caller) { + return $ticket; + } + + // End the current caller history (if any) + foreach ($ticket->getCallerHistories() as $callerHistory) { + if (null === $callerHistory->getEndDate()) { + $callerHistory->setEndDate($this->clock->now()); + } + } + + // Create a new caller history with the new caller + new CallerHistory( + $command->caller, + $ticket, + $this->clock->now(), + ); + + return $ticket; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetCallerCommand.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetCallerCommand.php new file mode 100644 index 000000000..2589abac4 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetCallerCommand.php @@ -0,0 +1,29 @@ + '\d+'], methods: ['POST'])] + public function setCaller(Ticket $ticket, Request $request): Response + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users are allowed to set ticket callers.'); + } + + try { + /** @var SetCallerCommand $command */ + $command = $this->serializer->deserialize( + $request->getContent(), + SetCallerCommand::class, + 'json' + ); + } catch (\Throwable $e) { + throw new BadRequestHttpException('Invalid request body: '.$e->getMessage(), $e); + } + + $this->setCallerCommandHandler->__invoke($ticket, $command); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($ticket, 'json', ['groups' => ['read']]), + json: true + ); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Controller/SuggestPersonForTicketApiController.php b/src/Bundle/ChillTicketBundle/src/Controller/SuggestPersonForTicketApiController.php new file mode 100644 index 000000000..a71065b73 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/SuggestPersonForTicketApiController.php @@ -0,0 +1,45 @@ + '\d+'], methods: ['GET'])] + public function __invoke(Ticket $ticket): Response + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users are allowed to suggest persons for tickets.'); + } + + $persons = $this->suggestPersonForTicket->suggestPerson($ticket, 0, 10); + + return new JsonResponse( + $this->serializer->serialize($persons, 'json', ['groups' => ['read']]), + json: true, + ); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php b/src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php new file mode 100644 index 000000000..491f347a5 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php @@ -0,0 +1,148 @@ + CallerHistory::class])] +class CallerHistory implements TrackCreationInterface +{ + use TrackCreationTrait; + + #[ORM\Id] + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Serializer\Groups(['read'])] + private ?int $id = null; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])] + #[Serializer\Groups(['read'])] + private ?\DateTimeImmutable $endDate = null; + + #[ORM\ManyToOne(targetEntity: Person::class)] + #[ORM\JoinColumn(nullable: true)] + #[Serializer\Groups(['read'])] + private ?Person $person = null; + + #[ORM\ManyToOne(targetEntity: ThirdParty::class)] + #[ORM\JoinColumn(nullable: true)] + #[Serializer\Groups(['read'])] + private ?ThirdParty $thirdParty = null; + + public function __construct( + ThirdParty|Person|null $caller, + #[ORM\ManyToOne(targetEntity: Ticket::class)] + #[ORM\JoinColumn(nullable: false)] + private Ticket $ticket, + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)] + #[Serializer\Groups(['read'])] + private \DateTimeImmutable $startDate = new \DateTimeImmutable('now'), + ) { + $this->setCaller($caller); + $ticket->addCallerHistory($this); + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function getThirdParty(): ?ThirdParty + { + return $this->thirdParty; + } + + public function getStartDate(): \DateTimeImmutable + { + return $this->startDate; + } + + public function getTicket(): Ticket + { + return $this->ticket; + } + + public function setEndDate(?\DateTimeImmutable $endDate): void + { + $this->endDate = $endDate; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + // If setting a person, ensure thirdParty is null + if (null !== $person) { + $this->thirdParty = null; + } + + return $this; + } + + public function setThirdParty(?ThirdParty $thirdParty): self + { + $this->thirdParty = $thirdParty; + + // If setting a thirdParty, ensure person is null + if (null !== $thirdParty) { + $this->person = null; + } + + return $this; + } + + /** + * Set the caller. + * + * This is a private method and should be only called while instance creation + */ + private function setCaller(Person|ThirdParty|null $caller): void + { + if ($caller instanceof Person) { + $this->setPerson($caller); + } elseif ($caller instanceof ThirdParty) { + $this->setThirdParty($caller); + } + } + + /** + * Get the caller, which can be either a Person or a ThirdParty. + */ + public function getCaller(): Person|ThirdParty|null + { + return $this->person ?? $this->thirdParty; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php index 8ef4f64a9..092aa5a35 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php @@ -96,6 +96,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface #[ORM\OneToMany(targetEntity: EmergencyStatusHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])] private Collection $emergencyStatusHistories; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: CallerHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])] + private Collection $callerHistories; + public function __construct() { $this->addresseeHistory = new ArrayCollection(); @@ -105,6 +111,7 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface $this->inputHistories = new ArrayCollection(); $this->stateHistories = new ArrayCollection(); $this->emergencyStatusHistories = new ArrayCollection(); + $this->callerHistories = new ArrayCollection(); } public function getId(): ?int @@ -294,4 +301,36 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface { return $this->emergencyStatusHistories; } + + /** + * @internal use @see{CallerHistory::__construct} instead + */ + public function addCallerHistory(CallerHistory $callerHistory): void + { + $this->callerHistories->add($callerHistory); + } + + /** + * Get the current caller (Person or ThirdParty) associated with this ticket. + * + * @return Person|ThirdParty|null + */ + public function getCaller() + { + foreach ($this->callerHistories as $callerHistory) { + if (null === $callerHistory->getEndDate()) { + return $callerHistory->getCaller(); + } + } + + return null; + } + + /** + * @return ReadableCollection + */ + public function getCallerHistories(): ReadableCollection + { + return $this->callerHistories; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepository.php b/src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepository.php new file mode 100644 index 000000000..21b61eb18 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepository.php @@ -0,0 +1,60 @@ +em); + $resultSetMappingBuilder->addRootEntityFromClassMetadata(Person::class, 'p'); + + $callerClause = match ($caller instanceof Person) { + true => 'caller_history.person_id = :callerId', + false => 'caller_history.thirdparty_id = :callerId', + }; + + $query = <<generateSelectClause()} FROM chill_person_person p + WHERE p.id IN ( + SELECT person_id + FROM chill_ticket.person_history person_history + WHERE person_history.endDate IS NULL + AND EXISTS ( + SELECT 1 FROM chill_ticket.caller_history + WHERE + caller_history.ticket_id = person_history.ticket_id + AND caller_history.endDate IS NULL + AND {$callerClause} + ) + ORDER BY person_history.startDate DESC, p.id ASC + ) + OFFSET :start LIMIT :limit; + SQL; + + $nql = $this->em->createNativeQuery($query, $resultSetMappingBuilder); + $nql + ->setParameter('callerId', $caller->getId()) + ->setParameter('start', $start) + ->setParameter('limit', $limit); + + + return $nql->getResult(); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepositoryInterface.php b/src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepositoryInterface.php new file mode 100644 index 000000000..dd1a1d014 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Repository/PersonTicketACLAwareRepositoryInterface.php @@ -0,0 +1,29 @@ + + */ + public function findPersonPreviouslyAssociatedWithCaller(Person|ThirdParty $caller, int $start = 0, int $limit = 100): array; +} diff --git a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php index 87aa5ad05..b10a0873d 100644 --- a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php +++ b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Chill\PersonBundle\Entity\Person; use Chill\TicketBundle\Entity\AddresseeHistory; +use Chill\TicketBundle\Entity\CallerHistory; use Chill\TicketBundle\Entity\Comment; use Chill\TicketBundle\Entity\EmergencyStatusHistory; use Chill\TicketBundle\Entity\MotiveHistory; @@ -53,6 +54,7 @@ final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInte 'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context), 'currentState' => $object->getState()?->value ?? 'open', 'emergency' => $object->getEmergencyStatus()?->value ?? 'no', + 'caller' => $this->normalizer->normalize($object->getCaller(), $format, ['groups' => 'read']), ]; } @@ -115,6 +117,17 @@ final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInte ], $ticket->getEmergencyStatusHistories()->toArray(), ), + ...array_map( + fn (CallerHistory $stateHistory) => [ + 'event_type' => 'set_caller', + 'at' => $stateHistory->getStartDate(), + 'by' => $stateHistory->getCreatedBy(), + 'data' => [ + 'new_caller' => $this->normalizer->normalize($ticket->getCaller(), $format, ['groups' => ['read']]), + ], + ], + $ticket->getCallerHistories()->toArray(), + ), ]; if (null !== $ticket->getCreatedBy() && null !== $ticket->getCreatedAt()) { diff --git a/src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicket.php b/src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicket.php new file mode 100644 index 000000000..a454408dd --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicket.php @@ -0,0 +1,30 @@ +getCaller(); + if (null === $caller) { + return []; + } + + return $this->personTicketACLAwareRepository->findPersonPreviouslyAssociatedWithCaller($caller, $start, $limit); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicketInterface.php b/src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicketInterface.php new file mode 100644 index 000000000..10d0eb31b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Service/Ticket/SuggestPersonForTicketInterface.php @@ -0,0 +1,26 @@ + + */ + public function suggestPerson(Ticket $ticket, int $start = 0, int $limit = 20): array; +} diff --git a/src/Bundle/ChillTicketBundle/src/config/services.yaml b/src/Bundle/ChillTicketBundle/src/config/services.yaml index 331e03898..e5618284a 100644 --- a/src/Bundle/ChillTicketBundle/src/config/services.yaml +++ b/src/Bundle/ChillTicketBundle/src/config/services.yaml @@ -17,6 +17,9 @@ services: Chill\TicketBundle\Serializer\: resource: '../Serializer/' + Chill\TicketBundle\Service\: + resource: '../Service/' + Chill\TicketBundle\Menu\: resource: '../Menu/' diff --git a/src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php b/src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php new file mode 100644 index 000000000..1a49a4f5b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php @@ -0,0 +1,93 @@ +addSql(<<<'SQL' + CREATE SEQUENCE chill_ticket.caller_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1 + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE chill_ticket.caller_history (id INT NOT NULL, person_id INT DEFAULT NULL, ticket_id INT NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, thirdParty_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id)) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE24217BBB47 ON chill_ticket.caller_history (person_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE243EA5CAB0 ON chill_ticket.caller_history (thirdParty_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE24700047D2 ON chill_ticket.caller_history (ticket_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE243174800F ON chill_ticket.caller_history (createdBy_id) + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.caller_history.endDate IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.caller_history.startDate IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.caller_history.createdAt IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE24217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE243EA5CAB0 FOREIGN KEY (thirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE24700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE243174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT caller_history_not_overlaps + exclude using gist (ticket_id with =, tsrange(startdate, enddate) with &&) + deferrable initially deferred + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP SEQUENCE chill_ticket.caller_history_id_seq CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE24217BBB47 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE243EA5CAB0 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE24700047D2 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE243174800F + SQL); + $this->addSql(<<<'SQL' + DROP TABLE chill_ticket.caller_history + SQL); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AssociateByPhonenumberCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AssociateByPhonenumberCommandHandlerTest.php index 5ade6ec82..067443bd9 100644 --- a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AssociateByPhonenumberCommandHandlerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/AssociateByPhonenumberCommandHandlerTest.php @@ -14,10 +14,13 @@ namespace Chill\TicketBundle\Tests\Action\Ticket\Handler; use Chill\MainBundle\Phonenumber\PhonenumberHelper; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface; +use Chill\ThirdPartyBundle\Entity\ThirdParty; +use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand; use Chill\TicketBundle\Action\Ticket\Handler\AssociateByPhonenumberCommandHandler; use Chill\TicketBundle\Entity\Ticket; use Doctrine\ORM\EntityManagerInterface; +use libphonenumber\PhoneNumber; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -37,6 +40,7 @@ class AssociateByPhonenumberCommandHandlerTest extends TestCase private function getHandler( PersonACLAwareRepositoryInterface $personACLAwareRepository, + ThirdPartyRepository $thirdPartyRepository, ): AssociateByPhonenumberCommandHandler { $entityManager = $this->prophesize(EntityManagerInterface::class); $phonenumberHelper = new PhonenumberHelper( @@ -51,6 +55,7 @@ class AssociateByPhonenumberCommandHandlerTest extends TestCase return new AssociateByPhonenumberCommandHandler( $personACLAwareRepository, + $thirdPartyRepository, $phonenumberHelper, new MockClock(), $entityManager->reveal() @@ -63,12 +68,29 @@ class AssociateByPhonenumberCommandHandlerTest extends TestCase $personAclAwareRepository = $this->prophesize(PersonACLAwareRepositoryInterface::class); $personAclAwareRepository->findByPhone(Argument::any())->willReturn([$person]); + $thirdPartyRepository = $this->prophesize(ThirdPartyRepository::class); + $thirdPartyRepository->findByPhonenumber(Argument::type(PhoneNumber::class))->willReturn([]); - $handler = $this->getHandler($personAclAwareRepository->reveal()); + $handler = $this->getHandler($personAclAwareRepository->reveal(), $thirdPartyRepository->reveal()); $ticket = new Ticket(); $handler($ticket, new AssociateByPhonenumberCommand('+3281136917')); - self::assertSame($person, $ticket->getPersons()[0]); + self::assertSame($person, $ticket->getCaller()); + } + + public function testHandleWithThirdPartyFoundByPhonenumber(): void + { + $personAclAwareRepository = $this->prophesize(PersonACLAwareRepositoryInterface::class); + $personAclAwareRepository->findByPhone(Argument::any())->willReturn([]); + $thirdPartyRepository = $this->prophesize(ThirdPartyRepository::class); + $thirdPartyRepository->findByPhonenumber(Argument::type(PhoneNumber::class))->willReturn([$thirdParty = new ThirdParty()]); + + $handler = $this->getHandler($personAclAwareRepository->reveal(), $thirdPartyRepository->reveal()); + + $ticket = new Ticket(); + $handler($ticket, new AssociateByPhonenumberCommand('081136917')); + + self::assertSame($thirdParty, $ticket->getCaller()); } } diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetCallerCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetCallerCommandHandlerTest.php new file mode 100644 index 000000000..e2e8a642b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetCallerCommandHandlerTest.php @@ -0,0 +1,178 @@ +now = new \DateTimeImmutable('2023-01-01 12:00:00'); + $this->clock = new MockClock($this->now); + $this->handler = new SetCallerCommandHandler($this->clock); + } + + public function testSetPersonAsCaller(): void + { + // Arrange + $ticket = new Ticket(); + $person = new Person(); + $command = new SetCallerCommand($person); + + // Act + $result = ($this->handler)($ticket, $command); + + // Assert + self::assertSame($ticket, $result); + self::assertSame($person, $ticket->getCaller()); + self::assertCount(1, $ticket->getCallerHistories()); + + $callerHistory = $ticket->getCallerHistories()->first(); + self::assertInstanceOf(CallerHistory::class, $callerHistory); + self::assertSame($person, $callerHistory->getPerson()); + self::assertNull($callerHistory->getThirdParty()); + self::assertEquals($this->now->getTimestamp(), $callerHistory->getStartDate()->getTimestamp()); + self::assertNull($callerHistory->getEndDate()); + } + + public function testSetThirdPartyAsCaller(): void + { + // Arrange + $ticket = new Ticket(); + $thirdParty = new ThirdParty(); + $command = new SetCallerCommand($thirdParty); + + // Act + $result = ($this->handler)($ticket, $command); + + // Assert + self::assertSame($ticket, $result); + self::assertSame($thirdParty, $ticket->getCaller()); + self::assertCount(1, $ticket->getCallerHistories()); + + $callerHistory = $ticket->getCallerHistories()->first(); + self::assertInstanceOf(CallerHistory::class, $callerHistory); + self::assertNull($callerHistory->getPerson()); + self::assertSame($thirdParty, $callerHistory->getThirdParty()); + self::assertEquals($this->now->getTimestamp(), $callerHistory->getStartDate()->getTimestamp()); + self::assertNull($callerHistory->getEndDate()); + } + + public function testChangeCallerFromPersonToThirdParty(): void + { + // Arrange + $ticket = new Ticket(); + $person = new Person(); + $thirdParty = new ThirdParty(); + + // Set initial person caller + $initialCallerHistory = new CallerHistory($person, $ticket, $this->now); + $initialCallerHistory->setPerson($person); + + // Create command to change to third party + $command = new SetCallerCommand($thirdParty); + + // Act + $this->clock->modify('+ 10 minutes'); + $result = ($this->handler)($ticket, $command); + + // Assert + self::assertSame($ticket, $result); + self::assertSame($thirdParty, $ticket->getCaller()); + self::assertCount(2, $ticket->getCallerHistories()); + + // Check that the first history is ended + $firstCallerHistory = $ticket->getCallerHistories()->first(); + self::assertSame($person, $firstCallerHistory->getPerson()); + self::assertEquals($this->clock->now()->getTimestamp(), $firstCallerHistory->getEndDate()->getTimestamp()); + + // Check that the new history is created correctly + $lastCallerHistory = $ticket->getCallerHistories()->last(); + self::assertNull($lastCallerHistory->getPerson()); + self::assertSame($thirdParty, $lastCallerHistory->getThirdParty()); + self::assertSame($this->clock->now()->getTimestamp(), $lastCallerHistory->getStartDate()->getTimestamp()); + self::assertNull($lastCallerHistory->getEndDate()); + } + + public function testRemoveCaller(): void + { + // Arrange + $ticket = new Ticket(); + $person = new Person(); + + // Set initial person caller + $initialCallerHistory = new CallerHistory($person, $ticket, $this->now); + + // Create command to remove caller + $command = new SetCallerCommand(null); + + // Act + $result = ($this->handler)($ticket, $command); + + // Assert + self::assertSame($ticket, $result); + self::assertNull($ticket->getCaller()); + self::assertCount(2, $ticket->getCallerHistories()); + + // Check that the history is ended + $callerHistory = $ticket->getCallerHistories()->first(); + self::assertSame($person, $callerHistory->getPerson()); + self::assertEquals($this->now->getTimestamp(), $callerHistory->getEndDate()->getTimestamp()); + } + + public function testNoChangeWhenCallerIsAlreadySet(): void + { + // Arrange + $ticket = new Ticket(); + $person = new Person(); + + // Set initial person caller + $initialCallerHistory = new CallerHistory($person, $ticket, $this->now); + + // Create command with the same person + $command = new SetCallerCommand($person); + + // Act + $result = ($this->handler)($ticket, $command); + + // Assert + self::assertSame($ticket, $result); + self::assertSame($person, $ticket->getCaller()); + self::assertCount(1, $ticket->getCallerHistories()); + + // Check that the history is unchanged + $callerHistory = $ticket->getCallerHistories()->first(); + self::assertSame($person, $callerHistory->getPerson()); + self::assertNull($callerHistory->getEndDate()); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/SetCallerApiControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/SetCallerApiControllerTest.php new file mode 100644 index 000000000..aad6342dd --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Controller/SetCallerApiControllerTest.php @@ -0,0 +1,200 @@ +prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(false); + + $setCallerCommandHandler = $this->prophesize(SetCallerCommandHandler::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $serializer = $this->prophesize(SerializerInterface::class); + + $controller = new SetCallerApiController( + $setCallerCommandHandler->reveal(), + $security->reveal(), + $entityManager->reveal(), + $serializer->reveal(), + ); + + $this->expectException(AccessDeniedHttpException::class); + $controller->setCaller($ticket, $request); + } + + public function testSetCallerWithInvalidRequestBody(): void + { + $ticket = new Ticket(); + $request = new Request([], [], [], [], [], [], 'invalid json'); + + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $setCallerCommandHandler = $this->prophesize(SetCallerCommandHandler::class); + $entityManager = $this->prophesize(EntityManagerInterface::class); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->deserialize('invalid json', SetCallerCommand::class, 'json') + ->willThrow(new \Exception('Invalid JSON')); + + $controller = new SetCallerApiController( + $setCallerCommandHandler->reveal(), + $security->reveal(), + $entityManager->reveal(), + $serializer->reveal(), + ); + + $this->expectException(BadRequestHttpException::class); + $controller->setCaller($ticket, $request); + } + + public function testSetCallerWithValidRequest(): void + { + $ticket = new Ticket(); + $request = new Request([], [], [], [], [], [], '{"caller": {"id": 123, "type": "person"}}'); + + $person = new Person(); + $command = new SetCallerCommand($person); + + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->deserialize('{"caller": {"id": 123, "type": "person"}}', SetCallerCommand::class, 'json') + ->willReturn($command); + $serializer->serialize($ticket, 'json', ['groups' => ['read']]) + ->willReturn('{}') + ->shouldBeCalled(); + + $setCallerCommandHandler = $this->prophesize(SetCallerCommandHandler::class); + $setCallerCommandHandler->__invoke($ticket, $command) + ->willReturn($ticket) + ->shouldBeCalled(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $controller = new SetCallerApiController( + $setCallerCommandHandler->reveal(), + $security->reveal(), + $entityManager->reveal(), + $serializer->reveal(), + ); + + $response = $controller->setCaller($ticket, $request); + + $this->assertInstanceOf(JsonResponse::class, $response); + } + + public function testSetCallerWithThirdParty(): void + { + $ticket = new Ticket(); + $request = new Request([], [], [], [], [], [], '{"caller": {"id": 456, "type": "thirdParty"}}'); + + $thirdParty = $this->prophesize(ThirdParty::class)->reveal(); + $command = new SetCallerCommand($thirdParty); + + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->deserialize('{"caller": {"id": 456, "type": "thirdParty"}}', SetCallerCommand::class, 'json') + ->willReturn($command); + $serializer->serialize($ticket, 'json', ['groups' => ['read']]) + ->willReturn('{}') + ->shouldBeCalled(); + + $setCallerCommandHandler = $this->prophesize(SetCallerCommandHandler::class); + $setCallerCommandHandler->__invoke($ticket, $command) + ->willReturn($ticket) + ->shouldBeCalled(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $controller = new SetCallerApiController( + $setCallerCommandHandler->reveal(), + $security->reveal(), + $entityManager->reveal(), + $serializer->reveal(), + ); + + $response = $controller->setCaller($ticket, $request); + + $this->assertInstanceOf(JsonResponse::class, $response); + } + + public function testSetCallerToNull(): void + { + $ticket = new Ticket(); + $request = new Request([], [], [], [], [], [], '{"caller": null}'); + + $command = new SetCallerCommand(null); + + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->deserialize('{"caller": null}', SetCallerCommand::class, 'json') + ->willReturn($command); + $serializer->serialize($ticket, 'json', ['groups' => ['read']]) + ->willReturn('{}') + ->shouldBeCalled(); + + $setCallerCommandHandler = $this->prophesize(SetCallerCommandHandler::class); + $setCallerCommandHandler->__invoke($ticket, $command) + ->willReturn($ticket) + ->shouldBeCalled(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $controller = new SetCallerApiController( + $setCallerCommandHandler->reveal(), + $security->reveal(), + $entityManager->reveal(), + $serializer->reveal(), + ); + + $response = $controller->setCaller($ticket, $request); + + $this->assertInstanceOf(JsonResponse::class, $response); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php new file mode 100644 index 000000000..36024fde5 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php @@ -0,0 +1,63 @@ +getTicket()); + self::assertNull($callerHistory->getEndDate()); + self::assertSame($person, $callerHistory->getPerson()); + self::assertNull($callerHistory->getThirdParty()); + self::assertSame($person, $callerHistory->getCaller()); + } + + public function testConstructorWithThirdParty(): void + { + $ticket = new Ticket(); + + $callerHistory = new CallerHistory($thirdParty = new ThirdParty(), $ticket); + + self::assertSame($ticket, $callerHistory->getTicket()); + self::assertNull($callerHistory->getEndDate()); + self::assertNull($callerHistory->getPerson()); + self::assertSame($thirdParty, $callerHistory->getThirdParty()); + self::assertSame($thirdParty, $callerHistory->getCaller()); + } + + public function testSetEndDate(): void + { + $ticket = $this->createMock(Ticket::class); + $callerHistory = new CallerHistory(new ThirdParty(), $ticket); + + $endDate = new \DateTimeImmutable('2023-01-01'); + $callerHistory->setEndDate($endDate); + + self::assertSame($endDate, $callerHistory->getEndDate()); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php new file mode 100644 index 000000000..ad9b8ecaf --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php @@ -0,0 +1,82 @@ +getCaller()); + + // Create a person + $person = new Person(); + + // Create a caller history with the person + $callerHistory = new CallerHistory($person, $ticket); + + // The ticket should now return the person as the caller + self::assertSame($person, $ticket->getCaller()); + + // Create a third party + $thirdParty = new ThirdParty(); + + // Create a new caller history with the third party + $callerHistory2 = new CallerHistory($thirdParty, $ticket); + + // End the first caller history + $callerHistory->setEndDate(new \DateTimeImmutable()); + + // The ticket should now return the third party as the caller + self::assertSame($thirdParty, $ticket->getCaller()); + + // End the second caller history + $callerHistory2->setEndDate(new \DateTimeImmutable()); + + // The ticket should now return null as there is no active caller + self::assertNull($ticket->getCaller()); + } + + public function testGetCallerHistories(): void + { + $ticket = new Ticket(); + + // Initially, there should be no caller histories + self::assertCount(0, $ticket->getCallerHistories()); + + // Create a caller history + $callerHistory = new CallerHistory(new Person(), $ticket); + + // The ticket should now have one caller history + self::assertCount(1, $ticket->getCallerHistories()); + self::assertSame($callerHistory, $ticket->getCallerHistories()->first()); + + // Create another caller history + $callerHistory2 = new CallerHistory(new ThirdParty(), $ticket); + + // The ticket should now have two caller histories + self::assertCount(2, $ticket->getCallerHistories()); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Repository/PersonTicketACLAwareRepositoryTest.php b/src/Bundle/ChillTicketBundle/tests/Repository/PersonTicketACLAwareRepositoryTest.php new file mode 100644 index 000000000..654ba2467 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Repository/PersonTicketACLAwareRepositoryTest.php @@ -0,0 +1,64 @@ +repository = self::getContainer()->get(PersonTicketACLAwareRepository::class); + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); + } + + public function testFindPersonPreviouslyAssociatedWithCallerWithPerson() + { + $person = $this->getRandomPerson(self::getContainer()->get(EntityManagerInterface::class)); + + $actual = $this->repository->findPersonPreviouslyAssociatedWithCaller($person); + + self::assertIsList($actual); + } + + public function testFindPersonPreviouslyAssociatedWithCallerWithThirdParty() + { + $thirdParty = $this->entityManager->createQuery( + sprintf('SELECT t FROM %s t', ThirdParty::class) + ) + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $thirdParty) { + throw new \RuntimeException('the third party table seems to be empty'); + } + + $actual = $this->repository->findPersonPreviouslyAssociatedWithCaller($thirdParty); + + self::assertIsList($actual); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php index d30334b0d..2e3de468d 100644 --- a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Chill\PersonBundle\Entity\Person; use Chill\TicketBundle\Entity\AddresseeHistory; +use Chill\TicketBundle\Entity\CallerHistory; use Chill\TicketBundle\Entity\Comment; use Chill\TicketBundle\Entity\EmergencyStatusEnum; use Chill\TicketBundle\Entity\EmergencyStatusHistory; @@ -91,27 +92,16 @@ class TicketNormalizerTest extends KernelTestCase 'history' => [ [ 'event_type' => 'create_ticket', - 'at' => 1718495999, - 'by' => [ - 0 => 'user', - ], - 'data' => [], ], [ 'event_type' => 'state_change', - 'at' => 1718495999, - 'by' => [ - 0 => 'user', - ], - 'data' => [ - 'new_state' => 'open', - ], ], ], 'currentState' => 'open', 'updatedAt' => $t->getUpdatedAt()->getTimestamp(), 'updatedBy' => ['user'], 'emergency' => 'no', + 'caller' => null, ], ]; @@ -165,6 +155,36 @@ class TicketNormalizerTest extends KernelTestCase 'updatedAt' => $ticket->getUpdatedAt()->getTimestamp(), 'updatedBy' => ['user'], 'emergency' => 'yes', + 'caller' => null, + ], + ]; + + // ticket with caller + $ticket = new Ticket(); + $ticket->setCreatedAt(new \DateTimeImmutable('2024-06-16T00:00:00')); + $ticket->setUpdatedAt(new \DateTimeImmutable('2024-06-16T00:00:00')); + new CallerHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00')); + + yield [ + $ticket, + [ + 'type' => 'ticket_ticket', + 'createdAt' => $ticket->getCreatedAt()?->getTimestamp(), + 'createdBy' => null, + 'id' => null, + 'externalRef' => '', + 'currentPersons' => [], + 'currentAddressees' => [], + 'currentInputs' => [], + 'currentMotive' => null, + 'history' => [ + ['event_type' => 'set_caller'], + ], + 'currentState' => 'open', + 'updatedAt' => $ticket->getUpdatedAt()->getTimestamp(), + 'updatedBy' => null, + 'emergency' => 'no', + 'caller' => ['person'], ], ]; } @@ -224,6 +244,11 @@ class TicketNormalizerTest extends KernelTestCase 'json', ['groups' => 'read'] )->will(fn ($args): array => $args[0]); + $normalizer->normalize( + Argument::that(fn ($arg) => is_array($arg) && 1 === count($arg) && array_key_exists('new_caller', $arg)), + 'json', + ['groups' => 'read'] + )->will(fn ($args): array => ['new_caller' => ['dummy']]); // datetime $normalizer->normalize(Argument::type(\DateTimeImmutable::class), 'json', Argument::type('array')) @@ -231,6 +256,9 @@ class TicketNormalizerTest extends KernelTestCase // user $normalizer->normalize(Argument::type(User::class), 'json', Argument::type('array')) ->willReturn(['user']); + // person + $normalizer->normalize(Argument::type(Person::class), 'json', Argument::type('array')) + ->willReturn(['person']); // motive $normalizer->normalize(Argument::type(Motive::class), 'json', Argument::type('array'))->willReturn(['type' => 'motive', 'id' => 0]); // person history diff --git a/src/Bundle/ChillTicketBundle/tests/Service/Ticket/SuggestPersonForTicketTest.php b/src/Bundle/ChillTicketBundle/tests/Service/Ticket/SuggestPersonForTicketTest.php new file mode 100644 index 000000000..05a786385 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Service/Ticket/SuggestPersonForTicketTest.php @@ -0,0 +1,70 @@ +repository = $this->prophesize(PersonTicketACLAwareRepositoryInterface::class); + } + + private function buildSuggestPersonForTicket(): SuggestPersonForTicket + { + return new SuggestPersonForTicket($this->repository->reveal()); + } + + public function testSuggestPersonNoCaller() + { + $ticket = new Ticket(); + + assert(null === $ticket->getCaller()); + + $this->repository->findPersonPreviouslyAssociatedWithCaller(Argument::any())->shouldNotBeCalled(); + $suggester = $this->buildSuggestPersonForTicket(); + + self::assertEquals([], $suggester->suggestPerson($ticket)); + } + + public function testSuggestPersonCaller() + { + $ticket = new Ticket(); + new CallerHistory($person = new Person(), $ticket); + + assert($person === $ticket->getCaller()); + + $this->repository->findPersonPreviouslyAssociatedWithCaller($person, 0, 20) + ->willReturn($result = [new Person(), new Person()]) + ->shouldBeCalledOnce(); + $suggester = $this->buildSuggestPersonForTicket(); + + self::assertEquals($result, $suggester->suggestPerson($ticket, 0, 20)); + } +}