From f5a94f7b130b2035c12f38fe5f54e28be1d7a180 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Mon, 21 Sep 2026 21:36:36 +0000 Subject: [PATCH] =?UTF-8?q?Migration=20de=20l'entit=C3=A9=20TicketEventTyp?= =?UTF-8?q?e=20de=20Ting=20vers=20Doctrine=20(#2383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan-baseline.php | 164 +-------------- .../Association/Form/TicketEventType.php | 2 +- .../Controller/Admin/Event/PricesAction.php | 2 +- .../Admin/Event/PricesAddAction.php | 29 ++- .../Admin/Event/PricesEditAction.php | 16 +- .../AppBundle/Controller/Admin/HomeAction.php | 6 +- .../Controller/Event/Ticket/TicketAction.php | 6 +- .../Repository/TicketEventTypeRepository.php | 101 +++++++++ .../Event/Entity/TicketEventType.php | 45 +++++ sources/AppBundle/Event/Form/TicketType.php | 53 ++--- sources/AppBundle/Event/JsonLd.php | 14 +- .../Model/Repository/InvoiceRepository.php | 2 +- .../TicketEventTypeHydrationRepository.php | 77 +++++++ .../Repository/TicketEventTypeRepository.php | 161 --------------- .../Model/Repository/TicketRepository.php | 6 +- sources/AppBundle/Event/Model/Ticket.php | 3 +- .../AppBundle/Event/Model/TicketEventType.php | 191 ------------------ sources/AppBundle/Event/Model/TicketOffer.php | 2 + .../AppBundle/Event/Ticket/TicketOffers.php | 8 +- .../Event/Ticket/TicketTypeAvailability.php | 19 +- .../Constraints/AvailableTicketValidator.php | 6 +- .../Constraints/CorporateMemberValidator.php | 6 +- .../Constraints/EarlyBirdTicketValidator.php | 7 +- .../Constraints/LoggedInMemberValidator.php | 4 +- .../Constraints/PublicTicketValidator.php | 4 +- .../TicketsCfpSubmitterValidator.php | 7 +- .../TicketEventTypeRepositoryTest.php | 132 ++++++++++++ 27 files changed, 466 insertions(+), 607 deletions(-) create mode 100644 sources/AppBundle/Event/Entity/Repository/TicketEventTypeRepository.php create mode 100644 sources/AppBundle/Event/Entity/TicketEventType.php create mode 100644 sources/AppBundle/Event/Model/Repository/TicketEventTypeHydrationRepository.php delete mode 100644 sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php delete mode 100644 sources/AppBundle/Event/Model/TicketEventType.php create mode 100644 tests/integration/AppBundle/Event/Entity/Repository/TicketEventTypeRepositoryTest.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php index f85ead6d8..05fe2dbbc 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -2953,48 +2953,12 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$dateEnd of method AppBundle\\\\Event\\\\Model\\\\TicketEventType\\:\\:setDateEnd\\(\\) expects DateTime, DateTime\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$dateStart of method AppBundle\\\\Event\\\\Model\\\\TicketEventType\\:\\:setDateStart\\(\\) expects DateTime, DateTime\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$eventId of method AppBundle\\\\Event\\\\Model\\\\TicketEventType\\:\\:setEventId\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$message of class Symfony\\\\Component\\\\Form\\\\FormError constructor expects string, string\\|Stringable given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method getId\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', 'identifier' => 'method.nonObject', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method setTicketType\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketEventType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$ticketEventType of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:update\\(\\) expects AppBundle\\\\Event\\\\Model\\\\TicketEventType, AppBundle\\\\Event\\\\Model\\\\TicketEventType\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Controller\\\\Admin\\\\Event\\\\RedirectEventFromSessionListener\\:\\:__construct\\(\\) has parameter \\$controllersWithEventSelector with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', @@ -4603,18 +4567,6 @@ 'count' => 6, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Ticket/TicketAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getIsRestrictedToMembers\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Ticket/TicketAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getTicketType\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketEventType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Ticket/TicketAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method getTickets\\(\\) on mixed\\.$#', 'identifier' => 'method.nonObject', @@ -5551,30 +5503,12 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketSpecialPriceType.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method getIsRestrictedToCfpSubmitter\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', 'identifier' => 'method.nonObject', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getIsRestrictedToMembers\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getPrettyName\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method getTransportInformationEnabled\\(\\) on AppBundle\\\\Event\\\\Model\\\\Event\\|null\\.$#', 'identifier' => 'method.nonObject', @@ -5588,7 +5522,7 @@ 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$event of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:getTicketsByEvent\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Event, AppBundle\\\\Event\\\\Model\\\\Event\\|null given\\.$#', + 'message' => '#^Parameter \\#1 \\$event of method AppBundle\\\\Event\\\\Entity\\\\Repository\\\\TicketEventTypeRepository\\:\\:getTicketsByEvent\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Event, AppBundle\\\\Event\\\\Model\\\\Event\\|null given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', @@ -5920,19 +5854,7 @@ $ignoreErrors[] = [ 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', 'identifier' => 'method.nonObject', - 'count' => 13, - 'path' => __DIR__ . '/sources/AppBundle/Event/JsonLd.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getPrettyName\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/JsonLd.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getTechnicalName\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, + 'count' => 11, 'path' => __DIR__ . '/sources/AppBundle/Event/JsonLd.php', ]; $ignoreErrors[] = [ @@ -6433,12 +6355,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/GithubUserRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getPrice\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketEventType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\InvoiceRepository\\:\\:getByEventId\\(\\) return type with generic interface CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface does not specify its types\\: T$#', 'identifier' => 'missingType.generics', @@ -6932,58 +6848,10 @@ 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getIsRestrictedToMembers\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:doesEventHasRestrictedToMembersTickets\\(\\) has parameter \\$datesFilter with no type specified\\.$#', - 'identifier' => 'missingType.parameter', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:getTicketsByEvent\\(\\) return type with generic interface CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface does not specify its types\\: T$#', - 'identifier' => 'missingType.generics', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', - 'identifier' => 'missingType.iterableValue', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:initMetadata\\(\\) should return M of CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata but returns CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\.$#', + 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeHydrationRepository\\:\\:initMetadata\\(\\) should return M of CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata but returns CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\.$#', 'identifier' => 'return.type', 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:update\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$databaseName of method CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\:\\:setDatabase\\(\\) expects string, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$datesFilter of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketEventTypeRepository\\:\\:getTicketsByEvent\\(\\) expects int\\|null, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php', + 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketEventTypeHydrationRepository.php', ]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between \'@\' and mixed results in an error\\.$#', @@ -7759,12 +7627,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Ticket/SponsorTokenMail.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getPrettyName\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Ticket/TicketOffers.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#4 \\$availableTickets of class AppBundle\\\\Event\\\\Model\\\\TicketOffer constructor expects int, int\\|null given\\.$#', 'identifier' => 'argument.type', @@ -7783,24 +7645,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getDay\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getDays\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getTechnicalName\\(\\) on AppBundle\\\\Event\\\\Model\\\\TicketType\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#', 'identifier' => 'argument.type', diff --git a/sources/AppBundle/Association/Form/TicketEventType.php b/sources/AppBundle/Association/Form/TicketEventType.php index 960ca98da..c123d253f 100644 --- a/sources/AppBundle/Association/Form/TicketEventType.php +++ b/sources/AppBundle/Association/Form/TicketEventType.php @@ -4,7 +4,7 @@ namespace AppBundle\Association\Form; -use AppBundle\Event\Model\TicketEventType as ModelTicketEventType; +use AppBundle\Event\Entity\TicketEventType as ModelTicketEventType; use AppBundle\Event\Model\TicketType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; diff --git a/sources/AppBundle/Controller/Admin/Event/PricesAction.php b/sources/AppBundle/Controller/Admin/Event/PricesAction.php index c1d28a00d..a1a3b1db6 100644 --- a/sources/AppBundle/Controller/Admin/Event/PricesAction.php +++ b/sources/AppBundle/Controller/Admin/Event/PricesAction.php @@ -5,7 +5,7 @@ namespace AppBundle\Controller\Admin\Event; use AppBundle\Event\AdminEventSelection; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; diff --git a/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php b/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php index dd2108273..13badc4d9 100644 --- a/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php +++ b/sources/AppBundle/Controller/Admin/Event/PricesAddAction.php @@ -6,22 +6,19 @@ use AppBundle\Association\Form\TicketEventType; use AppBundle\Event\AdminEventSelection; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\TicketEventType as ModelTicketEventType; use AppBundle\Event\Model\Repository\TicketTypeRepository; -use AppBundle\Event\Model\TicketEventType as ModelTicketEventType; -use AppBundle\Validator\Constraints\UniqueEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Validator\Validator\ValidatorInterface; class PricesAddAction extends AbstractController { public function __construct( private readonly TicketTypeRepository $ticketTypeRepository, private readonly TicketEventTypeRepository $ticketEventTypeRepository, - private readonly ValidatorInterface $validator, ) {} public function __invoke(Request $request, AdminEventSelection $eventSelection): Response @@ -30,9 +27,9 @@ public function __invoke(Request $request, AdminEventSelection $eventSelection): $event = $eventSelection->event; $ticketEventType = new ModelTicketEventType(); - $ticketEventType->setEventId($event->getId()); - $ticketEventType->setDateStart($event->getDateStart()); - $ticketEventType->setDateEnd($event->getDateEnd()); + $ticketEventType->eventId = (int) $event->getId(); + $ticketEventType->dateStart = $event->getDateStart() ?? new \DateTime(); + $ticketEventType->dateEnd = $event->getDateEnd() ?? new \DateTime(); $ticketTypes = $this->ticketTypeRepository->getAll(); $form = $this->createForm(TicketEventType::class, $ticketEventType, [ @@ -42,17 +39,15 @@ public function __invoke(Request $request, AdminEventSelection $eventSelection): $form->handleRequest($request); if ($form->isSubmitted()) { - $ticketEventType->setTicketTypeId($ticketEventType->getTicketType()->getId()); + $ticketEventType->ticketTypeId = $ticketEventType->ticketType->getId(); - $violations = $this->validator->validate($ticketEventType, [ - new UniqueEntity( - ['ticketTypeId', 'eventId'], - $this->ticketEventTypeRepository, - 'Ce type de ticket existe déjà pour cet évènement.', - ), + // Vérification maison d'unicité : la clé primaire est composée de (ticketTypeId, eventId) + $existant = $this->ticketEventTypeRepository->findOneBy([ + 'eventId' => $ticketEventType->eventId, + 'ticketTypeId' => $ticketEventType->ticketTypeId, ]); - foreach ($violations as $violation) { - $form->get('ticketType')->addError(new FormError($violation->getMessage())); + if ($existant !== null) { + $form->get('ticketType')->addError(new FormError('Ce type de ticket existe déjà pour cet évènement.')); } if ($form->isValid()) { diff --git a/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php b/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php index b6758d553..cf2b4f902 100644 --- a/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php +++ b/sources/AppBundle/Controller/Admin/Event/PricesEditAction.php @@ -7,12 +7,13 @@ use AppBundle\Association\Form\TicketEventType; use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Event\Form\EventSelectType; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; use AppBundle\Event\Model\Repository\TicketTypeRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class PricesEditAction extends AbstractController { @@ -28,11 +29,14 @@ public function __invoke(Request $request, int $event, int $id): Response $event = $this->eventActionHelper->getEventById($event); $ticketType = $this->ticketTypeRepository->get($id); - $ticketEventType = $this->ticketEventTypeRepository->get([ - 'id_event' => $event->getId(), - 'id_tarif' => $ticketType->getId(), + $ticketEventType = $this->ticketEventTypeRepository->find([ + 'eventId' => $event->getId(), + 'ticketTypeId' => $ticketType->getId(), ]); - $ticketEventType->setTicketType($ticketType); + if ($ticketEventType === null) { + throw new NotFoundHttpException(); + } + $ticketEventType->ticketType = $ticketType; $ticketTypes = $this->ticketTypeRepository->getAll(); $form = $this->createForm(TicketEventType::class, $ticketEventType, [ @@ -44,7 +48,7 @@ public function __invoke(Request $request, int $event, int $id): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->ticketEventTypeRepository->update($ticketEventType); + $this->ticketEventTypeRepository->save($ticketEventType); $this->addFlash('notice', 'Le tarif a été modifié'); diff --git a/sources/AppBundle/Controller/Admin/HomeAction.php b/sources/AppBundle/Controller/Admin/HomeAction.php index aa49551ce..9cfea213c 100644 --- a/sources/AppBundle/Controller/Admin/HomeAction.php +++ b/sources/AppBundle/Controller/Admin/HomeAction.php @@ -9,7 +9,7 @@ use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\EventStatsRepository; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; use AppBundle\AssembleeGenerale\Entity\Repository\PresenceRepository; use AppBundle\Security\Authentication; use AppBundle\Veille\Entity\Repository\NewsletterInscriptionRepository; @@ -69,8 +69,8 @@ public function __invoke(): Response $montantTotal = 0; foreach ($this->ticketEventTypeRepository->getTicketsByEvent($event, false) as $ticketEventType) { - if (array_key_exists($ticketEventType->getTicketTypeId(), $stats->ticketType->paying)) { - $montantTotal += $stats->ticketType->paying[$ticketEventType->getTicketTypeId()] * $ticketEventType->getPrice(); + if (array_key_exists($ticketEventType->ticketTypeId, $stats->ticketType->paying)) { + $montantTotal += $stats->ticketType->paying[$ticketEventType->ticketTypeId] * ($ticketEventType->price ?? 0.0); } } diff --git a/sources/AppBundle/Controller/Event/Ticket/TicketAction.php b/sources/AppBundle/Controller/Event/Ticket/TicketAction.php index 22be2bda4..87d7bfca8 100644 --- a/sources/AppBundle/Controller/Event/Ticket/TicketAction.php +++ b/sources/AppBundle/Controller/Event/Ticket/TicketAction.php @@ -13,7 +13,7 @@ use AppBundle\Event\Entity\Repository\BilleteriePriveeRepository; use AppBundle\Event\Model\Invoice; use AppBundle\Event\Model\Repository\InvoiceRepository; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; use AppBundle\Event\Model\Repository\TicketRepository; use AppBundle\Event\Model\Ticket; use AppBundle\Event\Ticket\PurchaseTypeFactory; @@ -96,7 +96,9 @@ public function __invoke($eventSlug, Request $request): Response } foreach ($tickets as $ticket) { - if ($ticket->getTicketEventType()->getTicketType()->getIsRestrictedToMembers() && isset($memberId, $memberType)) { + if ($ticket->getTicketEventType()?->ticketType !== null + && $ticket->getTicketEventType()->ticketType->getIsRestrictedToMembers() + && isset($memberId, $memberType)) { $ticket ->setMemberId($memberId) ->setMemberType($memberType) diff --git a/sources/AppBundle/Event/Entity/Repository/TicketEventTypeRepository.php b/sources/AppBundle/Event/Entity/Repository/TicketEventTypeRepository.php new file mode 100644 index 000000000..e9b2c8dac --- /dev/null +++ b/sources/AppBundle/Event/Entity/Repository/TicketEventTypeRepository.php @@ -0,0 +1,101 @@ + + */ +final class TicketEventTypeRepository extends EntityRepository +{ + public const int REMOVE_PAST_TICKETS = 1; + public const int REMOVE_FUTURE_TICKETS = 2; + public const int ACTUAL_TICKETS_ONLY = 3; // Combinaison de REMOVE_PAST_TICKETS et REMOVE_FUTURE_TICKETS + + public function __construct( + ManagerRegistry $registry, + private readonly TicketTypeRepository $ticketTypeRepository, + ) { + parent::__construct($registry, TicketEventType::class); + } + + /** + * Retourne les tarifs d'un évènement, éventuellement filtrés : + * - publicOnly : uniquement les types de tarifs publics ; + * - datesFilter : masque de bits des constantes REMOVE_PAST_TICKETS / REMOVE_FUTURE_TICKETS. + * + * @return list + */ + public function getTicketsByEvent(Event $event, bool $publicOnly = true, ?int $datesFilter = null): array + { + $queryBuilder = ($this->getEntityManager()->getConnection())->createQueryBuilder() + ->select('tarif_event.id_tarif', 'tarif_event.id_event') + ->from('afup_forum_tarif_event', 'tarif_event') + ->innerJoin('tarif_event', 'afup_forum_tarif', 'tarif', 'tarif.id = tarif_event.id_tarif') + ->where('tarif_event.id_event = :event') + ->setParameter('event', $event->getId()); + + if (($datesFilter & self::REMOVE_PAST_TICKETS) !== 0) { + $queryBuilder->andWhere('tarif_event.date_end > NOW()'); + } + if (($datesFilter & self::REMOVE_FUTURE_TICKETS) !== 0) { + $queryBuilder->andWhere('tarif_event.date_start < NOW()'); + } + if ($publicOnly === true) { + $queryBuilder->andWhere('tarif.public = 1'); + } + + $queryBuilder + ->orderBy('tarif_event.date_start') + ->addOrderBy('tarif_event.date_end') + ->addOrderBy('tarif_event.price') + ->addOrderBy('tarif.members_only', 'DESC'); + + $ticketTypes = []; + foreach ($this->ticketTypeRepository->getAll() as $ticketType) { + $ticketTypes[$ticketType->getId()] = $ticketType; + } + + $ticketEventTypes = []; + foreach ($queryBuilder->executeQuery()->fetchAllAssociative() as $row) { + if (!isset($row['id_tarif'], $row['id_event']) + || !is_numeric($row['id_tarif']) + || !is_numeric($row['id_event']) + ) { + continue; + } + $ticketEventType = $this->find([ + 'ticketTypeId' => (int) $row['id_tarif'], + 'eventId' => (int) $row['id_event'], + ]); + if ($ticketEventType === null) { + continue; + } + $ticketEventType->ticketType = $ticketTypes[$ticketEventType->ticketTypeId] ?? null; + $ticketEventTypes[] = $ticketEventType; + } + + return $ticketEventTypes; + } + + /** + * Indique si l'évènement propose au moins un tarif réservé aux membres. + */ + public function doesEventHasRestrictedToMembersTickets(Event $event, bool $publicOnly = true, ?int $datesFilter = null): bool + { + foreach ($this->getTicketsByEvent($event, $publicOnly, $datesFilter) as $ticketEventType) { + if ($ticketEventType->ticketType?->getIsRestrictedToMembers() === true) { + return true; + } + } + + return false; + } +} diff --git a/sources/AppBundle/Event/Entity/TicketEventType.php b/sources/AppBundle/Event/Entity/TicketEventType.php new file mode 100644 index 000000000..bd1c79873 --- /dev/null +++ b/sources/AppBundle/Event/Entity/TicketEventType.php @@ -0,0 +1,45 @@ +ticketType = $ticketType; + } +} diff --git a/sources/AppBundle/Event/Form/TicketType.php b/sources/AppBundle/Event/Form/TicketType.php index 905f5962a..bd3fa2447 100644 --- a/sources/AppBundle/Event/Form/TicketType.php +++ b/sources/AppBundle/Event/Form/TicketType.php @@ -8,11 +8,11 @@ use AppBundle\Association\Genre; use AppBundle\Event\Entity\BilleteriePrivee; use AppBundle\Event\Model\Repository\EventRepository; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository; use AppBundle\Event\Model\Repository\TicketTypeRepository; use AppBundle\Event\Model\Ticket; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\TicketEventType; use AppBundle\Event\Model\TicketSpecialPrice; use AppBundle\Event\Ticket\TicketTypeAvailability; use Symfony\Component\Form\AbstractType; @@ -85,7 +85,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $formEvent) use ($eventTickets, $options, $event): void { $filteredEventTickets = []; foreach ($eventTickets as $eventTicket) { - if ($eventTicket->getTicketType()->getIsRestrictedToCfpSubmitter() && !$options['is_cfp_submitter']) { + if ($eventTicket->ticketType->getIsRestrictedToCfpSubmitter() && !$options['is_cfp_submitter']) { continue; } $filteredEventTickets[] = $eventTicket; @@ -115,19 +115,22 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'error_bubbling' => false, 'choice_attr' => function (TicketEventType $type, $key, $index) use ($options, $event): array { $attr = [ - 'data-description' => $type->getDescription(), - 'data-price' => $type->getPrice(), - 'data-date-end' => $type->getDateEnd()->format('d/m'), - 'data-date-end-raw' => $type->getDateEnd()->format('Y-m-d'), - 'data-members-only' => (int) $type->getTicketType()->getIsRestrictedToMembers(), - 'data-max-tickets' => $type->getMaxTickets(), - 'data-stock' => $this->ticketTypeAvailability->getStock($type, $event), - 'data-label' => $type->getTicketType()->getPrettyName(), + 'data-description' => $type->description, + 'data-price' => $type->price, + 'data-date-end' => $type->dateEnd->format('d/m'), + 'data-date-end-raw' => $type->dateEnd->format('Y-m-d'), + 'data-max-tickets' => $type->maxTickets, ]; + if ($type->ticketType !== null) { + $attr['data-members-only'] = (int) $type->ticketType->getIsRestrictedToMembers(); + $attr['data-stock'] = $this->ticketTypeAvailability->getStock($type, $event); + $attr['data-label'] = $type->ticketType->getPrettyName(); + } + if ( - ($type->getTicketType()->getIsRestrictedToMembers() === true && $options['member_type'] === self::MEMBER_NOT) - || $attr['data-stock'] <= 0 + ($type->ticketType?->getIsRestrictedToMembers() === true && $options['member_type'] === self::MEMBER_NOT) + || (isset($attr['data-stock']) && $attr['data-stock'] <= 0) ) { $attr['disabled'] = 'disabled'; } @@ -193,12 +196,12 @@ private function createSpecialPriceTicketEventType(TicketSpecialPrice $ticketSpe } $eToken = new TicketEventType(); - $eToken->setDateStart($dateStart); - $eToken->setDateEnd($dateEnd); - $eToken->setPrice($ticketSpecialPrice->getPrice()); - $eToken->setTicketType($ticketType); - $eToken->setEventId($ticketSpecialPrice->getEventId()); - $eToken->setTicketTypeId(Ticket::TYPE_SPECIAL_PRICE); + $eToken->dateStart = $dateStart; + $eToken->dateEnd = $dateEnd; + $eToken->price = $ticketSpecialPrice->getPrice(); + $eToken->ticketType = $ticketType; + $eToken->eventId = $ticketSpecialPrice->getEventId(); + $eToken->ticketTypeId = Ticket::TYPE_SPECIAL_PRICE; return [$eToken]; } @@ -215,12 +218,12 @@ private function createBilleteriePriveeTicketEventType(BilleteriePrivee $billete } $eBilleterie = new TicketEventType(); - $eBilleterie->setDateStart(\DateTime::createFromImmutable($billeteriePrivee->dateDebut)); - $eBilleterie->setDateEnd(\DateTime::createFromImmutable($billeteriePrivee->dateFin)); - $eBilleterie->setPrice($billeteriePrivee->prix); - $eBilleterie->setTicketType($ticketType); - $eBilleterie->setEventId($billeteriePrivee->eventId); - $eBilleterie->setTicketTypeId(Ticket::TYPE_SPECIAL_PRICE); + $eBilleterie->dateStart = \DateTime::createFromImmutable($billeteriePrivee->dateDebut); + $eBilleterie->dateEnd = \DateTime::createFromImmutable($billeteriePrivee->dateFin); + $eBilleterie->price = $billeteriePrivee->prix; + $eBilleterie->ticketType = $ticketType; + $eBilleterie->eventId = $billeteriePrivee->eventId; + $eBilleterie->ticketTypeId = Ticket::TYPE_SPECIAL_PRICE; return [$eBilleterie]; } diff --git a/sources/AppBundle/Event/JsonLd.php b/sources/AppBundle/Event/JsonLd.php index 75f629c82..84de70406 100644 --- a/sources/AppBundle/Event/JsonLd.php +++ b/sources/AppBundle/Event/JsonLd.php @@ -7,8 +7,8 @@ use AppBundle\CFP\PhotoStorage; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\TicketEventType; use AppBundle\Event\Ticket\TicketTypeAvailability; use Symfony\Component\Asset\Packages; @@ -93,12 +93,12 @@ public function getDataForEvent(Event $event): array foreach ($eventTickets as $eventTicket) { $offers[] = [ '@type' => 'Offer', - 'name' => $eventTicket->getTicketType()->getPrettyName(), - 'sku' => $eventTicket->getTicketType()->getTechnicalName(), + 'name' => $eventTicket->ticketType?->getPrettyName(), + 'sku' => $eventTicket->ticketType?->getTechnicalName(), 'priceCurrency' => 'EUR', - 'price' => $eventTicket->getPrice(), - 'validFrom' => $eventTicket->getDateStart()->format('c'), - 'validThrough' => $eventTicket->getDateEnd()->format('c'), + 'price' => $eventTicket->price, + 'validFrom' => $eventTicket->dateStart->format('c'), + 'validThrough' => $eventTicket->dateEnd->format('c'), 'availability' => $this->ticketTypeAvailability->getStock($eventTicket, $event) > 0 ? $available : $notAvailable, ]; } diff --git a/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php b/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php index 9b82d050d..e3e641f3a 100644 --- a/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php +++ b/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php @@ -45,7 +45,7 @@ public function saveWithTickets(Invoice $invoice): void ->setTransportDistance(null) ->setReference($invoice->getReference()) ->setDate(new \DateTime()) - ->setAmount($ticket->getTicketEventType()->getPrice()) + ->setAmount($ticket->getTicketEventType()->price ?? 0.0) ->setStatus(Ticket::STATUS_CREATED) ->setInvoiceStatus(Ticket::INVOICE_TODO) ->setForumId($invoice->getForumId()) diff --git a/sources/AppBundle/Event/Model/Repository/TicketEventTypeHydrationRepository.php b/sources/AppBundle/Event/Model/Repository/TicketEventTypeHydrationRepository.php new file mode 100644 index 000000000..c28cf4e48 --- /dev/null +++ b/sources/AppBundle/Event/Model/Repository/TicketEventTypeHydrationRepository.php @@ -0,0 +1,77 @@ + + */ +class TicketEventTypeHydrationRepository extends Repository implements MetadataInitializer +{ + /** + * @param SerializerFactoryInterface $serializerFactory + * @param array $options + * + * @inheritDoc + */ + public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) + { + $metadata = new Metadata($serializerFactory); + + $metadata->setEntity(TicketEventType::class); + $metadata->setConnectionName('main'); + $metadata->setDatabase($options['database']); + $metadata->setTable('afup_forum_tarif_event'); + + $metadata + ->addField([ + 'columnName' => 'id_tarif', + 'fieldName' => 'ticketTypeId', + 'type' => 'int', + ]) + ->addField([ + 'columnName' => 'id_event', + 'fieldName' => 'eventId', + 'type' => 'int', + ]) + ->addField([ + 'columnName' => 'price', + 'fieldName' => 'price', + 'type' => 'float', + ]) + ->addField([ + 'columnName' => 'date_start', + 'fieldName' => 'dateStart', + 'type' => 'datetime', + ]) + ->addField([ + 'columnName' => 'date_end', + 'fieldName' => 'dateEnd', + 'type' => 'datetime', + ]) + ->addField([ + 'columnName' => 'description', + 'fieldName' => 'description', + 'type' => 'string', + ]) + ->addField([ + 'columnName' => 'max_tickets', + 'fieldName' => 'maxTickets', + 'type' => 'int', + ]) + ; + + return $metadata; + } +} diff --git a/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php b/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php deleted file mode 100644 index f711bc6a2..000000000 --- a/sources/AppBundle/Event/Model/Repository/TicketEventTypeRepository.php +++ /dev/null @@ -1,161 +0,0 @@ - - */ -class TicketEventTypeRepository extends Repository implements MetadataInitializer -{ - public const int REMOVE_PAST_TICKETS = 1; - public const int REMOVE_FUTURE_TICKETS = 2; - public const int ACTUAL_TICKETS_ONLY = 3; // Combination of REMOVE_PAST_TICKETS and REMOVE_FUTURE_TICKETS - /** - * @param bool $publicOnly - * @param null|int $datesFilter can be one of self::REMOVE_PAST_TICKETS, self::REMOVE_FUTURE_TICKETS. self::ACTUAL_TICKETS == self::REMOVE_PAST_TICKETS | self::REMOVE_FUTURE_TICKETS. Default value is ACTUAL_TICKETS - * @return CollectionInterface|TicketEventType[] - * @throws QueryException - */ - public function getTicketsByEvent(Event $event, $publicOnly = true, $datesFilter = null) - { - $sql = ' - SELECT - id_tarif, id_event, price, date_start, date_end, description, tarif_event.max_tickets, - tarif.id, tarif.technical_name, tarif.day, tarif.pretty_name, tarif.public, tarif.members_only, tarif.default_price, tarif.active, tarif.cfp_submitter_only - FROM afup_forum_tarif_event tarif_event - JOIN afup_forum_tarif tarif ON tarif.id = tarif_event.id_tarif - WHERE id_event = :event - '; - - $params = ['event' => $event->getId()]; - - if (($datesFilter & self::REMOVE_PAST_TICKETS) !== 0) { - $sql .= ' AND date_end > NOW() '; - } - if (($datesFilter & self::REMOVE_FUTURE_TICKETS) !== 0) { - $sql .= ' AND date_start < NOW() '; - } - - if ($publicOnly === true) { - $sql .= 'AND public = :public'; - $params['public'] = $publicOnly; - } - $sql .= ' - ORDER BY date_start, date_end, price, tarif.members_only DESC - '; - $query = $this->getPreparedQuery($sql)->setParams($params); - - return $query->query( - $this->getCollection( - new HydratorSingleObject()->mapObjectTo('tarif', 'tarif_event', 'setTicketType'), - ), - ); - } - - public function update(TicketEventType $ticketEventType) - { - $sql = 'UPDATE afup_forum_tarif_event - SET price = :price, - date_start = :date_start, - date_end = :date_end, - description = :description, - max_tickets = :max_tickets - WHERE id_tarif = :id_tarif AND id_event = :id_event'; - - $query = $this->getPreparedQuery($sql)->setParams([ - 'id_tarif' => $ticketEventType->getTicketTypeId(), - 'id_event' => $ticketEventType->getEventId(), - 'price' => $ticketEventType->getPrice(), - 'date_start' => $ticketEventType->getDateStart()->format('Y-m-d H:i:s'), - 'date_end' => $ticketEventType->getDateEnd()->format('Y-m-d H:i:s'), - 'description' => $ticketEventType->getDescription(), - 'max_tickets' => $ticketEventType->getMaxTickets(), - ]); - - return $query->execute(); - } - - /** - * @param bool $publicOnly - * - * - * @throws QueryException - */ - public function doesEventHasRestrictedToMembersTickets(Event $event, $publicOnly = true, $datesFilter = null): bool - { - $tickets = $this->getTicketsByEvent($event, $publicOnly, $datesFilter); - - foreach ($tickets as $ticket) { - if ($ticket->getTicketType()->getIsRestrictedToMembers()) { - return true; - } - } - - return false; - } - - /** - * @inheritDoc - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - - $metadata->setEntity(TicketEventType::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_forum_tarif_event'); - - $metadata - ->addField([ - 'columnName' => 'id_tarif', - 'fieldName' => 'ticketTypeId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'id_event', - 'fieldName' => 'eventId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'price', - 'fieldName' => 'price', - 'type' => 'float', - ]) - ->addField([ - 'columnName' => 'date_start', - 'fieldName' => 'dateStart', - 'type' => 'datetime', - ]) - ->addField([ - 'columnName' => 'date_end', - 'fieldName' => 'dateEnd', - 'type' => 'datetime', - ]) - ->addField([ - 'columnName' => 'description', - 'fieldName' => 'description', - 'type' => 'string', - ]) - ->addField([ - 'columnName' => 'max_tickets', - 'fieldName' => 'maxTickets', - 'type' => 'int', - ]) - ; - - return $metadata; - } -} diff --git a/sources/AppBundle/Event/Model/Repository/TicketRepository.php b/sources/AppBundle/Event/Model/Repository/TicketRepository.php index db01efb3d..bbbb0b00d 100644 --- a/sources/AppBundle/Event/Model/Repository/TicketRepository.php +++ b/sources/AppBundle/Event/Model/Repository/TicketRepository.php @@ -110,6 +110,8 @@ public function getWithTicketTypeByReference(string $reference): array public function getByInvoiceWithDetail(Invoice $invoice) { + // Les tarifs sont hydratés par Ting via un JOIN : l'entité Doctrine + // TicketEventType est ré-attachée au ticket (TicketEventTypeHydrationRepository) return $this->getPreparedQuery( 'SELECT inscriptions.id, inscriptions.date, inscriptions.reference, inscriptions.coupon, inscriptions.type_inscription, @@ -117,8 +119,8 @@ public function getByInvoiceWithDetail(Invoice $invoice) inscriptions.email, inscriptions.telephone, inscriptions.citer_societe, inscriptions.newsletter_afup, inscriptions.newsletter_nexen, inscriptions.commentaires, inscriptions.etat, inscriptions.facturation, inscriptions.id_forum, inscriptions.mail_partenaire, inscriptions.presence_day1, inscriptions.presence_day2, - tarif_event.id_tarif, tarif_event.id_event, tarif_event.price, tarif_event.date_start, tarif_event.date_end, tarif_event.description, - tarif.id, tarif.technical_name, tarif.day, tarif.pretty_name, tarif.public, tarif.members_only, tarif.default_price, tarif.active + tarif_event.id_tarif, tarif_event.id_event, tarif_event.price, tarif_event.date_start, tarif_event.date_end, tarif_event.description, tarif_event.max_tickets, + tarif.id, tarif.technical_name, tarif.day, tarif.pretty_name, tarif.public, tarif.members_only, tarif.default_price, tarif.active, tarif.cfp_submitter_only FROM afup_inscription_forum inscriptions JOIN afup_forum_tarif_event tarif_event ON tarif_event.id_tarif = inscriptions.type_inscription AND tarif_event.id_event = inscriptions.id_forum JOIN afup_forum_tarif tarif ON tarif.id = tarif_event.id_tarif diff --git a/sources/AppBundle/Event/Model/Ticket.php b/sources/AppBundle/Event/Model/Ticket.php index 67d06092c..9e71bd7a2 100644 --- a/sources/AppBundle/Event/Model/Ticket.php +++ b/sources/AppBundle/Event/Model/Ticket.php @@ -5,6 +5,7 @@ namespace AppBundle\Event\Model; use AppBundle\Association\Genre; +use AppBundle\Event\Entity\TicketEventType; use AppBundle\Event\Validator\Constraints as Assert; use CCMBenchmark\Ting\Entity\NotifyProperty; use CCMBenchmark\Ting\Entity\NotifyPropertyInterface; @@ -322,7 +323,7 @@ public function getTicketEventType(): ?TicketEventType public function setTicketEventType(TicketEventType $ticketEventType): self { $this->ticketEventType = $ticketEventType; - $this->ticketTypeId = $ticketEventType->getTicketTypeId(); + $this->ticketTypeId = $ticketEventType->ticketTypeId; return $this; } diff --git a/sources/AppBundle/Event/Model/TicketEventType.php b/sources/AppBundle/Event/Model/TicketEventType.php deleted file mode 100644 index a8c78ad67..000000000 --- a/sources/AppBundle/Event/Model/TicketEventType.php +++ /dev/null @@ -1,191 +0,0 @@ -ticketTypeId; - } - - /** - * @param int $ticketTypeId - */ - public function setTicketTypeId($ticketTypeId): self - { - $this->propertyChanged('ticketTypeId', $this->ticketTypeId, $ticketTypeId); - $this->ticketTypeId = $ticketTypeId; - return $this; - } - - /** - * @return int - */ - public function getEventId() - { - return $this->eventId; - } - - /** - * @param int $eventId - */ - public function setEventId($eventId): self - { - $this->propertyChanged('eventId', $this->eventId, $eventId); - $this->eventId = $eventId; - return $this; - } - - /** - * @return float - */ - public function getPrice() - { - return $this->price; - } - - /** - * @param float $price - */ - public function setPrice($price): self - { - $this->propertyChanged('price', $this->price, $price); - $this->price = $price; - return $this; - } - - /** - * @return \DateTime - */ - public function getDateStart(): ?\DateTime - { - return $this->dateStart; - } - - public function setDateStart(\DateTime $dateStart): self - { - $this->propertyChanged('dateStart', $this->dateStart, $dateStart); - $this->dateStart = $dateStart; - return $this; - } - - /** - * @return \DateTime - */ - public function getDateEnd(): ?\DateTime - { - return $this->dateEnd; - } - - public function setDateEnd(\DateTime $dateEnd): self - { - $this->propertyChanged('dateEnd', $this->dateEnd, $dateEnd); - $this->dateEnd = $dateEnd; - return $this; - } - - /** - * @return string - */ - public function getDescription() - { - return $this->description; - } - - /** - * @param string $description - */ - public function setDescription($description): self - { - $this->propertyChanged('description', $this->description, $description); - $this->description = $description; - return $this; - } - - /** - * @return TicketType - */ - public function getTicketType(): ?TicketType - { - return $this->ticketType; - } - - public function setTicketType(TicketType $ticketType): self - { - $this->ticketType = $ticketType; - return $this; - } - - /** - * @return int|null - */ - public function getMaxTickets() - { - return $this->maxTickets; - } - - /** - * @param int $maxTickets - * - * @return $this - */ - public function setMaxTickets($maxTickets): self - { - $this->propertyChanged('maxTickets', $this->maxTickets, $maxTickets); - $this->maxTickets = $maxTickets; - - return $this; - } - - public function uniqueId(): ?string - { - if ($this->ticketTypeId === null || $this->eventId === null) { - return null; - } - - return sprintf('%d-%d', $this->ticketTypeId, $this->eventId); - } -} diff --git a/sources/AppBundle/Event/Model/TicketOffer.php b/sources/AppBundle/Event/Model/TicketOffer.php index ac97198bf..d61269452 100644 --- a/sources/AppBundle/Event/Model/TicketOffer.php +++ b/sources/AppBundle/Event/Model/TicketOffer.php @@ -4,6 +4,8 @@ namespace AppBundle\Event\Model; +use AppBundle\Event\Entity\TicketEventType; + final readonly class TicketOffer { public function __construct( diff --git a/sources/AppBundle/Event/Ticket/TicketOffers.php b/sources/AppBundle/Event/Ticket/TicketOffers.php index 18aadd85c..352612f48 100644 --- a/sources/AppBundle/Event/Ticket/TicketOffers.php +++ b/sources/AppBundle/Event/Ticket/TicketOffers.php @@ -5,7 +5,7 @@ namespace AppBundle\Event\Ticket; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Repository\TicketEventTypeRepository; +use AppBundle\Event\Entity\Repository\TicketEventTypeRepository; use AppBundle\Event\Model\Repository\TicketTypeRepository; use AppBundle\Event\Model\TicketOffer; use AppBundle\Event\Model\TicketType; @@ -39,11 +39,11 @@ public function getAllOffersForEvent(Event $event): array $ticketTypes = $this->ticketEventTypeRepository->getTicketsByEvent($event, false); foreach ($ticketTypes as $ticketEventType) { - $ticketTypeId = $ticketEventType->getTicketTypeId(); + $ticketTypeId = $ticketEventType->ticketTypeId; $offers[$ticketTypeId] = new TicketOffer( $ticketTypeId, - $ticketEventType->getTicketType()->getPrettyName(), - $ticketEventType->getPrice(), + $ticketEventType->ticketType?->getPrettyName() ?? '', + $ticketEventType->price ?? 0.0, $this->ticketTypeAvailability->getStock($ticketEventType, $event), $event, $ticketEventType, diff --git a/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php b/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php index ab487f498..1b9b3edd9 100644 --- a/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php +++ b/sources/AppBundle/Event/Ticket/TicketTypeAvailability.php @@ -6,7 +6,7 @@ use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TicketRepository; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\TicketEventType; use AppBundle\Event\Model\TicketType; class TicketTypeAvailability @@ -20,23 +20,28 @@ public function __construct(private readonly TicketRepository $ticketRepository) public function getStock(TicketEventType $ticketEventType, Event $event): int { // Les tickets depuis les tokens doivent fonctionner même après le sold out - if ($ticketEventType->getTicketType()->getTechnicalName() == TicketType::SPECIAL_PRICE_TECHNICAL_NAME) { + if ($ticketEventType->ticketType !== null && $ticketEventType->ticketType->getTechnicalName() == TicketType::SPECIAL_PRICE_TECHNICAL_NAME) { return PHP_INT_MAX; } + if ($ticketEventType->ticketType === null) { + return 0; + } + $ticketType = $ticketEventType->ticketType; + // selon si on est sur un ticket un jour ou deux jours, on calcule différement le nombre de tickets vendus, et le nombre de tickets vendus du type - if (count($ticketEventType->getTicketType()->getDays()) === 2) { + if (count($ticketType->getDays()) === 2) { $allTickets = $this->ticketRepository->getPublicSoldTickets($event); - $typeTickets = $this->ticketRepository->getPublicSoldTicketsOfType($event, $ticketEventType->getTicketType()); + $typeTickets = $this->ticketRepository->getPublicSoldTicketsOfType($event, $ticketType); } else { - $allTickets = $this->ticketRepository->getPublicSoldTicketsByDay($ticketEventType->getTicketType()->getDay(), $event); - $typeTickets = $this->ticketRepository->getPublicSoldTicketsByDayOfType($event, $ticketEventType->getTicketType()->getDay(), $ticketEventType->getTicketType()); + $allTickets = $this->ticketRepository->getPublicSoldTicketsByDay($ticketType->getDay(), $event); + $typeTickets = $this->ticketRepository->getPublicSoldTicketsByDayOfType($event, $ticketType->getDay(), $ticketType); } // on regarde combien de tickets il nous reste $stockTotal = $event->getSeats() - $allTickets; - if (null !== ($maxTickets = $ticketEventType->getMaxTickets())) { + if (null !== ($maxTickets = $ticketEventType->maxTickets)) { $stockForType = $maxTickets - $typeTickets; // si on a un maximum sur le nombre de tickets, soit on a tout vendu au global, soit on a tout vendu pour le type de ticket diff --git a/sources/AppBundle/Event/Validator/Constraints/AvailableTicketValidator.php b/sources/AppBundle/Event/Validator/Constraints/AvailableTicketValidator.php index ca3f70389..ca7880a02 100644 --- a/sources/AppBundle/Event/Validator/Constraints/AvailableTicketValidator.php +++ b/sources/AppBundle/Event/Validator/Constraints/AvailableTicketValidator.php @@ -6,7 +6,7 @@ use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Ticket; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\TicketEventType; use AppBundle\Event\Ticket\TicketTypeAvailability; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -29,9 +29,9 @@ public function validate($ticket, Constraint $constraint): void return ; } - $event = $this->eventRepository->get($ticket->getTicketEventType()->getEventId()); + $event = $this->eventRepository->get($ticket->getTicketEventType()->eventId); if ( - $ticket->getTicketEventType()->getDateEnd() < new \DateTime() + $ticket->getTicketEventType()->dateEnd < new \DateTime() || $this->ticketTypeAvailability->getStock($ticket->getTicketEventType(), $event) <= 0 ) { $this->context->buildViolation($constraint->message) diff --git a/sources/AppBundle/Event/Validator/Constraints/CorporateMemberValidator.php b/sources/AppBundle/Event/Validator/Constraints/CorporateMemberValidator.php index 97c63971b..e9447d3d0 100644 --- a/sources/AppBundle/Event/Validator/Constraints/CorporateMemberValidator.php +++ b/sources/AppBundle/Event/Validator/Constraints/CorporateMemberValidator.php @@ -9,7 +9,7 @@ use AppBundle\Association\Model\User; use AppBundle\Event\Model\Repository\TicketRepository; use AppBundle\Event\Model\Ticket; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\TicketEventType; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -33,11 +33,11 @@ public function validate($tickets, Constraint $constraint): void $eventId = null; foreach ($tickets as $ticket) { - if (!($ticket instanceof Ticket) || !$ticket->getTicketEventType() instanceof TicketEventType || $ticket->getTicketEventType()->getTicketType()->getIsRestrictedToMembers() === false) { + if (!($ticket instanceof Ticket) || !$ticket->getTicketEventType() instanceof TicketEventType || $ticket->getTicketEventType()->ticketType->getIsRestrictedToMembers() === false) { continue; } if ($eventId === null) { - $eventId = $ticket->getTicketEventType()->getEventId(); + $eventId = $ticket->getTicketEventType()->eventId; } $restrictedTickets++; } diff --git a/sources/AppBundle/Event/Validator/Constraints/EarlyBirdTicketValidator.php b/sources/AppBundle/Event/Validator/Constraints/EarlyBirdTicketValidator.php index cb5ce61de..c8b474404 100644 --- a/sources/AppBundle/Event/Validator/Constraints/EarlyBirdTicketValidator.php +++ b/sources/AppBundle/Event/Validator/Constraints/EarlyBirdTicketValidator.php @@ -23,15 +23,14 @@ public function validate($value, Constraint $constraint): void $count = 0; foreach ($value as $index => $ticket) { - if ($ticket->getTicketEventType() - && $ticket->getTicketEventType()->getTicketType() - && $ticket->getTicketEventType()->getTicketType()->isEarly()) { + if ($ticket->getTicketEventType()?->ticketType !== null + && $ticket->getTicketEventType()->ticketType->isEarly()) { $count++; // On autorise qu'un seul ticket if ($count > 1) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ ticket_pretty_name }}', $ticket->getTicketEventType()->getTicketType()->getPrettyName()) + ->setParameter('{{ ticket_pretty_name }}', $ticket->getTicketEventType()->ticketType->getPrettyName()) ->atPath('[' . $index . '].ticketEventType') ->addViolation() ; diff --git a/sources/AppBundle/Event/Validator/Constraints/LoggedInMemberValidator.php b/sources/AppBundle/Event/Validator/Constraints/LoggedInMemberValidator.php index f63a922c5..677caa4d7 100644 --- a/sources/AppBundle/Event/Validator/Constraints/LoggedInMemberValidator.php +++ b/sources/AppBundle/Event/Validator/Constraints/LoggedInMemberValidator.php @@ -6,7 +6,7 @@ use AppBundle\Association\Model\User; use AppBundle\Event\Model\Ticket; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\TicketEventType; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -22,7 +22,7 @@ public function validate($ticket, Constraint $constraint): void throw new UnexpectedTypeException($constraint, LoggedInMember::class); } - if (!($ticket instanceof Ticket) || !$ticket->getTicketEventType() instanceof TicketEventType || $ticket->getTicketEventType()->getTicketType()->getIsRestrictedToMembers() === false) { + if (!($ticket instanceof Ticket) || !$ticket->getTicketEventType() instanceof TicketEventType || $ticket->getTicketEventType()->ticketType->getIsRestrictedToMembers() === false) { return ; } diff --git a/sources/AppBundle/Event/Validator/Constraints/PublicTicketValidator.php b/sources/AppBundle/Event/Validator/Constraints/PublicTicketValidator.php index c530ef375..9772f9347 100644 --- a/sources/AppBundle/Event/Validator/Constraints/PublicTicketValidator.php +++ b/sources/AppBundle/Event/Validator/Constraints/PublicTicketValidator.php @@ -5,7 +5,7 @@ namespace AppBundle\Event\Validator\Constraints; use AppBundle\Event\Model\Ticket; -use AppBundle\Event\Model\TicketEventType; +use AppBundle\Event\Entity\TicketEventType; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -22,7 +22,7 @@ public function validate($ticket, Constraint $constraint): void return ; } - if ($ticket->getTicketEventType()->getTicketType()->getIsRestrictedToMembers() === true) { + if ($ticket->getTicketEventType()->ticketType->getIsRestrictedToMembers() === true) { $this->context->buildViolation($constraint->messageNotLoggedIn) ->atPath('email') ->addViolation(); diff --git a/sources/AppBundle/Event/Validator/Constraints/TicketsCfpSubmitterValidator.php b/sources/AppBundle/Event/Validator/Constraints/TicketsCfpSubmitterValidator.php index c9e77557a..3d36433ea 100644 --- a/sources/AppBundle/Event/Validator/Constraints/TicketsCfpSubmitterValidator.php +++ b/sources/AppBundle/Event/Validator/Constraints/TicketsCfpSubmitterValidator.php @@ -19,15 +19,14 @@ public function validate($value, Constraint $constraint): void $specialCFPSubmitter = 0; foreach ($value as $index => $ticket) { - if ($ticket->getTicketEventType() - && $ticket->getTicketEventType()->getTicketType() - && $ticket->getTicketEventType()->getTicketType()->getIsRestrictedToCfpSubmitter()) { + if ($ticket->getTicketEventType()?->ticketType !== null + && $ticket->getTicketEventType()->ticketType->getIsRestrictedToCfpSubmitter()) { $specialCFPSubmitter++; // On autorise qu'un seul ticket au tarif CFP submitter if ($specialCFPSubmitter > 1) { $this->context->buildViolation($constraint->messageTooMuchCfpSubmitterTickets) - ->setParameter('{{ ticket_pretty_name }}', $ticket->getTicketEventType()->getTicketType()->getPrettyName()) + ->setParameter('{{ ticket_pretty_name }}', $ticket->getTicketEventType()->ticketType->getPrettyName()) ->atPath('[' . $index . '].ticketEventType') ->addViolation() ; diff --git a/tests/integration/AppBundle/Event/Entity/Repository/TicketEventTypeRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/TicketEventTypeRepositoryTest.php new file mode 100644 index 000000000..3da9ba1f2 --- /dev/null +++ b/tests/integration/AppBundle/Event/Entity/Repository/TicketEventTypeRepositoryTest.php @@ -0,0 +1,132 @@ +get(TicketEventTypeRepository::class); + $this->insertTicketEventTypes(); + + $ticketEventTypes = $repository->getTicketsByEvent($this->buildEvent(999)); + + self::assertCount(2, $ticketEventTypes); + self::assertSame(9001, $ticketEventTypes[0]->ticketTypeId); + self::assertSame(150.0, $ticketEventTypes[0]->price); + self::assertSame(2, $ticketEventTypes[0]->maxTickets); + self::assertSame('Tarif public', $ticketEventTypes[0]->ticketType->getPrettyName()); + self::assertSame(9002, $ticketEventTypes[1]->ticketTypeId); + self::assertTrue($ticketEventTypes[1]->ticketType->getIsRestrictedToMembers()); + } + + public function testGetTicketsByEventAvecFiltrePublic(): void + { + $repository = self::getContainer()->get(TicketEventTypeRepository::class); + $this->insertTicketEventTypes(); + + $ticketEventTypes = $repository->getTicketsByEvent($this->buildEvent(999), false); + + self::assertCount(3, $ticketEventTypes); + self::assertSame(9003, $ticketEventTypes[2]->ticketTypeId); + } + + public function testGetTicketsByEventAvecFiltresDeDates(): void + { + $repository = self::getContainer()->get(TicketEventTypeRepository::class); + $this->insertTicketEventTypes(); + + // seul le tarif passe (date_end deja passee) est exclu quand on retire les tarifs passes + $sansPasses = $repository->getTicketsByEvent($this->buildEvent(999), false, TicketEventTypeRepository::REMOVE_PAST_TICKETS); + self::assertCount(2, $sansPasses); + self::assertSame(9002, $sansPasses[0]->ticketTypeId); + + // seul le tarif futur est exclu quand on retire les tarifs futurs + $sansFuturs = $repository->getTicketsByEvent($this->buildEvent(999), false, TicketEventTypeRepository::REMOVE_FUTURE_TICKETS); + self::assertCount(2, $sansFuturs); + self::assertNotContains(9003, array_map(static fn(TicketEventType $t): int => $t->ticketTypeId, $sansFuturs)); + + // la combinaison des deux garde uniquement les tarifs en cours de vente + $actuels = $repository->getTicketsByEvent($this->buildEvent(999), false, TicketEventTypeRepository::ACTUAL_TICKETS_ONLY); + self::assertCount(1, $actuels); + self::assertSame(9002, $actuels[0]->ticketTypeId); + } + + public function testDoesEventHasRestrictedToMembersTickets(): void + { + $repository = self::getContainer()->get(TicketEventTypeRepository::class); + $this->insertTicketEventTypes(); + + self::assertTrue($repository->doesEventHasRestrictedToMembersTickets($this->buildEvent(999))); + self::assertFalse($repository->doesEventHasRestrictedToMembersTickets($this->buildEvent(998))); + } + + public function testSaveInsereEtMetAJourAvecClePrimaireComposee(): void + { + $repository = self::getContainer()->get(TicketEventTypeRepository::class); + + $ticketEventType = new TicketEventType(); + $ticketEventType->ticketTypeId = 9001; + $ticketEventType->eventId = 777; + $ticketEventType->price = 99.0; + $ticketEventType->dateStart = new \DateTime('2020-01-01 10:00:00'); + $ticketEventType->dateEnd = new \DateTime('2030-01-01 18:00:00'); + $ticketEventType->description = 'Nouveau tarif'; + $ticketEventType->maxTickets = 5; + $repository->save($ticketEventType); + + $saved = $repository->find(['ticketTypeId' => 9001, 'eventId' => 777]); + self::assertNotNull($saved); + self::assertSame(99.0, $saved->price); + self::assertSame(5, $saved->maxTickets); + self::assertSame('Nouveau tarif', $saved->description); + + $ticketEventType->price = 100.0; + $ticketEventType->maxTickets = null; + $repository->save($ticketEventType); + + $updated = $repository->find(['ticketTypeId' => 9001, 'eventId' => 777]); + self::assertNotNull($updated); + self::assertSame(100.0, $updated->price); + self::assertNull($updated->maxTickets); + } + + private function buildEvent(int $eventId): Event + { + $event = new Event(); + $event->setId($eventId); + + return $event; + } + + private function insertTicketEventTypes(): void + { + $connection = self::getContainer()->get(Connection::class); + + $tarifs = [ + ['id' => 9001, 'technical_name' => 'T_PUBLIC', 'pretty_name' => 'Tarif public', 'public' => 1, 'members_only' => 0, 'cfp_submitter_only' => 0, 'default_price' => 150.0, 'active' => 1, 'day' => 'one'], + ['id' => 9002, 'technical_name' => 'T_MEMBERS', 'pretty_name' => 'Tarif membres', 'public' => 1, 'members_only' => 1, 'cfp_submitter_only' => 0, 'default_price' => 100.0, 'active' => 1, 'day' => 'two'], + ['id' => 9003, 'technical_name' => 'T_CACHE', 'pretty_name' => 'Tarif cache', 'public' => 0, 'members_only' => 0, 'cfp_submitter_only' => 0, 'default_price' => 50.0, 'active' => 1, 'day' => 'one'], + ]; + foreach ($tarifs as $tarif) { + $connection->insert('afup_forum_tarif', $tarif); + } + + $ticketEventTypes = [ + ['id_tarif' => 9001, 'id_event' => 999, 'price' => 150.0, 'date_start' => '2009-01-01 10:00:00', 'date_end' => '2020-01-01 12:00:00', 'description' => 'Tarif passe', 'max_tickets' => 2], + ['id_tarif' => 9002, 'id_event' => 999, 'price' => 100.0, 'date_start' => '2010-01-01 10:00:00', 'date_end' => '2099-12-31 12:00:00', 'description' => 'Tarif en cours', 'max_tickets' => 10], + ['id_tarif' => 9003, 'id_event' => 999, 'price' => 50.0, 'date_start' => '2099-01-01 10:00:00', 'date_end' => '2099-12-31 12:00:00', 'description' => 'Tarif futur', 'max_tickets' => 20], + ]; + foreach ($ticketEventTypes as $ticketEventType) { + $connection->insert('afup_forum_tarif_event', $ticketEventType); + } + } +}