add ticket purchase page
Co-authored-by: Jan-Marlon Leibl <jleibl@proton.me> Reviewed-on: http://git.simonis.lol/projects/abiball/pulls/13 Reviewed-by: jank1619 <jan@kjan.email>
This commit is contained in:
63
src/Controller/TicketController.php
Normal file
63
src/Controller/TicketController.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DataObjects\TicketFormData;
|
||||
use App\Service\TicketService;
|
||||
use Stripe\Stripe;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final class TicketController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private TicketService $service,
|
||||
private SerializerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/ticket', name: 'app_ticket')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('ticket/index.html.twig');
|
||||
}
|
||||
|
||||
#[Route(path: '/ticket/submit', name: 'app_submit', methods: Request::METHOD_POST)]
|
||||
public function submit(Request $request): Response
|
||||
{
|
||||
$ticketData = $this->serializer->deserialize($request->getContent(), TicketFormData::class, 'json');
|
||||
|
||||
return $this->json(['id' => $this->service->handleTicketData($ticketData)->id]);
|
||||
}
|
||||
|
||||
#[Route(path: '/success', name: 'app_success', methods: Request::METHOD_GET)]
|
||||
public function success(Request $request): Response
|
||||
{
|
||||
$sessionId = $request->query->get('session_id');
|
||||
|
||||
if (!$sessionId) {
|
||||
noty()->error('Etwas ist schiefgelaufen');
|
||||
|
||||
return $this->redirectToRoute('app_ticket');
|
||||
}
|
||||
|
||||
if (!$this->service->completePayment($sessionId)) {
|
||||
noty()->error('Etwas ist schiefgelaufen');
|
||||
|
||||
return $this->redirectToRoute('app_ticket');
|
||||
}
|
||||
|
||||
return $this->render('ticket/success.html.twig');
|
||||
}
|
||||
|
||||
#[Route(path: '/cancelled', name: 'app_cancelled', methods: Request::METHOD_GET)]
|
||||
public function cancel(): Response
|
||||
{
|
||||
noty()->error('Bezahlung abgebrochen');
|
||||
|
||||
return $this->redirectToRoute('app_ticket');
|
||||
}
|
||||
}
|
14
src/DataObjects/PersonalData.php
Normal file
14
src/DataObjects/PersonalData.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataObjects;
|
||||
|
||||
class PersonalData
|
||||
{
|
||||
public function __construct(
|
||||
public string $firstname,
|
||||
public string $lastname,
|
||||
public string $email,
|
||||
public string $phone,
|
||||
) {
|
||||
}
|
||||
}
|
22
src/DataObjects/TicketData.php
Normal file
22
src/DataObjects/TicketData.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataObjects;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class TicketData
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\Range(min: 1, max: 3)]
|
||||
#[SerializedName('ticket')]
|
||||
public int $ticketType = 0,
|
||||
|
||||
#[Assert\Range(min: 1, max: 3)]
|
||||
#[SerializedName('food')]
|
||||
public int $foodType = 0,
|
||||
|
||||
public string $note = ''
|
||||
) {
|
||||
}
|
||||
}
|
20
src/DataObjects/TicketFormData.php
Normal file
20
src/DataObjects/TicketFormData.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataObjects;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
class TicketFormData
|
||||
{
|
||||
/**
|
||||
* @param PersonalData $personal
|
||||
* @param TicketData[] $tickets
|
||||
*/
|
||||
public function __construct(
|
||||
#[SerializedName('personal')]
|
||||
public PersonalData $personal,
|
||||
#[SerializedName('tickets')]
|
||||
public array $tickets,
|
||||
) {
|
||||
}
|
||||
}
|
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
152
src/Entity/Customer.php
Normal file
152
src/Entity/Customer.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\CustomerRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
|
||||
class Customer
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $phone = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $firstname = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $lastname = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Ticket>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Ticket::class, mappedBy: 'customer', cascade: ['persist'], fetch: 'EAGER', orphanRemoval: true)]
|
||||
private Collection $tickets;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'customer', cascade: ['persist', 'remove'])]
|
||||
private ?Payment $payment = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->tickets = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhone(): ?string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(?string $phone): static
|
||||
{
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFirstname(): ?string
|
||||
{
|
||||
return $this->firstname;
|
||||
}
|
||||
|
||||
public function setFirstname(string $firstname): static
|
||||
{
|
||||
$this->firstname = $firstname;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastname(): ?string
|
||||
{
|
||||
return $this->lastname;
|
||||
}
|
||||
|
||||
public function setLastname(string $lastname): static
|
||||
{
|
||||
$this->lastname = $lastname;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Ticket>
|
||||
*/
|
||||
public function getTickets(): Collection
|
||||
{
|
||||
return $this->tickets;
|
||||
}
|
||||
|
||||
public function addTicket(Ticket $ticket): static
|
||||
{
|
||||
if (!$this->tickets->contains($ticket)) {
|
||||
$this->tickets->add($ticket);
|
||||
$ticket->setCustomer($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTicket(Ticket $ticket): static
|
||||
{
|
||||
if ($this->tickets->removeElement($ticket)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($ticket->getCustomer() === $this) {
|
||||
$ticket->setCustomer(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTickets(array $tickets): static
|
||||
{
|
||||
foreach ($tickets as $ticket) {
|
||||
$this->addTicket($ticket);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPayment(): ?Payment
|
||||
{
|
||||
return $this->payment;
|
||||
}
|
||||
|
||||
public function setPayment(Payment $payment): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($payment->getCustomer() !== $this) {
|
||||
$payment->setCustomer($this);
|
||||
}
|
||||
|
||||
$this->payment = $payment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
66
src/Entity/Payment.php
Normal file
66
src/Entity/Payment.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\PaymentRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PaymentRepository::class)]
|
||||
class Payment
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $sessionId = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $completed = null;
|
||||
|
||||
#[ORM\OneToOne(inversedBy: 'payment', cascade: ['persist', 'remove'], fetch: 'EAGER')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getSessionId(): ?string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
public function setSessionId(string $sessionId): static
|
||||
{
|
||||
$this->sessionId = $sessionId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isCompleted(): ?bool
|
||||
{
|
||||
return $this->completed;
|
||||
}
|
||||
|
||||
public function setCompleted(bool $completed): static
|
||||
{
|
||||
$this->completed = $completed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): static
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
81
src/Entity/Ticket.php
Normal file
81
src/Entity/Ticket.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\TicketRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TicketRepository::class)]
|
||||
class Ticket
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $type = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $foodType = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $note = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'tickets')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): ?int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(int $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFoodType(): ?int
|
||||
{
|
||||
return $this->foodType;
|
||||
}
|
||||
|
||||
public function setFoodType(int $foodType): static
|
||||
{
|
||||
$this->foodType = $foodType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNote(): ?string
|
||||
{
|
||||
return $this->note;
|
||||
}
|
||||
|
||||
public function setNote(?string $note): static
|
||||
{
|
||||
$this->note = $note;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(?Customer $customer): static
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
25
src/Enum/TicketData.php
Normal file
25
src/Enum/TicketData.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
class TicketData
|
||||
{
|
||||
public const TICKET_DATA = [
|
||||
1 => [
|
||||
'name' => 'Ticket',
|
||||
'price' => 50,
|
||||
],
|
||||
2 => [
|
||||
'name' => 'After-Show Ticket',
|
||||
'price' => 20,
|
||||
],
|
||||
3 => [
|
||||
'name' => 'Kind (6-12 Jahre)',
|
||||
'price' => 0,
|
||||
],
|
||||
4 => [
|
||||
'name' => 'Kind (0-6 Jahre)',
|
||||
'price' => 0,
|
||||
],
|
||||
];
|
||||
}
|
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
18
src/Repository/CustomerRepository.php
Normal file
18
src/Repository/CustomerRepository.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Customer>
|
||||
*/
|
||||
class CustomerRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Customer::class);
|
||||
}
|
||||
}
|
18
src/Repository/PaymentRepository.php
Normal file
18
src/Repository/PaymentRepository.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Payment;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Payment>
|
||||
*/
|
||||
class PaymentRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Payment::class);
|
||||
}
|
||||
}
|
18
src/Repository/TicketRepository.php
Normal file
18
src/Repository/TicketRepository.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Ticket;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Ticket>
|
||||
*/
|
||||
class TicketRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Ticket::class);
|
||||
}
|
||||
}
|
129
src/Service/TicketService.php
Normal file
129
src/Service/TicketService.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\DataObjects\TicketData;
|
||||
use App\DataObjects\TicketFormData;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Payment;
|
||||
use App\Entity\Ticket;
|
||||
use App\Enum\TicketData as TicketEnum;
|
||||
use App\Repository\PaymentRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Stripe\Checkout\Session;
|
||||
use Stripe\Stripe;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class TicketService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $generator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly PaymentRepository $paymentRepository,
|
||||
#[Autowire(env: 'STRIPE_SECRET_KEY')]
|
||||
string $stripeKey
|
||||
) {
|
||||
Stripe::setApiKey($stripeKey);
|
||||
}
|
||||
|
||||
public function handleTicketData(TicketFormData $data): Session
|
||||
{
|
||||
$session = $this->createSession($this->getLineItems($data->tickets), $data->personal->email);
|
||||
|
||||
$this->saveTicketData($data, $session->id);
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function saveTicketData(TicketFormData $data, string $sessionId): void
|
||||
{
|
||||
$payment = (new Payment())
|
||||
->setSessionId($sessionId)
|
||||
->setCompleted(false)
|
||||
->setCustomer($this->createEntityFromData($data));
|
||||
|
||||
$this->em->persist($payment);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
|
||||
public function completePayment(string $sessionId): bool
|
||||
{
|
||||
if (!$payment = $this->paymentRepository->findOneBy(['sessionId' => $sessionId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payment->setCompleted(true);
|
||||
$this->em->flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getLineItems(array $tickets): array
|
||||
{
|
||||
$lineItems = [];
|
||||
|
||||
foreach ($tickets as $ticket) {
|
||||
$ticketData = TicketEnum::TICKET_DATA[$ticket->ticketType];
|
||||
$lineItems[] = [
|
||||
'price_data' => [
|
||||
'currency' => 'eur',
|
||||
'product_data' => [
|
||||
'name' => $ticketData['name'],
|
||||
],
|
||||
'unit_amount' => $ticketData['price'] * 100,
|
||||
],
|
||||
'quantity' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
return $lineItems;
|
||||
}
|
||||
|
||||
private function createSession(array $lineItems, string $email): Session
|
||||
{
|
||||
return Session::create([
|
||||
'line_items' => $lineItems,
|
||||
'mode' => 'payment',
|
||||
'customer_email' => $email,
|
||||
'success_url' => $this->generator->generate('app_success', [], 0) . '?session_id={CHECKOUT_SESSION_ID}',
|
||||
'cancel_url' => $this->generator->generate('app_cancelled', [], 0),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createEntityFromData(TicketFormData $ticketData): Customer
|
||||
{
|
||||
$personalData = $ticketData->personal;
|
||||
|
||||
$entity = (new Customer())
|
||||
->setFirstname($personalData->firstname)
|
||||
->setLastname($personalData->lastname)
|
||||
->setEmail($personalData->email)
|
||||
->setPhone($personalData->phone);
|
||||
|
||||
$entity->addTickets($this->createTicketEntities($ticketData->tickets));
|
||||
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TicketData[] $tickets
|
||||
*/
|
||||
private function createTicketEntities(array $tickets): array
|
||||
{
|
||||
$entities = [];
|
||||
|
||||
foreach ($tickets as $ticket) {
|
||||
$entities[] = (new Ticket())
|
||||
->setType($ticket->ticketType)
|
||||
->setFoodType($ticket->foodType)
|
||||
->setNote($ticket->note);
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
}
|
19
src/Twig/Environment.php
Normal file
19
src/Twig/Environment.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class Environment extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [new TwigFunction('env', $this->getVar(...))];
|
||||
}
|
||||
|
||||
public function getVar(string $name): string
|
||||
{
|
||||
return $_ENV[$name];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user