Skip to content

Commit b19bb8a

Browse files
authored
Migre l'entité TicketSpecialPrice de Ting vers Doctrine (#2383) (#2417)
1 parent 1c5d4f1 commit b19bb8a

13 files changed

Lines changed: 442 additions & 535 deletions

File tree

‎phpstan-baseline.php‎

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2767,12 +2767,6 @@
27672767
'count' => 1,
27682768
'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/EventAction.php',
27692769
];
2770-
$ignoreErrors[] = [
2771-
'message' => '#^Cannot clone DateTime\\|null\\.$#',
2772-
'identifier' => 'clone.nonObject',
2773-
'count' => 1,
2774-
'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php',
2775-
];
27762770
$ignoreErrors[] = [
27772771
'message' => '#^Parameter \\#1 \\$reference of method AppBundle\\\\Event\\\\Invoice\\\\EventInvoicePdfGenerator\\:\\:generateQuote\\(\\) expects string, string\\|null given\\.$#',
27782772
'identifier' => 'argument.type',
@@ -3055,18 +3049,6 @@
30553049
'count' => 1,
30563050
'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/SpeakersManagementAction.php',
30573051
];
3058-
$ignoreErrors[] = [
3059-
'message' => '#^Parameter \\#1 \\$entity of method CCMBenchmark\\\\Ting\\\\Repository\\\\Repository\\<AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice\\>\\:\\:save\\(\\) expects AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice, mixed given\\.$#',
3060-
'identifier' => 'argument.type',
3061-
'count' => 1,
3062-
'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php',
3063-
];
3064-
$ignoreErrors[] = [
3065-
'message' => '#^Parameter \\#1 \\$eventId of method AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice\\:\\:setEventId\\(\\) expects int, int\\|null given\\.$#',
3066-
'identifier' => 'argument.type',
3067-
'count' => 1,
3068-
'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php',
3069-
];
30703052
$ignoreErrors[] = [
30713053
'message' => '#^Cannot call method getId\\(\\) on AppBundle\\\\Event\\\\Model\\\\SponsorTicket\\|null\\.$#',
30723054
'identifier' => 'method.nonObject',
@@ -5269,12 +5251,6 @@
52695251
'count' => 1,
52705252
'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketInvoiceType.php',
52715253
];
5272-
$ignoreErrors[] = [
5273-
'message' => '#^Class AppBundle\\\\Event\\\\Form\\\\TicketSpecialPriceType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#',
5274-
'identifier' => 'missingType.generics',
5275-
'count' => 1,
5276-
'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketSpecialPriceType.php',
5277-
];
52785254
$ignoreErrors[] = [
52795255
'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#',
52805256
'identifier' => 'method.nonObject',
@@ -5317,24 +5293,12 @@
53175293
'count' => 1,
53185294
'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php',
53195295
];
5320-
$ignoreErrors[] = [
5321-
'message' => '#^Parameter \\#1 \\$event of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:findUnusedToken\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Event, AppBundle\\\\Event\\\\Model\\\\Event\\|null given\\.$#',
5322-
'identifier' => 'argument.type',
5323-
'count' => 1,
5324-
'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php',
5325-
];
53265296
$ignoreErrors[] = [
53275297
'message' => '#^Parameter \\#2 \\$event of method AppBundle\\\\Event\\\\Ticket\\\\TicketTypeAvailability\\:\\:getStock\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Event, AppBundle\\\\Event\\\\Model\\\\Event\\|null given\\.$#',
53285298
'identifier' => 'argument.type',
53295299
'count' => 1,
53305300
'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php',
53315301
];
5332-
$ignoreErrors[] = [
5333-
'message' => '#^Parameter \\#2 \\$token of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:findUnusedToken\\(\\) expects string\\|null, mixed given\\.$#',
5334-
'identifier' => 'argument.type',
5335-
'count' => 1,
5336-
'path' => __DIR__ . '/sources/AppBundle/Event/Form/TicketType.php',
5337-
];
53385302
$ignoreErrors[] = [
53395303
'message' => '#^Class AppBundle\\\\Event\\\\Form\\\\VoteType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#',
53405304
'identifier' => 'missingType.generics',
@@ -6733,36 +6697,6 @@
67336697
'count' => 1,
67346698
'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketRepository.php',
67356699
];
6736-
$ignoreErrors[] = [
6737-
'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:findUnusedToken\\(\\) should return AppBundle\\\\Event\\\\Model\\\\TicketSpecialPrice\\|null but returns mixed\\.$#',
6738-
'identifier' => 'return.type',
6739-
'count' => 1,
6740-
'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php',
6741-
];
6742-
$ignoreErrors[] = [
6743-
'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:getByEvent\\(\\) return type with generic interface CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface does not specify its types\\: T$#',
6744-
'identifier' => 'missingType.generics',
6745-
'count' => 1,
6746-
'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php',
6747-
];
6748-
$ignoreErrors[] = [
6749-
'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#',
6750-
'identifier' => 'missingType.iterableValue',
6751-
'count' => 1,
6752-
'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php',
6753-
];
6754-
$ignoreErrors[] = [
6755-
'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketSpecialPriceRepository\\:\\:initMetadata\\(\\) should return M of CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata but returns CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\<object\\>\\.$#',
6756-
'identifier' => 'return.type',
6757-
'count' => 1,
6758-
'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php',
6759-
];
6760-
$ignoreErrors[] = [
6761-
'message' => '#^Parameter \\#1 \\$databaseName of method CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\<object\\>\\:\\:setDatabase\\(\\) expects string, mixed given\\.$#',
6762-
'identifier' => 'argument.type',
6763-
'count' => 1,
6764-
'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketSpecialPriceRepository.php',
6765-
];
67666700
$ignoreErrors[] = [
67676701
'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TicketTypeRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#',
67686702
'identifier' => 'missingType.iterableValue',

‎sources/AppBundle/Association/Entity/Utilisateur.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,10 @@ class Utilisateur
1717

1818
#[ORM\Column(length: 255, nullable: true)]
1919
public ?string $email = null;
20+
21+
#[ORM\Column(name: 'prenom', nullable: false)]
22+
public string $firstname;
23+
24+
#[ORM\Column(name: 'nom', nullable: false)]
25+
public string $lastname;
2026
}

