diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index e15b57ba5..ce53732ff 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -13,6 +13,18 @@ components: fr: Retard de livraison active: type: boolean + MotiveById: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - ticket_motive + required: + - id + - type paths: /1.0/ticket/motive.json: @@ -23,3 +35,32 @@ paths: responses: 200: description: "OK" + + /1.0/ticket/{id}/motive/set: + post: + tags: + - ticket + summary: Replace the existing ticket's motive by a new one + 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: + motive: + $ref: "#/components/schemas/MotiveById" + responses: + 201: + description: "ACCEPTED" + 422: + description: "UNPROCESSABLE ENTITY" diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php new file mode 100644 index 000000000..7363d5e73 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php @@ -0,0 +1,56 @@ +motive) { + throw new \InvalidArgumentException('The new motive cannot be null'); + } + + // will add if there are no existing motive + $readyToAdd = 0 === count($ticket->getMotiveHistories()); + + foreach ($ticket->getMotiveHistories() as $history) { + if (null !== $history->getEndDate()) { + continue; + } + + if ($history->getMotive() === $command->motive) { + // we apply the same motive, we do nothing + continue; + } + + $history->setEndDate($this->clock->now()); + $readyToAdd = true; + } + + if ($readyToAdd) { + $history = new MotiveHistory($command->motive, $ticket, $this->clock->now()); + $ticket->addMotiveHistory($history); + $this->entityManager->persist($history); + } + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/ReplaceMotiveCommand.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/ReplaceMotiveCommand.php new file mode 100644 index 000000000..75e2a3f1b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/ReplaceMotiveCommand.php @@ -0,0 +1,25 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(''); + } + + $command = $this->serializer->deserialize($request->getContent(), ReplaceMotiveCommand::class, 'json', [ + AbstractNormalizer::GROUPS => ['write'], + ]); + + $errors = $this->validator->validate($command); + + if (0 < $errors->count()) { + return new JsonResponse( + $this->serializer->serialize($errors, 'json'), + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + $this->replaceMotiveCommandHandler->handle($ticket, $command); + + $this->entityManager->flush(); + + return new JsonResponse(null, Response::HTTP_CREATED); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php index f74a55a3f..3551aa93d 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php @@ -114,6 +114,11 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface $this->personHistories->add($personHistory); } + public function addMotiveHistory(MotiveHistory $motiveHistory): void + { + $this->motiveHistories->add($motiveHistory); + } + /** * @return list */ @@ -163,4 +168,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface return null; } + + /** + * @return ReadableCollection + */ + public function getMotiveHistories(): ReadableCollection + { + return $this->motiveHistories; + } } diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php new file mode 100644 index 000000000..ef86dad17 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php @@ -0,0 +1,108 @@ +prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(static function ($arg) use ($motive): bool { + if (!$arg instanceof MotiveHistory) { + return false; + } + + return $arg->getMotive() === $motive; + }))->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, new ReplaceMotiveCommand($motive)); + + self::assertSame($motive, $ticket->getMotive()); + } + + public function testHandleReplaceMotiveOnTicketWithExistingMotive(): void + { + $motive = new Motive(); + $ticket = new Ticket(); + $ticket->addMotiveHistory(new MotiveHistory(new Motive(), $ticket)); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(static function ($arg) use ($motive): bool { + if (!$arg instanceof MotiveHistory) { + return false; + } + + return $arg->getMotive() === $motive; + }))->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, new ReplaceMotiveCommand($motive)); + + self::assertSame($motive, $ticket->getMotive()); + self::assertCount(2, $ticket->getMotiveHistories()); + } + + public function testHandleReplaceMotiveOnTicketWithSameMotive(): void + { + $motive = new Motive(); + $ticket = new Ticket(); + $ticket->addMotiveHistory(new MotiveHistory($motive, $ticket)); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::that(static function ($arg) use ($motive): bool { + if (!$arg instanceof MotiveHistory) { + return false; + } + + return $arg->getMotive() === $motive; + }))->shouldNotBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal()); + + $handler->handle($ticket, new ReplaceMotiveCommand($motive)); + + self::assertSame($motive, $ticket->getMotive()); + self::assertCount(1, $ticket->getMotiveHistories()); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/ReplaceMotiveControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/ReplaceMotiveControllerTest.php new file mode 100644 index 000000000..dcf6a2318 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Controller/ReplaceMotiveControllerTest.php @@ -0,0 +1,113 @@ +serializer = self::getContainer()->get(SerializerInterface::class); + $this->validator = self::getContainer()->get(ValidatorInterface::class); + } + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + /** + * @dataProvider generateMotiveId + */ + public function testAddValidMotive(int $motiveId): void + { + $ticket = new Ticket(); + $payload = <<buildController(); + + $response = $controller($ticket, $request); + + self::assertEquals(201, $response->getStatusCode()); + } + + private function buildController(): ReplaceMotiveController + { + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->persist(Argument::type(MotiveHistory::class))->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + + $handler = new ReplaceMotiveCommandHandler( + new MockClock(), + $entityManager->reveal() + ); + + return new ReplaceMotiveController( + $security->reveal(), + $handler, + $this->serializer, + $this->validator, + $entityManager->reveal(), + ); + } + + public static function generateMotiveId(): iterable + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + + $motive = $em->createQuery('SELECT m FROM '.Motive::class.' m ') + ->setMaxResults(1) + ->getOneOrNullResult(); + + if (null === $motive) { + throw new \RuntimeException('the motive table seems to be empty'); + } + + self::ensureKernelShutdown(); + + yield [$motive->getId()]; + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php new file mode 100644 index 000000000..f8d7ad270 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php @@ -0,0 +1,47 @@ +getMotive()); + + $history = new MotiveHistory($motive, $ticket); + $ticket->addMotiveHistory($history); + + self::assertSame($motive, $ticket->getMotive()); + self::assertCount(1, $ticket->getMotiveHistories()); + + // replace motive + $motive2 = new Motive(); + $history->setEndDate(new \DateTimeImmutable()); + $ticket->addMotiveHistory(new MotiveHistory($motive2, $ticket)); + + self::assertCount(2, $ticket->getMotiveHistories()); + self::assertSame($motive2, $ticket->getMotive()); + } +}