diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index c9f1aacea..9b579fbcc 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -67,6 +67,19 @@ paths: type: integer format: integer minimum: 1 + - name: byCurrentState + in: query + description: the current state of the ticket + required: false + style: form + explode: false + schema: + type: array + items: + type: string + enum: + - open + - closed responses: 200: description: OK @@ -80,7 +93,7 @@ paths: results: type: array items: - $ref: '#component/schema/TicketSimple' + $ref: '#components/schemas/TicketSimple' /1.0/ticket/motive.json: diff --git a/src/Bundle/ChillTicketBundle/src/Controller/EditTicketController.php b/src/Bundle/ChillTicketBundle/src/Controller/EditTicketController.php index 0510c49cf..a686208ad 100644 --- a/src/Bundle/ChillTicketBundle/src/Controller/EditTicketController.php +++ b/src/Bundle/ChillTicketBundle/src/Controller/EditTicketController.php @@ -19,7 +19,7 @@ use Twig\Environment; class EditTicketController { - private string $personPerTicket; + private readonly string $personPerTicket; public function __construct( private readonly Environment $templating, diff --git a/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php b/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php index 8c9a90ab5..5e1250b2f 100644 --- a/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php +++ b/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php @@ -15,10 +15,12 @@ use Chill\MainBundle\Pagination\PaginatorFactoryInterface; use Chill\MainBundle\Serializer\Model\Collection; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\TicketBundle\Entity\StateEnum; use Chill\TicketBundle\Repository\TicketACLAwareRepositoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\SerializerInterface; @@ -53,6 +55,17 @@ final readonly class TicketListApiController } } + if ($request->query->has('byCurrentState')) { + try { + $params['byCurrentState'] = array_map( + fn (string $state): StateEnum => StateEnum::fromValue($state), + explode(',', $request->query->get('byCurrentState')) + ); + } catch (\InvalidArgumentException $e) { + throw new BadRequestHttpException($e->getMessage(), $e); + } + } + $nb = $this->ticketRepository->countTickets($params); $paginator = $this->paginatorFactory->create($nb); diff --git a/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php b/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php index 0c59a1ad9..ba5d9a1ee 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php @@ -18,4 +18,13 @@ enum StateEnum: string { case OPEN = 'open'; case CLOSED = 'closed'; + + public static function fromValue(string $value): self + { + return match ($value) { + 'open' => self::OPEN, + 'closed' => self::CLOSED, + default => throw new \InvalidArgumentException('Invalid state value'), + }; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php b/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php index 634f55214..0592ea0c3 100644 --- a/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php +++ b/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\TicketBundle\Repository; use Chill\TicketBundle\Entity\PersonHistory; +use Chill\TicketBundle\Entity\StateHistory; use Chill\TicketBundle\Entity\Ticket; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; @@ -58,6 +59,21 @@ final readonly class TicketACLAwareRepository implements TicketACLAwareRepositor $qb->andWhere($or); } + if (array_key_exists('byCurrentState', $params)) { + $qb->andWhere( + $qb->expr()->exists(sprintf( + 'SELECT 1 FROM %s tp_state_%d WHERE tp_state_%d.ticket = t + AND tp_state_%d.state IN (:currentState) AND tp_state_%d.endDate IS NULL', + StateHistory::class, + ++$i, + $i, + $i, + $i, + )) + ); + $qb->setParameter('currentState', $params['byCurrentState']); + } + return $qb; } } diff --git a/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepositoryInterface.php b/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepositoryInterface.php index e946c1ded..6fce13086 100644 --- a/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepositoryInterface.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\TicketBundle\Repository; use Chill\PersonBundle\Entity\Person; +use Chill\TicketBundle\Entity\StateEnum; use Chill\TicketBundle\Entity\Ticket; /** @@ -22,7 +23,7 @@ interface TicketACLAwareRepositoryInterface /** * Find tickets. * - * @param array{byPerson?: list} $params + * @param array{byPerson?: list, byCurrentState: list} $params * * @return list */ diff --git a/src/Bundle/ChillTicketBundle/src/Validation/Validator/SetPersonCommandConstraintValidator.php b/src/Bundle/ChillTicketBundle/src/Validation/Validator/SetPersonCommandConstraintValidator.php index 9c8b80c66..3b99a5188 100644 --- a/src/Bundle/ChillTicketBundle/src/Validation/Validator/SetPersonCommandConstraintValidator.php +++ b/src/Bundle/ChillTicketBundle/src/Validation/Validator/SetPersonCommandConstraintValidator.php @@ -19,7 +19,7 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException; class SetPersonCommandConstraintValidator extends ConstraintValidator { - private bool $isMulti; + private readonly bool $isMulti; public function __construct(ParameterBagInterface $parameterBag) { diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php index bcaf2336d..9582164fb 100644 --- a/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php @@ -18,6 +18,7 @@ use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\TicketBundle\Controller\TicketListApiController; +use Chill\TicketBundle\Entity\StateEnum; use Chill\TicketBundle\Entity\Ticket; use Chill\TicketBundle\Repository\TicketACLAwareRepositoryInterface; use PHPUnit\Framework\TestCase; @@ -26,6 +27,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\SerializerInterface; @@ -60,7 +62,7 @@ final class TicketListApiControllerTest extends TestCase $serializer->serialize( Argument::that(fn (Collection $collection) => $collection->getItems() === $tickets), 'json', - ['groups' => 'read'] + ['groups' => 'read:simple'] )->willReturn('{"items":[{},{}],"pagination":{}}'); $personRepository = $this->prophesize(PersonRepository::class); @@ -114,7 +116,7 @@ final class TicketListApiControllerTest extends TestCase $serializer->serialize( Argument::that(fn (Collection $collection) => $collection->getItems() === $tickets), 'json', - ['groups' => 'read'] + ['groups' => 'read:simple'] )->willReturn('{"items":[{},{}],"pagination":{}}'); $personRepository = $this->prophesize(PersonRepository::class); @@ -142,6 +144,93 @@ final class TicketListApiControllerTest extends TestCase $this->assertEquals('{"items":[{},{}],"pagination":{}}', $response->getContent()); } + public function testListTicketWithCurrentStateFilter(): void + { + // Mock dependencies + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $ticketRepository = $this->prophesize(TicketACLAwareRepositoryInterface::class); + $tickets = [new Ticket(), new Ticket()]; + $ticketRepository->countTickets( + Argument::that(fn ($params) => isset($params['byCurrentState']) && in_array(StateEnum::OPEN, $params['byCurrentState'])) + ) + ->willReturn(2); + $ticketRepository->findTickets( + Argument::that(fn ($params) => isset($params['byCurrentState']) && in_array(StateEnum::OPEN, $params['byCurrentState'])), + 0, + 10 + )->willReturn($tickets); + + $paginator = $this->prophesize(PaginatorInterface::class); + $paginator->getCurrentPageFirstItemNumber()->willReturn(0); + $paginator->getItemsPerPage()->willReturn(10); + + $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class); + $paginatorFactory->create(2)->willReturn($paginator->reveal()); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->serialize( + Argument::that(fn (Collection $collection) => $collection->getItems() === $tickets), + 'json', + ['groups' => 'read:simple'] + )->willReturn('{"items":[{},{}],"pagination":{}}'); + + $personRepository = $this->prophesize(PersonRepository::class); + + // Create controller + $controller = new TicketListApiController( + $security->reveal(), + $ticketRepository->reveal(), + $paginatorFactory->reveal(), + $serializer->reveal(), + $personRepository->reveal() + ); + + // Create request with person filter + $request = new Request( + query: ['byCurrentState' => 'open,closed'] + ); + + // Call controller method + $response = $controller->listTicket($request); + + // Assert response + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('{"items":[{},{}],"pagination":{}}', $response->getContent()); + } + + public function testListTicketWithCurrentStateWithInvalidFilter(): void + { + self::expectException(BadRequestHttpException::class); + + // Mock dependencies + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $ticketRepository = $this->prophesize(TicketACLAwareRepositoryInterface::class); + $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class); + $serializer = $this->prophesize(SerializerInterface::class); + $personRepository = $this->prophesize(PersonRepository::class); + + // Create controller + $controller = new TicketListApiController( + $security->reveal(), + $ticketRepository->reveal(), + $paginatorFactory->reveal(), + $serializer->reveal(), + $personRepository->reveal() + ); + + // Create request with person filter + $request = new Request( + query: ['byCurrentState' => 'foo'] + ); + + // Call controller method + $response = $controller->listTicket($request); + } + public function testListTicketWithoutUserRole(): void { // Mock dependencies diff --git a/src/Bundle/ChillTicketBundle/tests/Repository/TicketACLAwareRepositoryTest.php b/src/Bundle/ChillTicketBundle/tests/Repository/TicketACLAwareRepositoryTest.php index 4dac71bb1..8a0035032 100644 --- a/src/Bundle/ChillTicketBundle/tests/Repository/TicketACLAwareRepositoryTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Repository/TicketACLAwareRepositoryTest.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\TicketBundle\Tests\Repository; use Chill\PersonBundle\DataFixtures\Helper\RandomPersonHelperTrait; +use Chill\TicketBundle\Entity\StateEnum; use Chill\TicketBundle\Repository\TicketACLAwareRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -63,4 +64,18 @@ class TicketACLAwareRepositoryTest extends KernelTestCase self::assertIsInt($result); } + + public function testCountTicketByCurrentStateSingleState(): void + { + $result = $this->repository->countTickets(['byCurrentState' => [StateEnum::OPEN]]); + + self::assertIsInt($result); + } + + public function testFindTicketByCurrentStateMultipleState(): void + { + $result = $this->repository->countTickets(['byCurrentState' => [StateEnum::OPEN, StateEnum::CLOSED]]); + + self::assertIsInt($result); + } }