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
|
fr: Retard de livraison
|
||||||
active:
|
active:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
MotiveById:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ticket_motive
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- type
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/1.0/ticket/motive.json:
|
/1.0/ticket/motive.json:
|
||||||
@ -23,3 +35,32 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "OK"
|
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);
|
$this->personHistories->add($personHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addMotiveHistory(MotiveHistory $motiveHistory): void
|
||||||
|
{
|
||||||
|
$this->motiveHistories->add($motiveHistory);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<UserGroup|User>
|
* @return list<UserGroup|User>
|
||||||
*/
|
*/
|
||||||
@ -163,4 +168,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
return null;
|
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