‎sources/AppBundle/Controller/Admin/Event/ExtendSpecialPriceAction.php‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace AppBundle\Controller\Admin\Event;
66

77
use AppBundle\Event\AdminEventSelection;
8-
use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository;
9-
use AppBundle\Event\Model\TicketSpecialPrice;
8+
use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository;
9+
use AppBundle\Event\Entity\TicketSpecialPrice;
1010
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1111
use Symfony\Component\HttpFoundation\Response;
1212

@@ -16,15 +16,13 @@ public function __construct(private readonly TicketSpecialPriceRepository $ticke
1616

1717
public function __invoke(int $id, AdminEventSelection $eventSelection): Response
1818
{
19-
$specialPrice = $this->ticketSpecialPriceRepository->get($id);
19+
$specialPrice = $this->ticketSpecialPriceRepository->find($id);
2020

2121
if (!$specialPrice instanceof TicketSpecialPrice) {
2222
throw $this->createNotFoundException();
2323
}
2424

25-
$newDateEnd = clone $specialPrice->getDateEnd();
26-
$newDateEnd->modify(sprintf('+%d days', SpecialPriceAction::EXTEND_DAYS));
27-
$specialPrice->setDateEnd($newDateEnd);
25+
$specialPrice->dateEnd = $specialPrice->dateEnd->modify(sprintf('+%d days', SpecialPriceAction::EXTEND_DAYS));
2826

2927
$this->ticketSpecialPriceRepository->save($specialPrice);
3028

‎sources/AppBundle/Controller/Admin/Event/SpecialPriceAction.php‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
namespace AppBundle\Controller\Admin\Event;
66

77
use AppBundle\Event\AdminEventSelection;
8+
use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository;
9+
use AppBundle\Event\Entity\TicketSpecialPrice;
810
use AppBundle\Event\Form\TicketSpecialPriceType;
9-
use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository;
10-
use AppBundle\Event\Model\TicketSpecialPrice;
1111
use AppBundle\Security\Authentication;
12-
use DateTime;
12+
use DateTimeImmutable;
1313
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1414
use Symfony\Component\HttpFoundation\Request;
1515
use Symfony\Component\HttpFoundation\Response;
@@ -26,15 +26,20 @@ public function __construct(
2626
public function __invoke(Request $request, AdminEventSelection $eventSelection): Response
2727
{
2828
$event = $eventSelection->event;
29+
$eventId = $event->getId();
30+
if ($eventId === null) {
31+
throw $this->createNotFoundException();
32+
}
33+
34+
$dateEnd = DateTimeImmutable::createFromMutable($event->getDateEndSales());
2935

3036
$specialPrice = new TicketSpecialPrice();
31-
$specialPrice
32-
->setToken(base64_encode(random_bytes(30)))
33-
->setEventId($event->getId())
34-
->setDateStart(new DateTime())
35-
->setDateEnd($event->getDateEndSales())
36-
->setCreatedOn(new DateTime())
37-
->setCreatorId($this->authentication->getAfupUser()->getId());
37+
$specialPrice->token = base64_encode(random_bytes(30));
38+
$specialPrice->eventId = $eventId;
39+
$specialPrice->dateStart = new DateTimeImmutable();
40+
$specialPrice->dateEnd = $dateEnd;
41+
$specialPrice->createdOn = new DateTimeImmutable();
42+
$specialPrice->creatorId = $this->authentication->getAfupUser()->getId();
3843

3944
$form = $this->createForm(TicketSpecialPriceType::class, $specialPrice);
4045
$form->handleRequest($request);
@@ -45,12 +50,12 @@ public function __invoke(Request $request, AdminEventSelection $eventSelection):
4550
$this->addFlash('notice', 'Le token a été enregistré');
4651

4752
return $this->redirectToRoute('admin_event_special_price', [
48-
'id' => $event->getId(),
53+
'id' => $eventId,
4954
]);
5055
}
5156

5257
return $this->render('admin/event/special_price.html.twig', [
53-
'special_prices' => $this->ticketSpecialPriceRepository->getByEvent($event),
58+
'special_prices' => $this->ticketSpecialPriceRepository->getByEvent($eventId),
5459
'event' => $event,
5560
'title' => 'Gestion des prix custom',
5661
'form' => $form->createView(),

‎sources/AppBundle/Controller/Admin/Event/Ticket/IndexAction.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
use AppBundle\Event\AdminEventSelection;
88
use AppBundle\Event\Entity\Repository\BilleteriePriveeRepository;
9+
use AppBundle\Event\Entity\Repository\TicketSpecialPriceRepository;
910
use AppBundle\Event\Model\Event;
1011
use AppBundle\Event\Model\EventStats;
1112
use AppBundle\Event\Model\Repository\EventStatsRepository;
1213
use AppBundle\Event\Model\Repository\TicketRepository;
13-
use AppBundle\Event\Model\Repository\TicketSpecialPriceRepository;
1414
use AppBundle\Event\Model\Ticket;
1515
use AppBundle\Event\Model\TicketAggregate;
1616
use AppBundle\Event\Ticket\TicketOffers;
@@ -102,10 +102,10 @@ private function getSpecialPriceLabels(array $tickets): array
102102
}
103103

104104
foreach ($this->ticketSpecialPriceRepository->findByTokens($tokens) as $ticketSpecialPrice) {
105-
if (isset($labels[$ticketSpecialPrice->getToken()])) {
105+
if (isset($labels[$ticketSpecialPrice->token])) {
106106
continue;
107107
}
108-
$labels[$ticketSpecialPrice->getToken()] = sprintf('Token visiteurs - %s', $ticketSpecialPrice->getDescription());
108+
$labels[$ticketSpecialPrice->token] = sprintf('Token visiteurs - %s', $ticketSpecialPrice->description);
109109
}
110110

111111
return $labels;
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AppBundle\Event\Entity\Repository;
6+
7+
use AppBundle\Doctrine\EntityRepository;
8+
use AppBundle\Association\Entity\Utilisateur;
9+
use AppBundle\Event\Entity\Inscription;
10+
use AppBundle\Event\Entity\TicketSpecialPrice;
11+
use AppBundle\Event\Model\Ticket;
12+
use DateTimeImmutable;
13+
use Doctrine\DBAL\ArrayParameterType;
14+
use Doctrine\Persistence\ManagerRegistry;
15+
16+
/**
17+
* @extends EntityRepository<TicketSpecialPrice>
18+
*/
19+
final class TicketSpecialPriceRepository extends EntityRepository
20+
{
21+
public function __construct(ManagerRegistry $registry)
22+
{
23+
parent::__construct($registry, TicketSpecialPrice::class);
24+
}
25+
26+
/**
27+
* Retourne le token de prix spécial s'il est dans sa fenêtre de validité
28+
* et qu'aucune inscription (hors annulations) ne l'a déjà consommé.
29+
*/
30+
public function findUnusedToken(int $eventId, ?string $token): ?TicketSpecialPrice
31+
{
32+
if (!is_string($token) || $token === '') {
33+
return null;
34+
}
35+
36+
$now = new DateTimeImmutable();
37+
38+
/** @var TicketSpecialPrice|null $ticketSpecialPrice */
39+
$ticketSpecialPrice = $this->createQueryBuilder('specialPrice')
40+
->where('specialPrice.token = :token')
41+
->andWhere('specialPrice.eventId = :eventId')
42+
->andWhere('specialPrice.dateStart <= :now')
43+
->andWhere('specialPrice.dateEnd >= :now')
44+
->andWhere('NOT EXISTS (
45+
SELECT inscription.id
46+
FROM ' . Inscription::class . ' inscription
47+
WHERE inscription.specialPriceToken = specialPrice.token
48+
AND inscription.etat <> :etatAnnule
49+
)')
50+
->setMaxResults(1)
51+
->setParameter('token', $token)
52+
->setParameter('eventId', $eventId)
53+
->setParameter('now', $now)
54+
->setParameter('etatAnnule', Ticket::STATUS_CANCELLED)
55+
->getQuery()
56+
->getOneOrNullResult()
57+
;
58+
59+
return $ticketSpecialPrice;
60+
}
61+
62+
/**
63+
* @param list<string> $tokens
64+
* @return list<TicketSpecialPrice>
65+
*/
66+
public function findByTokens(array $tokens): array
67+
{
68+
$tokens = array_values(array_unique($tokens));
69+
if ($tokens === []) {
70+
return [];
71+
}
72+
73+
/** @var list<TicketSpecialPrice> $specialPrices */
74+
$specialPrices = $this->createQueryBuilder('specialPrice')
75+
->where('specialPrice.token IN (:tokens)')
76+
->setParameter('tokens', $tokens, ArrayParameterType::STRING)
77+
->getQuery()
78+
->getResult()
79+
;
80+
81+
return $specialPrices;
82+
}
83+
84+
/**
85+
* Liste des tokens d'un évènement avec le nom du créateur et l'indicateur
86+
* d'utilisation (une inscription non annulée a consommé le token).
87+
*
88+
* @return list<array{specialPrice: TicketSpecialPrice, creatorPrenom: ?string, creatorNom: ?string, used: bool}>
89+
*/
90+
public function getByEvent(int $eventId): array
91+
{
92+
/** @var list<TicketSpecialPrice> $specialPrices */
93+
$specialPrices = $this->findBy(['eventId' => $eventId], ['id' => 'DESC']);
94+
if ($specialPrices === []) {
95+
return [];
96+
}
97+
98+
// Comptage des inscriptions (hors annulées) qui ont consommé chaque token
99+
$tokens = array_map(
100+
static fn(TicketSpecialPrice $specialPrice): string => $specialPrice->token,
101+
$specialPrices,
102+
);
103+
104+
$queryBuilder = $this->getEntityManager()->createQueryBuilder();
105+
$queryBuilder
106+
->select('inscription.specialPriceToken AS token', 'COUNT(inscription.id) AS nbInscriptions')
107+
->from(Inscription::class, 'inscription')
108+
->where('inscription.specialPriceToken IN (:tokens)')
109+
->andWhere('inscription.etat <> :etatAnnule')
110+
->groupBy('inscription.specialPriceToken')
111+
->setParameter('tokens', $tokens, ArrayParameterType::STRING)
112+
->setParameter('etatAnnule', Ticket::STATUS_CANCELLED)
113+
;
114+
115+
$usedCounts = [];
116+
foreach ($queryBuilder->getQuery()->getArrayResult() as $row) {
117+
if (!is_array($row) || !is_string($row['token'] ?? null)) {
118+
continue; // le groupe par token garantit la présence de la clé
119+
}
120+
121+
$token = $row['token'];
122+
$nbInscriptions = $row['nbInscriptions'] ?? null;
123+
if (is_int($nbInscriptions)) {
124+
$usedCounts[$token] = $nbInscriptions > 0;
125+
}
126+
}
127+
128+
// Noms des créateurs, chargés depuis l'entité Utilisateur (table afup_personnes_physiques)
129+
$creatorIds = array_map(
130+
static fn(TicketSpecialPrice $specialPrice): int => $specialPrice->creatorId,
131+
$specialPrices,
132+
);
133+
$creators = $this->getEntityManager()
134+
->getRepository(Utilisateur::class)
135+
->findBy(['id' => array_values(array_unique($creatorIds))])
136+
;
137+
$creatorsById = [];
138+
foreach ($creators as $creator) {
139+
if ($creator->id !== null) {
140+
$creatorsById[$creator->id] = $creator;
141+
}
142+
}
143+
144+
$byEvent = [];
145+
foreach ($specialPrices as $specialPrice) {
146+
$creator = $creatorsById[$specialPrice->creatorId] ?? null;
147+
$byEvent[] = [
148+
'specialPrice' => $specialPrice,
149+
'creatorPrenom' => $creator?->firstname,
150+
'creatorNom' => $creator?->lastname,
151+
'used' => $usedCounts[$specialPrice->token] ?? false,
152+
];
153+
}
154+
155+
return $byEvent;
156+
}
157+
}

0 commit comments

Comments
 (0)