diff --git a/phpstan-baseline.php b/phpstan-baseline.php index f85ead6d8..6cd6cb525 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -2905,12 +2905,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/EventAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot clone DateTime\\|null\\.$#', - 'identifier' => 'clone.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$reference of method AppBundle\\\\Event\\\\Invoice\\\\EventInvoicePdfGenerator\\:\\:generateQuote\\(\\) expects string, string\\|null given\\.$#', 'identifier' => 'argument.type', @@ -3217,18 +3211,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/SpeakersManagementAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$entity of method CCMBenchmark\\\\Ting\\\\Repository\\\\Repository\\\\:\\:save\\(\\) expects AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$eventId of method AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice\\:\\:setEventId\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method getId\\(\\) on AppBundle\\\\Event\\\\Model\\\\SponsorTicket\\|null\\.$#', 'identifier' => 'method.nonObject', @@ -5545,12 +5527,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketInvoiceType.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Class AppBundle\\\\Event\\\\Form\\\\TicketSpecialPriceType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#', - 'identifier' => 'missingType.generics', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketSpecialPriceType.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', 'identifier' => 'method.nonObject', @@ -5593,24 +5569,12 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$event of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:findUnusedToken\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Event, AppBundle\\\\Event\\\\Model\\\\Event\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#2 \\$event of method AppBundle\\\\Event\\\\Ticket\\\\TicketTypeAvailability\\:\\:getStock\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Event, AppBundle\\\\Event\\\\Model\\\\Event\\|null given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$token of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:findUnusedToken\\(\\) expects string\\|null, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php', -]; $ignoreErrors[] = [ 'message' => '#^Class AppBundle\\\\Event\\\\Form\\\\VoteType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#', 'identifier' => 'missingType.generics', @@ -7165,36 +7129,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:findUnusedToken\\(\\) should return AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice\\|null but returns mixed\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:getByEvent\\(\\) 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/TicketSpecialPriceRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\: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/TicketSpecialPriceRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\: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/TicketSpecialPriceRepository.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/TicketSpecialPriceRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketTypeRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', diff --git a/sources/AppBundle/Association/Entity/Utilisateur.php b/sources/AppBundle/Association/Entity/Utilisateur.php index ab4cf3e9e..1c28bd441 100644 --- a/sources/AppBundle/Association/Entity/Utilisateur.php +++ b/sources/AppBundle/Association/Entity/Utilisateur.php @@ -17,4 +17,10 @@ class Utilisateur #[ORM\Column(length: 255, nullable: true)] public ?string $email = null; + + #[ORM\Column(name: 'prenom', nullable: false)] + public string $firstname; + + #[ORM\Column(name: 'nom', nullable: false)] + public string $lastname; } diff --git a/sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php b/sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php index 9b17018c1..07babef48 100644 --- a/sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php +++ b/sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php @@ -5,8 +5,8 @@ namespace AppBundle\Controller\Admin\Event; use AppBundle\Event\AdminEventSelection; -use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository; -use AppBundle\Event\Model\TicketSpecialPrice; +use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository; +use AppBundle\Event\Entity\TicketSpecialPrice; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -16,15 +16,13 @@ public function __construct(private readonly TicketSpecialPriceRepository $ticke public function __invoke(int $id, AdminEventSelection $eventSelection): Response { - $specialPrice = $this->ticketSpecialPriceRepository->get($id); + $specialPrice = $this->ticketSpecialPriceRepository->find($id); if (!$specialPrice instanceof TicketSpecialPrice) { throw $this->createNotFoundException(); } - $newDateEnd = clone $specialPrice->getDateEnd(); - $newDateEnd->modify(sprintf('+%d days', SpecialPriceAction::EXTEND_DAYS)); - $specialPrice->setDateEnd($newDateEnd); + $specialPrice->dateEnd = $specialPrice->dateEnd->modify(sprintf('+%d days', SpecialPriceAction::EXTEND_DAYS)); $this->ticketSpecialPriceRepository->save($specialPrice); diff --git a/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php b/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php index dadf96393..82af4de56 100644 --- a/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php +++ b/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php @@ -5,11 +5,11 @@ namespace AppBundle\Controller\Admin\Event; use AppBundle\Event\AdminEventSelection; +use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository; +use AppBundle\Event\Entity\TicketSpecialPrice; use AppBundle\Event\Form\TicketSpecialPriceType; -use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository; -use AppBundle\Event\Model\TicketSpecialPrice; use AppBundle\Security\Authentication; -use DateTime; +use DateTimeImmutable; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,15 +26,20 @@ public function __construct( public function __invoke(Request $request, AdminEventSelection $eventSelection): Response { $event = $eventSelection->event; + $eventId = $event->getId(); + if ($eventId === null) { + throw $this->createNotFoundException(); + } + + $dateEnd = DateTimeImmutable::createFromMutable($event->getDateEndSales()); $specialPrice = new TicketSpecialPrice(); - $specialPrice - ->setToken(base64_encode(random_bytes(30))) - ->setEventId($event->getId()) - ->setDateStart(new DateTime()) - ->setDateEnd($event->getDateEndSales()) - ->setCreatedOn(new DateTime()) - ->setCreatorId($this->authentication->getAfupUser()->getId()); + $specialPrice->token = base64_encode(random_bytes(30)); + $specialPrice->eventId = $eventId; + $specialPrice->dateStart = new DateTimeImmutable(); + $specialPrice->dateEnd = $dateEnd; + $specialPrice->createdOn = new DateTimeImmutable(); + $specialPrice->creatorId = $this->authentication->getAfupUser()->getId(); $form = $this->createForm(TicketSpecialPriceType::class, $specialPrice); $form->handleRequest($request); @@ -45,12 +50,12 @@ public function __invoke(Request $request, AdminEventSelection $eventSelection): $this->addFlash('notice', 'Le token a été enregistré'); return $this->redirectToRoute('admin_event_special_price', [ - 'id' => $event->getId(), + 'id' => $eventId, ]); } return $this->render('admin/event/special_price.html.twig', [ - 'special_prices' => $this->ticketSpecialPriceRepository->getByEvent($event), + 'special_prices' => $this->ticketSpecialPriceRepository->getByEvent($eventId), 'event' => $event, 'title' => 'Gestion des prix custom', 'form' => $form->createView(), diff --git a/sources/AppBundle/Event/Entity/Repository/TicketSpecialPriceRepository.php b/sources/AppBundle/Event/Entity/Repository/TicketSpecialPriceRepository.php new file mode 100644 index 000000000..63bc11aa9 --- /dev/null +++ b/sources/AppBundle/Event/Entity/Repository/TicketSpecialPriceRepository.php @@ -0,0 +1,135 @@ + + */ +final class TicketSpecialPriceRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, TicketSpecialPrice::class); + } + + /** + * Retourne le token de prix spécial s'il est dans sa fenêtre de validité + * et qu'aucune inscription (hors annulations) ne l'a déjà consommé. + */ + public function findUnusedToken(int $eventId, ?string $token): ?TicketSpecialPrice + { + if (!is_string($token) || $token === '') { + return null; + } + + $now = new DateTimeImmutable(); + + /** @var TicketSpecialPrice|null $ticketSpecialPrice */ + $ticketSpecialPrice = $this->createQueryBuilder('specialPrice') + ->where('specialPrice.token = :token') + ->andWhere('specialPrice.eventId = :eventId') + ->andWhere('specialPrice.dateStart <= :now') + ->andWhere('specialPrice.dateEnd >= :now') + ->andWhere('NOT EXISTS ( + SELECT inscription.id + FROM ' . Inscription::class . ' inscription + WHERE inscription.specialPriceToken = specialPrice.token + AND inscription.etat <> :etatAnnule + )') + ->setMaxResults(1) + ->setParameter('token', $token) + ->setParameter('eventId', $eventId) + ->setParameter('now', $now) + ->setParameter('etatAnnule', Ticket::STATUS_CANCELLED) + ->getQuery() + ->getOneOrNullResult() + ; + + return $ticketSpecialPrice; + } + + /** + * Liste des tokens d'un évènement avec le nom du créateur et l'indicateur + * d'utilisation (une inscription non annulée a consommé le token). + * + * @return list + */ + public function getByEvent(int $eventId): array + { + /** @var list $specialPrices */ + $specialPrices = $this->findBy(['eventId' => $eventId], ['id' => 'DESC']); + if ($specialPrices === []) { + return []; + } + + // Comptage des inscriptions (hors annulées) qui ont consommé chaque token + $tokens = array_map( + static fn(TicketSpecialPrice $specialPrice): string => $specialPrice->token, + $specialPrices, + ); + + $queryBuilder = $this->getEntityManager()->createQueryBuilder(); + $queryBuilder + ->select('inscription.specialPriceToken AS token', 'COUNT(inscription.id) AS nbInscriptions') + ->from(Inscription::class, 'inscription') + ->where('inscription.specialPriceToken IN (:tokens)') + ->andWhere('inscription.etat <> :etatAnnule') + ->groupBy('inscription.specialPriceToken') + ->setParameter('tokens', $tokens, ArrayParameterType::STRING) + ->setParameter('etatAnnule', Ticket::STATUS_CANCELLED) + ; + + $usedCounts = []; + foreach ($queryBuilder->getQuery()->getArrayResult() as $row) { + if (!is_array($row) || !is_string($row['token'] ?? null)) { + continue; // le groupe par token garantit la présence de la clé + } + + $token = $row['token']; + $nbInscriptions = $row['nbInscriptions'] ?? null; + if (is_int($nbInscriptions)) { + $usedCounts[$token] = $nbInscriptions > 0; + } + } + + // Noms des créateurs, chargés depuis l'entité Utilisateur (table afup_personnes_physiques) + $creatorIds = array_map( + static fn(TicketSpecialPrice $specialPrice): int => $specialPrice->creatorId, + $specialPrices, + ); + $creators = $this->getEntityManager() + ->getRepository(Utilisateur::class) + ->findBy(['id' => array_values(array_unique($creatorIds))]) + ; + $creatorsById = []; + foreach ($creators as $creator) { + if ($creator->id !== null) { + $creatorsById[$creator->id] = $creator; + } + } + + $byEvent = []; + foreach ($specialPrices as $specialPrice) { + $creator = $creatorsById[$specialPrice->creatorId] ?? null; + $byEvent[] = [ + 'specialPrice' => $specialPrice, + 'creatorPrenom' => $creator?->firstname, + 'creatorNom' => $creator?->lastname, + 'used' => $usedCounts[$specialPrice->token] ?? false, + ]; + } + + return $byEvent; + } +} diff --git a/sources/AppBundle/Event/Entity/TicketSpecialPrice.php b/sources/AppBundle/Event/Entity/TicketSpecialPrice.php new file mode 100644 index 000000000..ea0c7c6bb --- /dev/null +++ b/sources/AppBundle/Event/Entity/TicketSpecialPrice.php @@ -0,0 +1,43 @@ + + */ class TicketSpecialPriceType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/sources/AppBundle/Event/Form/TicketType.php b/sources/AppBundle/Event/Form/TicketType.php index 905f5962a..90ead8193 100644 --- a/sources/AppBundle/Event/Form/TicketType.php +++ b/sources/AppBundle/Event/Form/TicketType.php @@ -9,11 +9,11 @@ use AppBundle\Event\Entity\BilleteriePrivee; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\TicketEventTypeRepository; -use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository; +use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository; use AppBundle\Event\Model\Repository\TicketTypeRepository; use AppBundle\Event\Model\Ticket; use AppBundle\Event\Model\TicketEventType; -use AppBundle\Event\Model\TicketSpecialPrice; +use AppBundle\Event\Entity\TicketSpecialPrice; use AppBundle\Event\Ticket\TicketTypeAvailability; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Exception\RuntimeException; @@ -91,8 +91,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $filteredEventTickets[] = $eventTicket; } - $event = $this->eventRepository->get($options['event_id']); - $ticketSpecialPrice = $this->ticketSpecialPriceRepository->findUnusedToken($event, $options['special_price_token']); + $specialPriceToken = $options['special_price_token']; + $ticketSpecialPrice = null; + if (is_int($options['event_id'])) { + $ticketSpecialPrice = $this->ticketSpecialPriceRepository->findUnusedToken( + $options['event_id'], + is_string($specialPriceToken) ? $specialPriceToken : null, + ); + } if (null !== $ticketSpecialPrice) { $filteredEventTickets = $this->createSpecialPriceTicketEventType($ticketSpecialPrice, $filteredEventTickets); @@ -186,18 +192,12 @@ private function createSpecialPriceTicketEventType(TicketSpecialPrice $ticketSpe return $filteredEventTickets; } - $dateStart = $ticketSpecialPrice->getDateStart(); - $dateEnd = $ticketSpecialPrice->getDateEnd(); - if ($dateStart === null || $dateEnd === null) { - return $filteredEventTickets; - } - $eToken = new TicketEventType(); - $eToken->setDateStart($dateStart); - $eToken->setDateEnd($dateEnd); - $eToken->setPrice($ticketSpecialPrice->getPrice()); + $eToken->setDateStart(\DateTime::createFromImmutable($ticketSpecialPrice->dateStart)); + $eToken->setDateEnd(\DateTime::createFromImmutable($ticketSpecialPrice->dateEnd)); + $eToken->setPrice($ticketSpecialPrice->price ?? 0.0); $eToken->setTicketType($ticketType); - $eToken->setEventId($ticketSpecialPrice->getEventId()); + $eToken->setEventId($ticketSpecialPrice->eventId); $eToken->setTicketTypeId(Ticket::TYPE_SPECIAL_PRICE); return [$eToken]; diff --git a/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php b/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php deleted file mode 100644 index 92514c2fb..000000000 --- a/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php +++ /dev/null @@ -1,130 +0,0 @@ - - */ -class TicketSpecialPriceRepository extends Repository implements MetadataInitializer -{ - public function findUnusedToken(Event $event, ?string $token): ?TicketSpecialPrice - { - $query = $this - ->getPreparedQuery( - 'SELECT afup_forum_special_price.* - FROM afup_forum_special_price - LEFT JOIN ( - SELECT DISTINCT afup_inscription_forum.special_price_token as used_token - FROM afup_inscription_forum - WHERE afup_inscription_forum.etat <> :etat_annule - ) as used_tokens ON (afup_forum_special_price.token = used_tokens.used_token) - WHERE afup_forum_special_price.token = :token - AND used_tokens.used_token IS NULL - AND afup_forum_special_price.date_start <= NOW() - AND afup_forum_special_price.date_end >= NOW() - AND afup_forum_special_price.id_event = :id_event - LIMIT 1 - ', - )->setParams(['token' => $token, 'id_event' => $event->getId(), 'etat_annule' => Ticket::STATUS_CANCELLED]); - - return $query->query($this->getCollection(new HydratorSingleObject()))->first(); - } - - /** - * @return CollectionInterface - */ - public function getByEvent(Event $event) - { - $hydrator = new JoinHydrator(); - $hydrator->aggregateOn('special_price', 'inscription', 'getId'); - - $query = $this->getPreparedQuery( - 'SELECT special_price.*, inscription.*, creator.* - FROM afup_forum_special_price as special_price - LEFT JOIN afup_inscription_forum as inscription ON (special_price.token = inscription.special_price_token AND inscription.etat <> :etat_annule) - LEFT JOIN afup_personnes_physiques as creator ON (special_price.creator_id = creator.id) - WHERE special_price.id_event = :id_event - ORDER BY special_price.id_event, special_price.id DESC, inscription.id DESC - ', - )->setParams(['id_event' => $event->getId(), 'etat_annule' => Ticket::STATUS_CANCELLED]); - - return $query->query($this->getCollection($hydrator)); - } - - /** - * @inheritDoc - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - - $metadata->setEntity(TicketSpecialPrice::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_forum_special_price'); - - $metadata - ->addField([ - 'columnName' => 'id', - 'fieldName' => 'id', - 'primary' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'id_event', - 'fieldName' => 'eventId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'token', - 'fieldName' => 'token', - 'type' => 'string', - ]) - ->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' => 'created_on', - 'fieldName' => 'createdOn', - 'type' => 'datetime', - ]) - ->addField([ - 'columnName' => 'creator_id', - 'fieldName' => 'creatorId', - 'type' => 'int', - ]) - ; - - return $metadata; - } -} diff --git a/sources/AppBundle/Event/Model/TicketSpecialPrice.php b/sources/AppBundle/Event/Model/TicketSpecialPrice.php deleted file mode 100644 index b174a6cff..000000000 --- a/sources/AppBundle/Event/Model/TicketSpecialPrice.php +++ /dev/null @@ -1,257 +0,0 @@ -id; - } - - /** - * @param int $id - * - * @return $this - */ - public function setId($id): self - { - $this->propertyChanged('id', $this->id, $id); - $this->id = $id; - - return $this; - } - - /** - * @return int - */ - public function getEventId() - { - return $this->eventId; - } - - /** - * @param int $eventId - * - * @return $this - */ - 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 - * - * @return $this - */ - 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; - } - - /** - * @return $this - */ - 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; - } - - /** - * @return $this - */ - 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 - - * @return $this - */ - 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; - } - - /** - * @return $this - */ - public function setTicketType(TicketType $ticketType): self - { - $this->ticketType = $ticketType; - - return $this; - } - - /** - * @return string - */ - public function getToken() - { - return $this->token; - } - - /** - * @param string $token - * - * @return $this - */ - public function setToken($token): self - { - $this->propertyChanged('token', $this->token, $token); - $this->token = $token; - - return $this; - } - - /** - * @return \DateTime - */ - public function getCreatedOn() - { - return $this->createdOn; - } - - /** - * @param \DateTime $createdOn - * - * @return $this - */ - public function setCreatedOn($createdOn): self - { - $this->propertyChanged('createdOn', $this->createdOn, $createdOn); - $this->createdOn = $createdOn; - - return $this; - } - - /** - * @return int - */ - public function getCreatorId() - { - return $this->creatorId; - } - - /** - * @param int $creatorId - * - * @return $this - */ - public function setCreatorId($creatorId): self - { - $this->propertyChanged('creatorId', $this->creatorId, $creatorId); - $this->creatorId = $creatorId; - - return $this; - } -} diff --git a/templates/admin/event/special_price.html.twig b/templates/admin/event/special_price.html.twig index 1c3ce1341..f0df5e60b 100644 --- a/templates/admin/event/special_price.html.twig +++ b/templates/admin/event/special_price.html.twig @@ -21,28 +21,28 @@ {% for row in special_prices %} - {{ row.special_price.description }} - {{ row.special_price.createdOn|date('d/m/Y') }} - {% if row.creator %}{{ row.creator.firstName }} {{ row.creator.lastName }}{% endif %} - {{ row.special_price.token }} - {{ row.special_price.price }} - {{ row.special_price.dateStart|date('d/m/Y') }} - {{ row.special_price.dateEnd|date('d/m/Y') }} + {{ row.specialPrice.description }} + {{ row.specialPrice.createdOn|date('d/m/Y') }} + {% if row.creatorPrenom %}{{ row.creatorPrenom }} {{ row.creatorNom }}{% endif %} + {{ row.specialPrice.token }} + {{ row.specialPrice.price }} + {{ row.specialPrice.dateStart|date('d/m/Y') }} + {{ row.specialPrice.dateEnd|date('d/m/Y') }} - {% if row['.aggregation']['inscription']|length > 0 %} + {% if row.used %} oui {% else %} non {% endif %} - - Lien + Lien {% endfor %} diff --git a/tests/integration/AppBundle/Event/Entity/Repository/TicketSpecialPriceRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/TicketSpecialPriceRepositoryTest.php new file mode 100644 index 000000000..d5034a748 --- /dev/null +++ b/tests/integration/AppBundle/Event/Entity/Repository/TicketSpecialPriceRepositoryTest.php @@ -0,0 +1,184 @@ +get(TicketSpecialPriceRepository::class); + self::assertInstanceOf(TicketSpecialPriceRepository::class, $repository); + $this->repository = $repository; + + $connection = self::getContainer()->get(Connection::class); + self::assertInstanceOf(Connection::class, $connection); + $this->connection = $connection; + } + + public function testSaveEtFind(): void + { + $specialPrice = $this->buildToken('TOKEN-SAVE', now: new \DateTimeImmutable('-1 hour')); + $this->repository->save($specialPrice); + + /** @var TicketSpecialPrice|null $loaded */ + $loaded = $this->repository->find($specialPrice->id); + self::assertInstanceOf(TicketSpecialPrice::class, $loaded); + self::assertSame('TOKEN-SAVE', $loaded->token); + self::assertSame(80.0, $loaded->price); + self::assertSame(self::FORUM_ID, $loaded->eventId); + self::assertSame(101, $loaded->creatorId); + } + + public function testFindUnusedTokenRetourneLeTokenDansSaFenetreDeValidite(): void + { + $specialPrice = $this->buildToken('TOKEN-VALIDE', now: new \DateTimeImmutable('-1 hour')); + $this->repository->save($specialPrice); + + $found = $this->repository->findUnusedToken(self::FORUM_ID, 'TOKEN-VALIDE'); + self::assertInstanceOf(TicketSpecialPrice::class, $found); + self::assertSame($specialPrice->id, $found->id); + } + + public function testFindUnusedTokenIgnoreLeTokenDejaUtilise(): void + { + $specialPrice = $this->buildToken('TOKEN-UTILISE', now: new \DateTimeImmutable('-1 hour')); + $this->repository->save($specialPrice); + $this->insertInscription($this->connection, 'TOKEN-UTILISE', 'A', self::FORUM_ID, 0); + + self::assertNull($this->repository->findUnusedToken(self::FORUM_ID, 'TOKEN-UTILISE')); + } + + public function testFindUnusedTokenIgnoreLesInscriptionsAnnulees(): void + { + $specialPrice = $this->buildToken('TOKEN-ANNULE', now: new \DateTimeImmutable('-1 hour')); + $this->repository->save($specialPrice); + $this->insertInscription($this->connection, 'TOKEN-ANNULE', 'B', self::FORUM_ID, Ticket::STATUS_CANCELLED); + + $found = $this->repository->findUnusedToken(self::FORUM_ID, 'TOKEN-ANNULE'); + self::assertInstanceOf(TicketSpecialPrice::class, $found); + } + + public function testFindUnusedTokenIgnoreUnTokenHorsFenetreDeValidite(): void + { + $this->repository->save($this->buildToken('TOKEN-FUTUR', now: null, dateStart: new \DateTimeImmutable('+1 hour'))); + $this->repository->save($this->buildToken('TOKEN-PASSE', now: null, dateEnd: new \DateTimeImmutable('-1 hour'))); + + self::assertNull($this->repository->findUnusedToken(self::FORUM_ID, 'TOKEN-FUTUR')); + self::assertNull($this->repository->findUnusedToken(self::FORUM_ID, 'TOKEN-PASSE')); + } + + public function testFindUnusedTokenIgnoreUnTokenDAutreEvenement(): void + { + $specialPrice = $this->buildToken('TOKEN-AUTRE-FORUM', now: new \DateTimeImmutable('-1 hour')); + $specialPrice->eventId = self::FORUM_ID + 1; + $this->repository->save($specialPrice); + + self::assertNull($this->repository->findUnusedToken(self::FORUM_ID, 'TOKEN-AUTRE-FORUM')); + } + + public function testGetByEventRetourneLesTokensAvecCreateurEtUtilisation(): void + { + $this->insertUtilisateur($this->connection, 200, 'Jean', 'Dupont'); + $this->insertUtilisateur($this->connection, 201, 'Marie', 'Martin'); + + $unused = $this->buildToken('TOKEN-LISTE-UNUSED', now: new \DateTimeImmutable('-1 hour'), creatorId: 201); + $this->repository->save($unused); + $used = $this->buildToken('TOKEN-LISTE-USED', now: new \DateTimeImmutable('-1 hour'), creatorId: 200); + $this->repository->save($used); + $this->insertInscription($this->connection, 'TOKEN-LISTE-USED', 'C', self::FORUM_ID, 0); + // Une inscription annulee ne consomme pas le token + $this->insertInscription($this->connection, 'TOKEN-LISTE-UNUSED', 'D', self::FORUM_ID, Ticket::STATUS_CANCELLED); + + $rows = $this->repository->getByEvent(self::FORUM_ID); + self::assertCount(2, $rows); + + // Tri par id decroissant : le dernier token cree apparait en premier + /** @var array{specialPrice: TicketSpecialPrice, creatorPrenom: ?string, creatorNom: ?string, used: bool} $firstRow */ + $firstRow = $rows[0]; + /** @var array{specialPrice: TicketSpecialPrice, creatorPrenom: ?string, creatorNom: ?string, used: bool} $secondRow */ + $secondRow = $rows[1]; + + self::assertSame('TOKEN-LISTE-USED', $firstRow['specialPrice']->token); + self::assertTrue($firstRow['used']); + self::assertSame('Jean', $firstRow['creatorPrenom']); + self::assertSame('Dupont', $firstRow['creatorNom']); + + self::assertSame('TOKEN-LISTE-UNUSED', $secondRow['specialPrice']->token); + self::assertFalse($secondRow['used']); + self::assertSame('Marie', $secondRow['creatorPrenom']); + self::assertSame('Martin', $secondRow['creatorNom']); + } + + public function testGetByEventSansCreateurConnuRenvoieDesNomsNuls(): void + { + $specialPrice = $this->buildToken('TOKEN-SANS-CREATEUR', now: new \DateTimeImmutable('-1 hour')); + $specialPrice->creatorId = 999999; + $this->repository->save($specialPrice); + + $rows = $this->repository->getByEvent(self::FORUM_ID); + self::assertCount(1, $rows); + self::assertNull($rows[0]['creatorPrenom']); + self::assertNull($rows[0]['creatorNom']); + self::assertFalse($rows[0]['used']); + } + + private function buildToken( + string $token, + ?\DateTimeImmutable $now = null, + ?\DateTimeImmutable $dateStart = null, + ?\DateTimeImmutable $dateEnd = null, + int $creatorId = 101, + ): TicketSpecialPrice { + $now ??= new \DateTimeImmutable(); + $specialPrice = new TicketSpecialPrice(); + $specialPrice->eventId = self::FORUM_ID; + $specialPrice->token = $token; + $specialPrice->price = 80.0; + $specialPrice->dateStart = $dateStart ?? $now; + $specialPrice->dateEnd = $dateEnd ?? $now->modify('+1 day'); + $specialPrice->description = 'Description de test'; + $specialPrice->createdOn = $now; + $specialPrice->creatorId = $creatorId; + + return $specialPrice; + } + + private function insertUtilisateur(Connection $connection, int $id, string $prenom, string $nom): void + { + $connection->insert('afup_personnes_physiques', [ + 'id' => $id, + 'login' => 'user' . $id, + 'roles' => 'ROLE_USER', + 'nom' => $nom, + 'prenom' => $prenom, + 'email' => 'user' . $id . '@example.com', + 'adresse' => '', + 'etat' => 0, + ]); + } + + private function insertInscription(Connection $connection, string $token, string $reference, int $forumId, int $etat): void + { + $connection->insert('afup_inscription_forum', [ + 'reference' => $reference, + 'special_price_token' => $token, + 'id_forum' => $forumId, + 'etat' => $etat, + ]); + } +}