Files
e-ticket/src/Service/BilletOrderService.php
Serreau Jovann 52cb19df8b Add BilletOrder entity, PDF generation, email with QR codes, public order page
- 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>
2026-03-21 14:04:45 +01:00

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,
);
}
}