mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-14 14:24:24 +00:00
Implement functionality to replace ticket's motive
The commit introduces several features related to ticket motive management in the Chill-TicketBundle: - Adds capability to replace a ticket's motive with a new one. - Provides ticket motive history management features. - Implements relevant changes in Controller, Action Handler, and Entity levels. - Incorporates new API endpoints and updates the API specification file for the new feature. - Includes tests to ensure the new functionality works as expected.
This commit is contained in:
parent
a9760b323f
commit
670b8eb82b
@ -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"
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Action\Ticket\Handler;
|
||||
|
||||
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
final readonly class ReplaceMotiveCommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function handle(Ticket $ticket, ReplaceMotiveCommand $command): void
|
||||
{
|
||||
if (null === $command->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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Action\Ticket;
|
||||
|
||||
use Chill\TicketBundle\Entity\Motive;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final readonly class ReplaceMotiveCommand
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotNull]
|
||||
#[Groups(['write'])]
|
||||
public ?Motive $motive,
|
||||
) {}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Controller;
|
||||
|
||||
use Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler;
|
||||
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final readonly class ReplaceMotiveController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private ReplaceMotiveCommandHandler $replaceMotiveCommandHandler,
|
||||
private SerializerInterface $serializer,
|
||||
private ValidatorInterface $validator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/ticket/{id}/motive/set', name: 'chill_ticket_motive_set', methods: ['POST'])]
|
||||
public function __invoke(Ticket $ticket, Request $request): Response
|
||||
{
|
||||
if (!$this->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);
|
||||
}
|
||||
}
|
@ -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<UserGroup|User>
|
||||
*/
|
||||
@ -163,4 +168,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReadableCollection<int, MotiveHistory>
|
||||
*/
|
||||
public function getMotiveHistories(): ReadableCollection
|
||||
{
|
||||
return $this->motiveHistories;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
|
||||
|
||||
use Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler;
|
||||
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
|
||||
use Chill\TicketBundle\Entity\Motive;
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private function buildHandler(
|
||||
EntityManagerInterface $entityManager,
|
||||
): ReplaceMotiveCommandHandler {
|
||||
$clock = new MockClock();
|
||||
|
||||
return new ReplaceMotiveCommandHandler($clock, $entityManager);
|
||||
}
|
||||
|
||||
public function testHandleOnTicketWithoutMotive(): void
|
||||
{
|
||||
$motive = new Motive();
|
||||
$ticket = new 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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Tests\Controller;
|
||||
|
||||
use Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler;
|
||||
use Chill\TicketBundle\Controller\ReplaceMotiveController;
|
||||
use Chill\TicketBundle\Entity\Motive;
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ReplaceMotiveControllerTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private SerializerInterface $serializer;
|
||||
|
||||
private ValidatorInterface $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->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 = <<<JSON
|
||||
{"motive": {"type": "ticket_motive", "id": {$motiveId}}}
|
||||
JSON;
|
||||
|
||||
$request = new Request(content: $payload);
|
||||
|
||||
$controller = $this->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()];
|
||||
}
|
||||
}
|
47
src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php
Normal file
47
src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\TicketBundle\Tests\Entity;
|
||||
|
||||
use Chill\TicketBundle\Entity\Motive;
|
||||
use Chill\TicketBundle\Entity\MotiveHistory;
|
||||
use Chill\TicketBundle\Entity\Ticket;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TicketTest extends KernelTestCase
|
||||
{
|
||||
public function testGetMotive(): void
|
||||
{
|
||||
$ticket = new Ticket();
|
||||
$motive = new Motive();
|
||||
|
||||
self::assertNull($ticket->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());
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user