From b434d38091508a6fda2ae1e7abb365975322746f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 23 Apr 2024 17:47:07 +0200 Subject: [PATCH 1/5] Add functionality to set addressees for a ticket This update includes the implementation of methods to add and retrieve addressee history in the Ticket entity, a handler for addressee setting command, denormalizer for transforming request data to SetAddresseesCommand, and corresponding tests. Additionally, it adds a SetAddresseesController for handling addressee related requests and updates the API specifications. --- .../ChillMainBundle/chill.api.specs.yaml | 38 ++++- .../config/services/validator.yaml | 3 + .../ChillTicketBundle/chill.api.specs.yaml | 36 +++++ .../Handler/SetAddresseesCommandHandler.php | 50 ++++++ .../Action/Ticket/SetAddresseesCommand.php | 31 ++++ .../Controller/SetAddresseesController.php | 67 ++++++++ .../src/Entity/AddresseeHistory.php | 9 ++ .../src/Entity/PersonHistory.php | 7 + .../ChillTicketBundle/src/Entity/Ticket.php | 16 ++ .../SetAddresseesCommandDenormalizer.php | 56 +++++++ .../src/config/services.yaml | 6 +- .../SetAddressesCommandHandlerTest.php | 118 ++++++++++++++ .../SetAddresseesControllerTest.php | 146 ++++++++++++++++++ .../tests/Entity/TicketTest.php | 31 ++++ .../SetAddresseesCommandDenormalizerTest.php | 66 ++++++++ 15 files changed, 677 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/SetAddresseesCommand.php create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php create mode 100644 src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/SetAddresseesCommandDenormalizer.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/SetAddresseesCommandDenormalizerTest.php diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index fec36312d..958afab07 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -29,6 +29,42 @@ components: type: string text: type: string + UserById: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - user + UserGroup: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - user_group + label: + type: object + additionalProperties: true + backgroundColor: + type: string + foregroundColor: + type: string + exclusionKey: + type: string + UserGroupById: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - user_group Center: type: object properties: @@ -921,6 +957,6 @@ paths: schema: type: array items: - $ref: '#/components/schemas/NewsItem' + $ref: '#/components/schemas/UserGroup' 403: description: "Unauthorized" diff --git a/src/Bundle/ChillMainBundle/config/services/validator.yaml b/src/Bundle/ChillMainBundle/config/services/validator.yaml index b3b60b9d6..32b8903cc 100644 --- a/src/Bundle/ChillMainBundle/config/services/validator.yaml +++ b/src/Bundle/ChillMainBundle/config/services/validator.yaml @@ -3,6 +3,9 @@ services: autowire: true autoconfigure: true + Chill\MainBundle\Validation\: + resource: '../../Validation' + chill_main.validator_user_circle_consistency: class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator arguments: diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index 86049cc24..232982f01 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -93,3 +93,39 @@ paths: description: "ACCEPTED" 422: description: "UNPROCESSABLE ENTITY" + + /1.0/ticket/{id}/addressees/set: + post: + tags: + - ticket + summary: Set the addresses for an existing ticket (will replace all the existing addresses) + 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: + addresses: + type: array + items: + oneOf: + - $ref: '#/components/schemas/UserGroupById' + - $ref: '#/components/schemas/UserById' + + + responses: + 201: + description: "ACCEPTED" + 422: + description: "UNPROCESSABLE ENTITY" + diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php new file mode 100644 index 000000000..25ac97e5d --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php @@ -0,0 +1,50 @@ +getAddresseeHistories() as $addressHistory) { + if (null !== $addressHistory->getEndDate()) { + continue; + } + + if (!in_array($addressHistory->getAddressee(), $command->addressees, true)) { + $addressHistory->setEndDate($this->clock->now()); + } + } + + // add new addresses + foreach ($command->addressees as $address) { + if (in_array($address, $ticket->getCurrentAddressee(), true)) { + continue; + } + + $history = new AddresseeHistory($address, $this->clock->now(), $ticket); + $this->entityManager->persist($history); + } + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetAddresseesCommand.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetAddresseesCommand.php new file mode 100644 index 000000000..824e2506f --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetAddresseesCommand.php @@ -0,0 +1,31 @@ + + */ + #[UserGroupDoNotExclude] + #[GreaterThan(0)] + #[Groups(['read'])] + public array $addressees + ) {} +} diff --git a/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php b/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php new file mode 100644 index 000000000..add8d480a --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php @@ -0,0 +1,67 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users can set addressees.'); + } + + $command = $this->serializer->deserialize($request->getContent(), SetAddresseesCommand::class, 'json', [AbstractNormalizer::GROUPS => ['read']]); + + if (0 < count($errors = $this->validator->validate($command))) { + return new JsonResponse( + $this->serializer->serialize($errors, 'json'), + Response::HTTP_UNPROCESSABLE_ENTITY, + [], + true + ); + } + + $this->addressesCommandHandler->handle($ticket, $command); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($ticket, 'json', ['groups' => 'read']), + Response::HTTP_OK, + [], + true, + ); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php b/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php index 3939a24c3..b3718a9ca 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php @@ -54,6 +54,8 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface } else { $this->addresseeGroup = $addressee; } + + $this->ticket->addAddresseeHistory($this); } public function getAddressee(): UserGroup|User @@ -94,4 +96,11 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface { return $this->ticket; } + + public function setEndDate(?\DateTimeImmutable $endDate): self + { + $this->endDate = $endDate; + + return $this; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Entity/PersonHistory.php b/src/Bundle/ChillTicketBundle/src/Entity/PersonHistory.php index 57a9809e2..927ea978f 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/PersonHistory.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/PersonHistory.php @@ -84,4 +84,11 @@ class PersonHistory implements TrackCreationInterface { return $this->removedBy; } + + public function setEndDate(?\DateTimeImmutable $endDate): self + { + $this->endDate = $endDate; + + return $this; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php index cd49d7377..04cfe828b 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php @@ -130,6 +130,14 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface $this->motiveHistories->add($motiveHistory); } + /** + * @internal use @see{AddresseHistory::__construct} instead + */ + public function addAddresseeHistory(AddresseeHistory $addresseeHistory): void + { + $this->addresseeHistory->add($addresseeHistory); + } + /** * @return list */ @@ -195,4 +203,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface { return $this->personHistories; } + + /** + * @return ReadableCollection + */ + public function getAddresseeHistories(): ReadableCollection + { + return $this->addresseeHistory; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/SetAddresseesCommandDenormalizer.php b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/SetAddresseesCommandDenormalizer.php new file mode 100644 index 000000000..d97ca5d92 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/SetAddresseesCommandDenormalizer.php @@ -0,0 +1,56 @@ + $this->denormalizer->denormalize($address, UserGroup::class, $format, $context), + 'user' => $this->denormalizer->denormalize($address, User::class, $format, $context), + default => throw new UnexpectedValueException('the type is not set or not supported') + }; + } + + return new SetAddresseesCommand($addresses); + } + + public function supportsDenormalization($data, string $type, ?string $format = null) + { + return SetAddresseesCommand::class === $type && 'json' === $format; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/config/services.yaml b/src/Bundle/ChillTicketBundle/src/config/services.yaml index 490e215c4..006fa5cb6 100644 --- a/src/Bundle/ChillTicketBundle/src/config/services.yaml +++ b/src/Bundle/ChillTicketBundle/src/config/services.yaml @@ -14,5 +14,7 @@ services: Chill\TicketBundle\Serializer\: resource: '../Serializer/' - Chill\TicketBundle\DataFixtures\: - resource: '../DataFixtures/' +when@dev: + services: + Chill\TicketBundle\DataFixtures\: + resource: '../DataFixtures/' diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php new file mode 100644 index 000000000..efb545bac --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php @@ -0,0 +1,118 @@ +prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(function ($arg) use ($user1) { + return $arg instanceof AddresseeHistory && $arg->getAddressee() === $user1; + }))->shouldBeCalledOnce(); + $entityManager->persist(Argument::that(function ($arg) use ($group1) { + return $arg instanceof AddresseeHistory && $arg->getAddressee() === $group1; + }))->shouldBeCalledOnce(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, $command); + + self::assertCount(2, $ticket->getCurrentAddressee()); + } + + public function testHandleExistingUserIsNotRemovedNorCreatingDouble(): void + { + $ticket = new Ticket(); + $user = new User(); + $history = new AddresseeHistory($user, new \DateTimeImmutable('1 month ago'), $ticket); + $command = new SetAddresseesCommand([$user]); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(function ($arg) use ($user) { + return $arg instanceof AddresseeHistory && $arg->getAddressee() === $user; + }))->shouldNotBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, $command); + + self::assertNull($history->getEndDate()); + self::assertCount(1, $ticket->getCurrentAddressee()); + } + + public function testHandleRemoveExistingAddressee(): void + { + $ticket = new Ticket(); + $user = new User(); + $group = new UserGroup(); + $history = new AddresseeHistory($user, new \DateTimeImmutable('1 month ago'), $ticket); + $command = new SetAddresseesCommand([$group]); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(function ($arg) use ($group) { + return $arg instanceof AddresseeHistory && $arg->getAddressee() === $group; + }))->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, $command); + + self::assertNotNull($history->getEndDate()); + self::assertContains($group, $ticket->getCurrentAddressee()); + } + + public function testAddingDoublingAddresseeDoesNotCreateDoubleHistories(): void + { + $ticket = new Ticket(); + $group = new UserGroup(); + $command = new SetAddresseesCommand([$group, $group]); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(function ($arg) use ($group) { + return $arg instanceof AddresseeHistory && $arg->getAddressee() === $group; + }))->shouldBeCalledOnce(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, $command); + + self::assertCount(1, $ticket->getCurrentAddressee()); + } + + private function buildHandler(EntityManagerInterface $entityManager): SetAddresseesCommandHandler + { + return new SetAddresseesCommandHandler(new MockClock(), $entityManager); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php new file mode 100644 index 000000000..ed842f742 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php @@ -0,0 +1,146 @@ +serializer = self::getContainer()->get(SerializerInterface::class); + } + + /** + * @dataProvider getContentData + */ + public function testSetAddresseesWithValidData(array $bodyAsArray): void + { + $controller = $this->buildController(true, true); + $request = new Request(content: json_encode(['addressees' => $bodyAsArray], JSON_THROW_ON_ERROR, 512)); + $ticket = new Ticket(); + + $response = $controller->setAddressees($ticket, $request); + + self::assertEquals(200, $response->getStatusCode()); + + $asArray = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + self::assertIsArray($asArray); + self::assertArrayHasKey('type', $asArray); + self::assertEquals('ticket_ticket', $asArray['type']); + } + + /** + * @dataProvider getContentData + */ + public function testSetAddresseesWithInvalidData(array $bodyAsArray): void + { + $controller = $this->buildController(false, false); + $request = new Request(content: json_encode(['addressees' => $bodyAsArray], JSON_THROW_ON_ERROR, 512)); + $ticket = new Ticket(); + + $response = $controller->setAddressees($ticket, $request); + + self::assertEquals(422, $response->getStatusCode()); + + $asArray = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + self::assertIsArray($asArray); + self::arrayHasKey('violations', $asArray); + self::assertGreaterThan(0, count($asArray['violations'])); + } + + public static function getContentData(): iterable + { + self::bootKernel(); + $entityManager = self::getContainer()->get(EntityManagerInterface::class); + + $userGroup = $entityManager->createQuery('SELECT ug FROM '.UserGroup::class.' ug ') + ->setMaxResults(1)->getOneOrNullResult(); + + if (null === $userGroup) { + throw new \RuntimeException('User group not existing in database'); + } + + $user = $entityManager->createQuery('SELECT u FROM '.User::class.' u') + ->setMaxResults(1)->getOneOrNullResult(); + + if (null === $user) { + throw new \RuntimeException('User not existing in database'); + } + + self::ensureKernelShutdown(); + + yield [[['type' => 'user', 'id' => $user->getId()]]]; + yield [[['type' => 'user', 'id' => $user->getId()], ['type' => 'user_group', 'id' => $userGroup->getId()]]]; + yield [[['type' => 'user_group', 'id' => $userGroup->getId()]]]; + } + + private function buildController(bool $willSave, bool $isValid): SetAddresseesController + { + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + + if ($willSave) { + $entityManager->flush()->shouldBeCalled(); + $entityManager->persist(Argument::type(AddresseeHistory::class))->shouldBeCalled(); + } + + $validator = $this->prophesize(ValidatorInterface::class); + + if ($isValid) { + $validator->validate(Argument::type(SetAddresseesCommand::class))->willReturn(new ConstraintViolationList([])); + } else { + $validator->validate(Argument::type(SetAddresseesCommand::class))->willReturn( + new ConstraintViolationList([ + new ConstraintViolation('Fake constraint', 'fake message template', [], [], 'addresses', []), + ]) + ); + } + + return new SetAddresseesController( + $security->reveal(), + $entityManager->reveal(), + $this->serializer, + new SetAddresseesCommandHandler(new MockClock(), $entityManager->reveal()), + $validator->reveal() + ); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php index 6f710f1aa..f0efe5493 100644 --- a/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php @@ -11,7 +11,10 @@ declare(strict_types=1); namespace Chill\TicketBundle\Tests\Entity; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\PersonBundle\Entity\Person; +use Chill\TicketBundle\Entity\AddresseeHistory; use Chill\TicketBundle\Entity\Motive; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\PersonHistory; @@ -57,5 +60,33 @@ class TicketTest extends KernelTestCase self::assertCount(1, $ticket->getPersons()); self::assertSame($person, $ticket->getPersons()[0]); + + $history->setEndDate(new \DateTimeImmutable('now')); + + self::assertCount(0, $ticket->getPersons()); + } + + public function testGetAddresse(): void + { + $ticket = new Ticket(); + $user = new User(); + $group = new UserGroup(); + + self::assertEquals([], $ticket->getCurrentAddressee()); + + $history = new AddresseeHistory($user, new \DateTimeImmutable('now'), $ticket); + + self::assertCount(1, $ticket->getCurrentAddressee()); + self::assertSame($user, $ticket->getCurrentAddressee()[0]); + + $history2 = new AddresseeHistory($group, new \DateTimeImmutable('now'), $ticket); + + self::assertCount(2, $ticket->getCurrentAddressee()); + self::assertContains($group, $ticket->getCurrentAddressee()); + + $history->setEndDate(new \DateTimeImmutable('now')); + + self::assertCount(1, $ticket->getCurrentAddressee()); + self::assertSame($group, $ticket->getCurrentAddressee()[0]); } } diff --git a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/SetAddresseesCommandDenormalizerTest.php b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/SetAddresseesCommandDenormalizerTest.php new file mode 100644 index 000000000..a4b9bbf49 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/SetAddresseesCommandDenormalizerTest.php @@ -0,0 +1,66 @@ +supportsDenormalization('', SetAddresseesCommand::class, 'json')); + self::assertFalse($denormalizer->supportsDenormalization('', stdClass::class, 'json')); + } + + public function testDenormalize() + { + $denormalizer = $this->buildDenormalizer(); + + $actual = $denormalizer->denormalize(['addressees' => [['type' => 'user'], ['type' => 'user_group']]], SetAddresseesCommand::class, 'json'); + + self::assertInstanceOf(SetAddresseesCommand::class, $actual); + self::assertIsArray($actual->addressees); + self::assertCount(2, $actual->addressees); + self::assertInstanceOf(User::class, $actual->addressees[0]); + self::assertInstanceOf(UserGroup::class, $actual->addressees[1]); + } + + private function buildDenormalizer(): SetAddresseesCommandDenormalizer + { + $normalizer = $this->prophesize(DenormalizerInterface::class); + $normalizer->denormalize(Argument::any(), User::class, 'json', Argument::any()) + ->willReturn(new User()); + $normalizer->denormalize(Argument::any(), UserGroup::class, 'json', Argument::any()) + ->willReturn(new UserGroup()); + + $denormalizer = new SetAddresseesCommandDenormalizer(); + $denormalizer->setDenormalizer($normalizer->reveal()); + + return $denormalizer; + } +} From fa6783569045615b3a0487972adf08c1d4caf095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 23 Apr 2024 23:00:12 +0200 Subject: [PATCH 2/5] Add functionality to add single addressee to tickets This update introduces a new feature allowing end-users to add a single addressee to a ticket without removing the existing ones. This was achieved by adding a new API endpoint and updating the SetAddresseesController to handle the addition of a single addressee. Accompanying tests have also been provided to ensure the new feature works as expected. --- .../ChillTicketBundle/chill.api.specs.yaml | 34 +++++++++- .../src/Action/Ticket/AddAddresseeCommand.php | 29 +++++++++ .../Action/Ticket/SetAddresseesCommand.php | 9 +++ .../Controller/SetAddresseesController.php | 26 ++++++-- .../SetAddresseesControllerTest.php | 64 +++++++++++++++++++ 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Action/Ticket/AddAddresseeCommand.php diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index 232982f01..5be9ac0c0 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -115,7 +115,7 @@ paths: schema: type: object properties: - addresses: + addressees: type: array items: oneOf: @@ -129,3 +129,35 @@ paths: 422: description: "UNPROCESSABLE ENTITY" + /1.0/ticket/{id}/addressee/add: + post: + tags: + - ticket + summary: Add an addressee to a ticket, without removing existing ones. + 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: + addressee: + oneOf: + - $ref: '#/components/schemas/UserGroupById' + - $ref: '#/components/schemas/UserById' + + + responses: + 201: + description: "ACCEPTED" + 422: + description: "UNPROCESSABLE ENTITY" diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/AddAddresseeCommand.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/AddAddresseeCommand.php new file mode 100644 index 000000000..d7f9c545f --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/AddAddresseeCommand.php @@ -0,0 +1,29 @@ +addressee, + ...$ticket->getCurrentAddressee(), + ]); + } } diff --git a/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php b/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php index add8d480a..9fa6dc366 100644 --- a/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php +++ b/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\TicketBundle\Controller; +use Chill\TicketBundle\Action\Ticket\AddAddresseeCommand; use Chill\TicketBundle\Action\Ticket\Handler\SetAddresseesCommandHandler; use Chill\TicketBundle\Action\Ticket\SetAddresseesCommand; use Chill\TicketBundle\Entity\Ticket; @@ -28,11 +29,11 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; final readonly class SetAddresseesController { public function __construct( - private Security $security, - private EntityManagerInterface $entityManager, - private SerializerInterface $serializer, + private Security $security, + private EntityManagerInterface $entityManager, + private SerializerInterface $serializer, private SetAddresseesCommandHandler $addressesCommandHandler, - private ValidatorInterface $validator, + private ValidatorInterface $validator, ) {} #[Route('/api/1.0/ticket/{id}/addressees/set', methods: ['POST'])] @@ -44,6 +45,23 @@ final readonly class SetAddresseesController $command = $this->serializer->deserialize($request->getContent(), SetAddresseesCommand::class, 'json', [AbstractNormalizer::GROUPS => ['read']]); + return $this->registerSetAddressees($command, $ticket); + } + + #[Route('/api/1.0/ticket/{id}/addressee/add', methods: ['POST'])] + public function addAddressee(Ticket $ticket, Request $request): Response + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users can add addressees.'); + } + + $command = $this->serializer->deserialize($request->getContent(), AddAddresseeCommand::class, 'json', [AbstractNormalizer::GROUPS => ['read']]); + + return $this->registerSetAddressees(SetAddresseesCommand::fromAddAddresseeCommand($command, $ticket), $ticket); + } + + private function registerSetAddressees(SetAddresseesCommand $command, Ticket $ticket): Response + { if (0 < count($errors = $this->validator->validate($command))) { return new JsonResponse( $this->serializer->serialize($errors, 'json'), diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php index ed842f742..a7ca214c1 100644 --- a/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php @@ -85,6 +85,70 @@ class SetAddresseesControllerTest extends KernelTestCase self::assertGreaterThan(0, count($asArray['violations'])); } + /** + * @dataProvider getContentDataUnique + */ + public function testAddAddresseeWithValidData(array $bodyAsArray): void + { + $controller = $this->buildController(true, true); + $request = new Request(content: json_encode(['addressee' => $bodyAsArray], JSON_THROW_ON_ERROR, 512)); + $ticket = new Ticket(); + + $response = $controller->addAddressee($ticket, $request); + + self::assertEquals(200, $response->getStatusCode()); + + $asArray = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + self::assertIsArray($asArray); + self::assertArrayHasKey('type', $asArray); + self::assertEquals('ticket_ticket', $asArray['type']); + } + + /** + * @throws \JsonException + * + * @dataProvider getContentDataUnique + */ + public function testAddAddresseeWithInvalidData(array $bodyAsArray): void + { + $controller = $this->buildController(false, false); + $request = new Request(content: json_encode(['addressee' => $bodyAsArray], JSON_THROW_ON_ERROR, 512)); + $ticket = new Ticket(); + + $response = $controller->addAddressee($ticket, $request); + + self::assertEquals(422, $response->getStatusCode()); + + $asArray = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + self::assertIsArray($asArray); + self::arrayHasKey('violations', $asArray); + self::assertGreaterThan(0, count($asArray['violations'])); + } + + public static function getContentDataUnique(): iterable + { + $entityManager = self::getContainer()->get(EntityManagerInterface::class); + + $userGroup = $entityManager->createQuery('SELECT ug FROM '.UserGroup::class.' ug ') + ->setMaxResults(1)->getOneOrNullResult(); + + if (null === $userGroup) { + throw new \RuntimeException('User group not existing in database'); + } + + $user = $entityManager->createQuery('SELECT u FROM '.User::class.' u') + ->setMaxResults(1)->getOneOrNullResult(); + + if (null === $user) { + throw new \RuntimeException('User not existing in database'); + } + + self::ensureKernelShutdown(); + + yield [['type' => 'user', 'id' => $user->getId()]]; + yield [['type' => 'user_group', 'id' => $userGroup->getId()]]; + } + public static function getContentData(): iterable { self::bootKernel(); From ed45f14a45538bba59deeabc810efd5b4ce73309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 23 Apr 2024 23:38:34 +0200 Subject: [PATCH 3/5] Add tracking of addressee history in ticket system The updates introduce tracking for the history of addressees in the ticket system, both when added and when removed. The user who removed an addressee is now recorded. The changes also ensure these updated aspects are correctly normalized and users can see them in the ticket history. A new database migration file was created for the changes. --- .../Handler/SetAddresseesCommandHandler.php | 6 +++ .../src/migrations/Version20240423212824.php | 40 +++++++++++++++++++ .../SetAddressesCommandHandlerTest.php | 6 ++- .../SetAddresseesControllerTest.php | 4 +- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/migrations/Version20240423212824.php diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php index 25ac97e5d..b2805ed2f 100644 --- a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetAddresseesCommandHandler.php @@ -11,17 +11,20 @@ declare(strict_types=1); namespace Chill\TicketBundle\Action\Ticket\Handler; +use Chill\MainBundle\Entity\User; use Chill\TicketBundle\Action\Ticket\SetAddresseesCommand; use Chill\TicketBundle\Entity\AddresseeHistory; use Chill\TicketBundle\Entity\Ticket; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\Security\Core\Security; final readonly class SetAddresseesCommandHandler { public function __construct( private ClockInterface $clock, private EntityManagerInterface $entityManager, + private Security $security, ) {} public function handle(Ticket $ticket, SetAddresseesCommand $command): void @@ -34,6 +37,9 @@ final readonly class SetAddresseesCommandHandler if (!in_array($addressHistory->getAddressee(), $command->addressees, true)) { $addressHistory->setEndDate($this->clock->now()); + if (($user = $this->security->getUser()) instanceof User) { + $addressHistory->setRemovedBy($user); + } } } diff --git a/src/Bundle/ChillTicketBundle/src/migrations/Version20240423212824.php b/src/Bundle/ChillTicketBundle/src/migrations/Version20240423212824.php new file mode 100644 index 000000000..3c025d763 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/migrations/Version20240423212824.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE chill_ticket.addressee_history ADD endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT null'); + $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD removedBy_id INT DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN chill_ticket.addressee_history.endDate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBDB8346CCF FOREIGN KEY (removedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_434EBDBDB8346CCF ON chill_ticket.addressee_history (removedBy_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBDB8346CCF'); + $this->addSql('DROP INDEX chill_ticket.IDX_434EBDBDB8346CCF'); + $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP endDate'); + $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP removedBy_id'); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php index efb545bac..cef1ae153 100644 --- a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php @@ -22,6 +22,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Clock\MockClock; +use Symfony\Component\Security\Core\Security; /** * @internal @@ -113,6 +114,9 @@ final class SetAddressesCommandHandlerTest extends TestCase private function buildHandler(EntityManagerInterface $entityManager): SetAddresseesCommandHandler { - return new SetAddresseesCommandHandler(new MockClock(), $entityManager); + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn(new User()); + + return new SetAddresseesCommandHandler(new MockClock(), $entityManager, $security->reveal()); } } diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php index a7ca214c1..af0d48e87 100644 --- a/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php @@ -177,8 +177,10 @@ class SetAddresseesControllerTest extends KernelTestCase private function buildController(bool $willSave, bool $isValid): SetAddresseesController { + $user = new User(); $security = $this->prophesize(Security::class); $security->isGranted('ROLE_USER')->willReturn(true); + $security->getUser()->willReturn($user); $entityManager = $this->prophesize(EntityManagerInterface::class); @@ -203,7 +205,7 @@ class SetAddresseesControllerTest extends KernelTestCase $security->reveal(), $entityManager->reveal(), $this->serializer, - new SetAddresseesCommandHandler(new MockClock(), $entityManager->reveal()), + new SetAddresseesCommandHandler(new MockClock(), $entityManager->reveal(), $security->reveal()), $validator->reveal() ); } From 45828174d1156c45a86cd16fcdb4f2d5639cce16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 23 Apr 2024 23:39:01 +0200 Subject: [PATCH 4/5] Add addressee history to ticket serialization This update extends the tickets serialization and normalisation process to include addressee history. With the changes, AddresseeHistory class now also keeps track of who removed an addressee. Additional types, tests and interfaces have been introduced to support this change. --- .../ChillMainBundle/Resources/public/types.ts | 2 ++ .../src/Entity/AddresseeHistory.php | 24 +++++++++++++ .../src/Resources/public/types.ts | 35 +++++++++++++++---- .../Normalizer/TicketNormalizer.php | 19 ++++++++++ .../Normalizer/TicketNormalizerTest.php | 18 ++++++++-- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 840a0e939..ada3089a0 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -51,6 +51,8 @@ export interface UserGroup { excludeKey: string, } +export type UserGroupOrUser = User | UserGroup; + export interface UserAssociatedInterface { type: "user"; id: number; diff --git a/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php b/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php index b3718a9ca..2717df2a5 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php @@ -18,9 +18,11 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation as Serializer; #[ORM\Entity()] #[ORM\Table(name: 'addressee_history', schema: 'chill_ticket')] +#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_addressee_history' => AddresseeHistory::class])] class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface { use TrackCreationTrait; @@ -29,6 +31,7 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface #[ORM\Id] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Serializer\Groups(['read'])] private ?int $id = null; #[ORM\ManyToOne(targetEntity: User::class)] @@ -39,11 +42,19 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface #[ORM\JoinColumn(nullable: true)] private ?UserGroup $addresseeGroup = 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: User::class)] + #[ORM\JoinColumn(nullable: true)] + #[Serializer\Groups(['read'])] + private ?User $removedBy = null; + public function __construct( User|UserGroup $addressee, #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)] + #[Serializer\Groups(['read'])] private \DateTimeImmutable $startDate, #[ORM\ManyToOne(targetEntity: Ticket::class)] #[ORM\JoinColumn(nullable: false)] @@ -58,6 +69,7 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface $this->ticket->addAddresseeHistory($this); } + #[Serializer\Groups(['read'])] public function getAddressee(): UserGroup|User { if (null !== $this->addresseeGroup) { @@ -97,6 +109,18 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface return $this->ticket; } + public function getRemovedBy(): ?User + { + return $this->removedBy; + } + + public function setRemovedBy(?User $removedBy): self + { + $this->removedBy = $removedBy; + + return $this; + } + public function setEndDate(?\DateTimeImmutable $endDate): self { $this->endDate = $endDate; diff --git a/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts b/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts index 2df53da99..0bcd1ecbb 100644 --- a/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts +++ b/src/Bundle/ChillTicketBundle/src/Resources/public/types.ts @@ -1,4 +1,10 @@ -import {DateTime, TranslatableString, User} from "../../../../ChillMainBundle/Resources/public/types"; +import { + DateTime, + TranslatableString, + User, + UserGroup, + UserGroupOrUser +} from "../../../../ChillMainBundle/Resources/public/types"; import {Person} from "../../../../ChillPersonBundle/Resources/public/types"; export interface Motive { @@ -46,18 +52,33 @@ interface Comment { updatedAt: DateTime|null, } +interface AddresseeHistory { + type: "ticket_addressee_history", + id: number, + startDate: DateTime|null, + addressee: UserGroupOrUser, + endDate: DateTime|null, + removedBy: User|null, + createdBy: User|null, + createdAt: DateTime|null, + updatedBy: User|null, + updatedAt: DateTime|null, +} + interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {}; interface AddCommentEvent extends TicketHistory<"add_comment", Comment> {}; interface SetMotiveEvent extends TicketHistory<"set_motive", MotiveHistory> {}; +interface AddAddressee extends TicketHistory<"add_addressee", AddresseeHistory> {}; -type TicketHistoryLine = AddPersonEvent | AddCommentEvent | SetMotiveEvent; +type TicketHistoryLine = AddPersonEvent | AddCommentEvent | SetMotiveEvent | AddAddressee; export interface Ticket { - type: "ticket_ticket" - id: number - externalRef: string - currentPersons: Person[] - currentMotive: null|Motive + type: "ticket_ticket", + id: number, + externalRef: string, + currentAddressees: UserGroupOrUser[], + currentPersons: Person[], + currentMotive: null|Motive, history: TicketHistoryLine[], } diff --git a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php index c68ab34ef..f7aff602b 100644 --- a/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php +++ b/src/Bundle/ChillTicketBundle/src/Serializer/Normalizer/TicketNormalizer.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\TicketBundle\Serializer\Normalizer; +use Chill\TicketBundle\Entity\AddresseeHistory; use Chill\TicketBundle\Entity\Comment; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\PersonHistory; @@ -79,6 +80,24 @@ final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInte ], $ticket->getComments()->toArray(), ), + ...array_map( + fn (AddresseeHistory $history) => [ + 'event_type' => 'add_addressee', + 'at' => $history->getStartDate(), + 'by' => $history->getCreatedBy(), + 'data' => $history, + ], + $ticket->getAddresseeHistories()->toArray(), + ), + ...array_map( + fn (AddresseeHistory $history) => [ + 'event_type' => 'remove_addressee', + 'at' => $history->getStartDate(), + 'by' => $history->getRemovedBy(), + 'data' => $history, + ], + $ticket->getAddresseeHistories()->filter(fn (AddresseeHistory $history) => null !== $history->getEndDate())->toArray() + ), ]; usort( diff --git a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php index 447bebfbf..415fc15fb 100644 --- a/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Serializer/Normalizer/TicketNormalizerTest.php @@ -12,7 +12,9 @@ declare(strict_types=1); namespace Chill\TicketBundle\Tests\Serializer\Normalizer; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\PersonBundle\Entity\Person; +use Chill\TicketBundle\Entity\AddresseeHistory; use Chill\TicketBundle\Entity\Comment; use Chill\TicketBundle\Entity\Motive; use Chill\TicketBundle\Entity\MotiveHistory; @@ -108,6 +110,8 @@ class TicketNormalizerTest extends KernelTestCase ->willReturn(['motiveHistory']); $normalizer->normalize(Argument::type(Comment::class), 'json', Argument::type('array')) ->willReturn(['comment']); + $normalizer->normalize(Argument::type(AddresseeHistory::class), 'json', Argument::type('array')) + ->willReturn(['addresseeHistory']); // null values $normalizer->normalize(null, 'json', Argument::type('array'))->willReturn(null); @@ -142,6 +146,9 @@ class TicketNormalizerTest extends KernelTestCase $comment = new Comment('blabla test', $ticket); $comment->setCreatedAt(new \DateTimeImmutable('2024-04-01T12:04:00')); $comment->setCreatedBy(new User()); + $addresseeHistory = new AddresseeHistory(new User(), new \DateTimeImmutable('2024-04-01T12:05:00'), $ticket); + $addresseeHistory->setEndDate(new \DateTimeImmutable('2024-04-01T12:06:00')); + new AddresseeHistory(new UserGroup(), new \DateTimeImmutable('2024-04-01T12:07:00'), $ticket); yield [ $ticket, @@ -150,10 +157,17 @@ class TicketNormalizerTest extends KernelTestCase 'id' => null, 'externalRef' => '2134', 'currentPersons' => ['embedded'], - 'currentAddressees' => [], + 'currentAddressees' => ['embedded'], 'currentInputs' => [], 'currentMotive' => ['type' => 'motive', 'id' => 0], - 'history' => [['event_type' => 'add_person'], ['event_type' => 'set_motive'], ['event_type' => 'add_comment']], + 'history' => [ + ['event_type' => 'add_person'], + ['event_type' => 'set_motive'], + ['event_type' => 'add_comment'], + ['event_type' => 'add_addressee'], + ['event_type' => 'remove_addressee'], + ['event_type' => 'add_addressee'], + ], ], ]; } From 2d8b960d9e59730f3874787231b8a7731726018c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 24 Apr 2024 18:48:00 +0200 Subject: [PATCH 5/5] Re-open the same ticket if a ticket already exists with the same externalRef, instead of creating a new one --- .../src/Controller/CreateTicketController.php | 12 +++- .../src/Repository/TicketRepository.php | 56 +++++++++++++++++++ .../Repository/TicketRepositoryInterface.php | 23 ++++++++ .../src/config/services.yaml | 7 ++- 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Repository/TicketRepository.php create mode 100644 src/Bundle/ChillTicketBundle/src/Repository/TicketRepositoryInterface.php diff --git a/src/Bundle/ChillTicketBundle/src/Controller/CreateTicketController.php b/src/Bundle/ChillTicketBundle/src/Controller/CreateTicketController.php index 8a6d8dd32..a3bdf31c8 100644 --- a/src/Bundle/ChillTicketBundle/src/Controller/CreateTicketController.php +++ b/src/Bundle/ChillTicketBundle/src/Controller/CreateTicketController.php @@ -15,6 +15,7 @@ use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand; use Chill\TicketBundle\Action\Ticket\Handler\AssociateByPhonenumberCommandHandler; use Chill\TicketBundle\Action\Ticket\CreateTicketCommand; use Chill\TicketBundle\Action\Ticket\Handler\CreateTicketCommandHandler; +use Chill\TicketBundle\Repository\TicketRepositoryInterface; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -31,7 +32,8 @@ final readonly class CreateTicketController private AssociateByPhonenumberCommandHandler $associateByPhonenumberCommandHandler, private Security $security, private UrlGeneratorInterface $urlGenerator, - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, + private TicketRepositoryInterface $ticketRepository, ) {} #[Route('{_locale}/ticket/ticket/create')] @@ -41,6 +43,14 @@ final readonly class CreateTicketController throw new AccessDeniedHttpException('Only users are allowed to create tickets.'); } + if ('' !== $extId = $request->query->get('extId', '')) { + if (null !== $ticket = $this->ticketRepository->findOneByExternalRef($extId)) { + return new RedirectResponse( + $this->urlGenerator->generate('chill_ticket_ticket_edit', ['id' => $ticket->getId()]) + ); + } + } + $createCommand = new CreateTicketCommand($request->query->get('extId', '')); $ticket = $this->createTicketCommandHandler->__invoke($createCommand); diff --git a/src/Bundle/ChillTicketBundle/src/Repository/TicketRepository.php b/src/Bundle/ChillTicketBundle/src/Repository/TicketRepository.php new file mode 100644 index 000000000..d4301585a --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Repository/TicketRepository.php @@ -0,0 +1,56 @@ +repository = $objectManager->getRepository($this->getClassName()); + } + + public function find($id): ?Ticket + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?Ticket + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName() + { + return Ticket::class; + } + + public function findOneByExternalRef(string $extId): ?Ticket + { + return $this->repository->findOneBy(['externalRef' => $extId]); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Repository/TicketRepositoryInterface.php b/src/Bundle/ChillTicketBundle/src/Repository/TicketRepositoryInterface.php new file mode 100644 index 000000000..0b2cb4d66 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Repository/TicketRepositoryInterface.php @@ -0,0 +1,23 @@ + + */ +interface TicketRepositoryInterface extends ObjectRepository +{ + public function findOneByExternalRef(string $extId): ?Ticket; +} diff --git a/src/Bundle/ChillTicketBundle/src/config/services.yaml b/src/Bundle/ChillTicketBundle/src/config/services.yaml index 006fa5cb6..4b662ecdc 100644 --- a/src/Bundle/ChillTicketBundle/src/config/services.yaml +++ b/src/Bundle/ChillTicketBundle/src/config/services.yaml @@ -3,13 +3,16 @@ services: autoconfigure: true autowire: true + Chill\TicketBundle\Action\Ticket\Handler\: + resource: '../Action/Ticket/Handler/' + Chill\TicketBundle\Controller\: resource: '../Controller/' tags: - controller.service_arguments - Chill\TicketBundle\Action\Ticket\Handler\: - resource: '../Action/Ticket/Handler/' + Chill\TicketBundle\Repository\: + resource: '../Repository/' Chill\TicketBundle\Serializer\: resource: '../Serializer/'