Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 0 additions & 66 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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\\<AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice\\>\\:\\: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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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\\<object\\>\\.$#',
'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\\<object\\>\\:\\: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',
Expand Down
6 changes: 6 additions & 0 deletions sources/AppBundle/Association/Entity/Utilisateur.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Expand Down
29 changes: 17 additions & 12 deletions sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace AppBundle\Event\Entity\Repository;

use AppBundle\Doctrine\EntityRepository;
use AppBundle\Association\Entity\Utilisateur;
use AppBundle\Event\Entity\Inscription;
use AppBundle\Event\Entity\TicketSpecialPrice;
use AppBundle\Event\Model\Ticket;
use DateTimeImmutable;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends EntityRepository<TicketSpecialPrice>
*/
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<array{specialPrice: TicketSpecialPrice, creatorPrenom: ?string, creatorNom: ?string, used: bool}>
*/
public function getByEvent(int $eventId): array
{
/** @var list<TicketSpecialPrice> $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;
}
}
43 changes: 43 additions & 0 deletions sources/AppBundle/Event/Entity/TicketSpecialPrice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace AppBundle\Event\Entity;

use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: TicketSpecialPriceRepository::class)]
#[ORM\Table(name: 'afup_forum_special_price')]
class TicketSpecialPrice
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int $id;

#[ORM\Column(name: 'id_event', nullable: false)]
public int $eventId;

#[ORM\Column(nullable: false)]
public string $token;

#[ORM\Column(nullable: true)]
public ?float $price = null;

#[ORM\Column(name: 'date_start', type: 'datetime_immutable', nullable: false)]
public DateTimeImmutable $dateStart;

#[ORM\Column(name: 'date_end', type: 'datetime_immutable', nullable: false)]
public DateTimeImmutable $dateEnd;

#[ORM\Column(nullable: false)]
public string $description;

#[ORM\Column(name: 'created_on', type: 'datetime_immutable', nullable: false)]
public DateTimeImmutable $createdOn;

#[ORM\Column(name: 'creator_id', nullable: false)]
public int $creatorId;
}
Loading
Loading