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/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..5be9ac0c0 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -93,3 +93,71 @@ 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: + addressees: + type: array + items: + oneOf: + - $ref: '#/components/schemas/UserGroupById' + - $ref: '#/components/schemas/UserById' + + + responses: + 201: + description: "ACCEPTED" + 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 @@ +getAddresseeHistories() as $addressHistory) { + if (null !== $addressHistory->getEndDate()) { + continue; + } + + if (!in_array($addressHistory->getAddressee(), $command->addressees, true)) { + $addressHistory->setEndDate($this->clock->now()); + if (($user = $this->security->getUser()) instanceof User) { + $addressHistory->setRemovedBy($user); + } + } + } + + // 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..e0d474d11 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/SetAddresseesCommand.php @@ -0,0 +1,40 @@ + + */ + #[UserGroupDoNotExclude] + #[GreaterThan(0)] + #[Groups(['read'])] + public array $addressees + ) {} + + public static function fromAddAddresseeCommand(AddAddresseeCommand $command, Ticket $ticket): self + { + return new self([ + $command->addressee, + ...$ticket->getCurrentAddressee(), + ]); + } +} 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/Controller/SetAddresseesController.php b/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php new file mode 100644 index 000000000..9fa6dc366 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/SetAddresseesController.php @@ -0,0 +1,85 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users can set addressees.'); + } + + $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'), + 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..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)] @@ -54,8 +65,11 @@ class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface } else { $this->addresseeGroup = $addressee; } + + $this->ticket->addAddresseeHistory($this); } + #[Serializer\Groups(['read'])] public function getAddressee(): UserGroup|User { if (null !== $this->addresseeGroup) { @@ -94,4 +108,23 @@ 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; + + 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/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/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/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/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/src/config/services.yaml b/src/Bundle/ChillTicketBundle/src/config/services.yaml index 490e215c4..4b662ecdc 100644 --- a/src/Bundle/ChillTicketBundle/src/config/services.yaml +++ b/src/Bundle/ChillTicketBundle/src/config/services.yaml @@ -3,16 +3,21 @@ 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/' - Chill\TicketBundle\DataFixtures\: - resource: '../DataFixtures/' +when@dev: + services: + Chill\TicketBundle\DataFixtures\: + resource: '../DataFixtures/' 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 new file mode 100644 index 000000000..cef1ae153 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetAddressesCommandHandlerTest.php @@ -0,0 +1,122 @@ +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 + { + $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 new file mode 100644 index 000000000..af0d48e87 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Controller/SetAddresseesControllerTest.php @@ -0,0 +1,212 @@ +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'])); + } + + /** + * @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(); + $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 + { + $user = new User(); + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + $security->getUser()->willReturn($user); + + $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(), $security->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; + } +} 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'], + ], ], ]; }