- BilletOrder entity: individual tickets with unique ETICKET-XXXX reference,
billetBuyer link, billet link, isScanned, scannedAt for entry control
- BilletOrderService: generates tickets after payment, creates A4 PDF with
BilletDesign colors if present (default otherwise), real QR code via
endroid/qr-code, event poster + org logo as base64, sends confirmation
email with all ticket PDFs attached
- PDF template (pdf/billet.html.twig): A4 layout matching preview design,
real QR code linking to /ticket/verify/{reference}
- Email template: order recap table, ticket references list, link to
/ma-commande/{reference}
- Public order page /ma-commande/{reference}: no auth required, shows
order details, ticket list with individual PDF download links
- Ticket verification page /ticket/verify/{reference}: shows valid/scanned
status with ticket and event details
- Download route /ma-commande/{ref}/billet/{ticketRef}: generates PDF on-the-fly
- Migration for billet_order table with unique reference index
- BilletOrderTest: 8 tests, 24 assertions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
5.1 KiB
PHP
159 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Service;
|
|
|
|
use App\Entity\BilletBuyer;
|
|
use App\Entity\BilletDesign;
|
|
use App\Entity\BilletOrder;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Dompdf\Dompdf;
|
|
use Endroid\QrCode\Builder\Builder;
|
|
use Endroid\QrCode\Encoding\Encoding;
|
|
use Endroid\QrCode\Writer\PngWriter;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
use Twig\Environment;
|
|
|
|
/**
|
|
* @codeCoverageIgnore PDF generation + email sending
|
|
*/
|
|
class BilletOrderService
|
|
{
|
|
public function __construct(
|
|
private EntityManagerInterface $em,
|
|
private Environment $twig,
|
|
private MailerService $mailer,
|
|
private UrlGeneratorInterface $urlGenerator,
|
|
#[Autowire('%kernel.project_dir%')] private string $projectDir,
|
|
) {
|
|
}
|
|
|
|
public function generateOrderTickets(BilletBuyer $order): void
|
|
{
|
|
foreach ($order->getItems() as $item) {
|
|
for ($i = 0; $i < $item->getQuantity(); ++$i) {
|
|
$ticket = new BilletOrder();
|
|
$ticket->setBilletBuyer($order);
|
|
$ticket->setBillet($item->getBillet());
|
|
$ticket->setBilletName($item->getBilletName());
|
|
$ticket->setUnitPriceHT($item->getUnitPriceHT());
|
|
|
|
$this->em->persist($ticket);
|
|
}
|
|
}
|
|
|
|
$order->setStatus(BilletBuyer::STATUS_PAID);
|
|
$order->setPaidAt(new \DateTimeImmutable());
|
|
$this->em->flush();
|
|
}
|
|
|
|
public function generatePdf(BilletOrder $ticket): string
|
|
{
|
|
$order = $ticket->getBilletBuyer();
|
|
$event = $order->getEvent();
|
|
$organizer = $event->getAccount();
|
|
|
|
$design = $this->em->getRepository(BilletDesign::class)->findOneBy(['event' => $event]);
|
|
|
|
$qrData = $this->urlGenerator->generate('app_ticket_verify', [
|
|
'reference' => $ticket->getReference(),
|
|
], UrlGeneratorInterface::ABSOLUTE_URL);
|
|
|
|
$qrCode = (new Builder(
|
|
writer: new PngWriter(),
|
|
data: $qrData,
|
|
encoding: new Encoding('UTF-8'),
|
|
size: 200,
|
|
margin: 5,
|
|
))->build();
|
|
|
|
$qrBase64 = 'data:image/png;base64,'.base64_encode($qrCode->getString());
|
|
|
|
$logoBase64 = '';
|
|
if ($organizer->getLogoName()) {
|
|
$logoPath = $this->projectDir.'/public/uploads/logos/'.$organizer->getLogoName();
|
|
if (file_exists($logoPath)) {
|
|
$mime = mime_content_type($logoPath) ?: 'image/png';
|
|
$logoBase64 = 'data:'.$mime.';base64,'.base64_encode((string) file_get_contents($logoPath));
|
|
}
|
|
}
|
|
|
|
$posterBase64 = '';
|
|
if ($event->getEventMainPictureName()) {
|
|
$posterPath = $this->projectDir.'/public/uploads/events/'.$event->getEventMainPictureName();
|
|
if (file_exists($posterPath)) {
|
|
$mime = mime_content_type($posterPath) ?: 'image/png';
|
|
$posterBase64 = 'data:'.$mime.';base64,'.base64_encode((string) file_get_contents($posterPath));
|
|
}
|
|
}
|
|
|
|
$html = $this->twig->render('pdf/billet.html.twig', [
|
|
'ticket' => $ticket,
|
|
'order' => $order,
|
|
'event' => $event,
|
|
'organizer' => $organizer,
|
|
'design' => $design,
|
|
'qrBase64' => $qrBase64,
|
|
'logoBase64' => $logoBase64,
|
|
'posterBase64' => $posterBase64,
|
|
]);
|
|
|
|
$dompdf = new Dompdf();
|
|
$dompdf->loadHtml($html);
|
|
$dompdf->setPaper('A4');
|
|
$dompdf->render();
|
|
|
|
return $dompdf->output();
|
|
}
|
|
|
|
public function generateAndSendTickets(BilletBuyer $order): void
|
|
{
|
|
$tickets = $this->em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
|
|
if (0 === \count($tickets)) {
|
|
return;
|
|
}
|
|
|
|
$dir = $this->projectDir.'/var/billets/'.$order->getReference();
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0o755, true);
|
|
}
|
|
|
|
$attachments = [];
|
|
foreach ($tickets as $ticket) {
|
|
$billet = $ticket->getBillet();
|
|
if (!$billet || !$billet->isGeneratedBillet()) {
|
|
continue;
|
|
}
|
|
|
|
$pdf = $this->generatePdf($ticket);
|
|
$filename = $dir.'/'.$ticket->getReference().'.pdf';
|
|
file_put_contents($filename, $pdf);
|
|
|
|
$attachments[] = [
|
|
'path' => $filename,
|
|
'name' => $ticket->getReference().'.pdf',
|
|
];
|
|
}
|
|
|
|
$orderUrl = $this->urlGenerator->generate('app_order_public', [
|
|
'reference' => $order->getReference(),
|
|
], UrlGeneratorInterface::ABSOLUTE_URL);
|
|
|
|
$html = $this->twig->render('email/order_confirmation.html.twig', [
|
|
'order' => $order,
|
|
'tickets' => $tickets,
|
|
'orderUrl' => $orderUrl,
|
|
]);
|
|
|
|
$this->mailer->sendEmail(
|
|
$order->getEmail(),
|
|
'Vos billets - '.$order->getEvent()->getTitle(),
|
|
$html,
|
|
'E-Ticket <contact@e-cosplay.fr>',
|
|
null,
|
|
false,
|
|
$attachments,
|
|
);
|
|
}
|
|
}
|