Créer un point d'api de suggestion des usagers pour un ticket

This commit is contained in:
Julien Fastré 2025-07-01 12:38:02 +00:00
parent 0566ab0910
commit 4f93150874
27 changed files with 1485 additions and 26 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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<ThirdParty> 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.
*

View File

@ -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'

View File

@ -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);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket\Handler;
use Chill\TicketBundle\Action\Ticket\SetCallerCommand;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use Symfony\Component\Clock\ClockInterface;
/**
* Handler for setting the caller of a ticket.
*/
class SetCallerCommandHandler
{
public function __construct(private readonly ClockInterface $clock) {}
public function __invoke(Ticket $ticket, SetCallerCommand $command): Ticket
{
// If the ticket already has the requested caller, return it without changes
$currentCaller = $ticket->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;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
/**
* Command to set the caller of a ticket.
* The caller can be either a Person or a ThirdParty.
*/
final readonly class SetCallerCommand
{
/**
* @param Person|ThirdParty|null $caller The caller to associate with the ticket
*/
public function __construct(
public Person|ThirdParty|null $caller,
) {}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use Chill\TicketBundle\Action\Ticket\SetCallerCommand;
use Chill\TicketBundle\Action\Ticket\Handler\SetCallerCommandHandler;
use Chill\TicketBundle\Entity\Ticket;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Controller for setting the caller of a ticket.
*/
final readonly class SetCallerApiController
{
public function __construct(
private SetCallerCommandHandler $setCallerCommandHandler,
private Security $security,
private EntityManagerInterface $entityManager,
private SerializerInterface $serializer,
) {}
#[Route('/api/1.0/ticket/ticket/{id}/set-caller', name: 'chill_ticket_ticket_set_caller_api', requirements: ['id' => '\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
);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Service\Ticket\SuggestPersonForTicketInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
final readonly class SuggestPersonForTicketApiController
{
public function __construct(
private SuggestPersonForTicketInterface $suggestPersonForTicket,
private SerializerInterface $serializer,
private Security $security,
) {}
#[Route('/api/1.0/ticket/ticket/{id}/suggest-person', name: 'chill_ticket_ticket_suggest_person_api', requirements: ['id' => '\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,
);
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* Represents the history of a caller associated with a ticket.
*
* This entity is used to track the changes in a ticket's caller over time.
* The caller can be either a Person or a ThirdParty.
* Implements the TrackCreationInterface for tracking entity lifecycle creation.
*/
#[ORM\Entity]
#[ORM\Table(name: 'caller_history', schema: 'chill_ticket')]
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_caller_history' => 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;
}
}

View File

@ -96,6 +96,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface
#[ORM\OneToMany(targetEntity: EmergencyStatusHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])]
private Collection $emergencyStatusHistories;
/**
* @var Collection<int, CallerHistory>
*/
#[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<int, CallerHistory>
*/
public function getCallerHistories(): ReadableCollection
{
return $this->callerHistories;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Repository;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
final readonly class PersonTicketACLAwareRepository implements PersonTicketACLAwareRepositoryInterface
{
public function __construct(private EntityManagerInterface $em) {}
public function findPersonPreviouslyAssociatedWithCaller(Person|ThirdParty $caller, int $start = 0, int $limit = 100): array
{
$resultSetMappingBuilder = new ResultSetMappingBuilder($this->em);
$resultSetMappingBuilder->addRootEntityFromClassMetadata(Person::class, 'p');
$callerClause = match ($caller instanceof Person) {
true => 'caller_history.person_id = :callerId',
false => 'caller_history.thirdparty_id = :callerId',
};
$query = <<<SQL
SELECT DISTINCT {$resultSetMappingBuilder->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();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Repository;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
/**
* Interface for repository operations related to accessing and managing
* person tickets within an ACL-aware context.
*/
interface PersonTicketACLAwareRepositoryInterface
{
/**
* Find all the Person entity that were previously associated with a ticket with the same caller.
*
* @return list<Person>
*/
public function findPersonPreviouslyAssociatedWithCaller(Person|ThirdParty $caller, int $start = 0, int $limit = 100): array;
}

View File

@ -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()) {

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Service\Ticket;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Repository\PersonTicketACLAwareRepositoryInterface;
final readonly class SuggestPersonForTicket implements SuggestPersonForTicketInterface
{
public function __construct(private PersonTicketACLAwareRepositoryInterface $personTicketACLAwareRepository) {}
public function suggestPerson(Ticket $ticket, int $start = 0, int $limit = 20): array
{
$caller = $ticket->getCaller();
if (null === $caller) {
return [];
}
return $this->personTicketACLAwareRepository->findPersonPreviouslyAssociatedWithCaller($caller, $start, $limit);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Service\Ticket;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Entity\Ticket;
/**
* Suggest Person entities for a ticket.
*/
interface SuggestPersonForTicketInterface
{
/**
* @return list<Person>
*/
public function suggestPerson(Ticket $ticket, int $start = 0, int $limit = 20): array;
}

View File

@ -17,6 +17,9 @@ services:
Chill\TicketBundle\Serializer\:
resource: '../Serializer/'
Chill\TicketBundle\Service\:
resource: '../Service/'
Chill\TicketBundle\Menu\:
resource: '../Menu/'

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Ticket;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250624105842 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add CallerHistory entity to associate a ticket with either a Person or a ThirdParty entity';
}
public function up(Schema $schema): void
{
$this->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);
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Action\Ticket\Handler\SetCallerCommandHandler;
use Chill\TicketBundle\Action\Ticket\SetCallerCommand;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
/**
* @internal
*
* @coversNothing
*/
class SetCallerCommandHandlerTest extends TestCase
{
use ProphecyTrait;
private \DateTimeImmutable $now;
private ClockInterface $clock;
private SetCallerCommandHandler $handler;
protected function setUp(): void
{
$this->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());
}
}

View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Tests\Controller;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Action\Ticket\Handler\SetCallerCommandHandler;
use Chill\TicketBundle\Action\Ticket\SetCallerCommand;
use Chill\TicketBundle\Controller\SetCallerApiController;
use Chill\TicketBundle\Entity\Ticket;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @internal
*
* @coversNothing
*/
final class SetCallerApiControllerTest extends TestCase
{
use ProphecyTrait;
public function testSetCallerWithoutPermission(): void
{
$ticket = new Ticket();
$request = new Request();
$security = $this->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);
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Tests\Entity;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class CallerHistoryTest extends TestCase
{
public function testConstructorWithPerson(): void
{
$ticket = new Ticket();
$callerHistory = new CallerHistory($person = new Person(), $ticket);
self::assertSame($ticket, $callerHistory->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());
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Tests\Entity;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class TicketCallerTest extends TestCase
{
public function testGetCaller(): void
{
$ticket = new Ticket();
// Initially, there should be no caller
self::assertNull($ticket->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());
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Repository;
use Chill\PersonBundle\DataFixtures\Helper\RandomPersonHelperTrait;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Repository\PersonTicketACLAwareRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class PersonTicketACLAwareRepositoryTest extends KernelTestCase
{
use RandomPersonHelperTrait;
private PersonTicketACLAwareRepository $repository;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$this->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);
}
}

View File

@ -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

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Service\Ticket;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Repository\PersonTicketACLAwareRepositoryInterface;
use Chill\TicketBundle\Service\Ticket\SuggestPersonForTicket;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
class SuggestPersonForTicketTest extends TestCase
{
use ProphecyTrait;
private \Prophecy\Prophecy\ObjectProphecy $repository;
protected function setUp(): void
{
$this->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));
}
}