Save Stripe payment details on order confirmation, add arrive early tip

- Retrieve PaymentIntent on success redirect, save payment_method, card_brand, card_last4
- Display payment info on /ma-commande page (card type + last 4 digits)
- Add "Il est recommande d'arriver en avance" to practical info on ticket PDF
- Migration for payment_method, card_brand, card_last4 columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-21 16:56:50 +01:00
parent c8faf76741
commit 7a29372b60
6 changed files with 139 additions and 1 deletions

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260321250000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add payment details to billet_buyer';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE billet_buyer ADD COLUMN IF NOT EXISTS payment_method VARCHAR(50) DEFAULT NULL');
$this->addSql('ALTER TABLE billet_buyer ADD COLUMN IF NOT EXISTS card_brand VARCHAR(50) DEFAULT NULL');
$this->addSql('ALTER TABLE billet_buyer ADD COLUMN IF NOT EXISTS card_last4 VARCHAR(4) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE billet_buyer DROP COLUMN IF EXISTS payment_method');
$this->addSql('ALTER TABLE billet_buyer DROP COLUMN IF EXISTS card_brand');
$this->addSql('ALTER TABLE billet_buyer DROP COLUMN IF EXISTS card_last4');
}
}

View File

@@ -185,7 +185,7 @@ class OrderController extends AbstractController
}
#[Route('/commande/{id}/confirmation', name: 'app_order_success', requirements: ['id' => '\d+'], methods: ['GET'])]
public function success(int $id, Request $request, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
public function success(int $id, Request $request, EntityManagerInterface $em, BilletOrderService $billetOrderService, StripeService $stripeService): Response
{
$order = $em->getRepository(BilletBuyer::class)->find($id);
if (!$order) {
@@ -193,8 +193,10 @@ class OrderController extends AbstractController
}
$redirectStatus = $request->query->getString('redirect_status');
$paymentIntentId = $request->query->getString('payment_intent');
if ('succeeded' === $redirectStatus && BilletBuyer::STATUS_PENDING === $order->getStatus()) {
$this->savePaymentDetails($order, $paymentIntentId, $stripeService, $em);
$billetOrderService->generateOrderTickets($order);
$billetOrderService->generateAndSendTickets($order);
}
@@ -294,4 +296,41 @@ class OrderController extends AbstractController
return $totalHT;
}
/**
* @codeCoverageIgnore Requires live Stripe API
*/
private function savePaymentDetails(BilletBuyer $order, string $paymentIntentId, StripeService $stripeService, EntityManagerInterface $em): void
{
if (!$paymentIntentId) {
return;
}
$organizer = $order->getEvent()->getAccount();
if (!$organizer->getStripeAccountId()) {
return;
}
try {
$pi = $stripeService->getClient()->paymentIntents->retrieve(
$paymentIntentId,
['expand' => ['payment_method']],
['stripe_account' => $organizer->getStripeAccountId()]
);
$pm = $pi->payment_method;
if ($pm) {
$order->setPaymentMethod($pm->type ?? null);
if (isset($pm->card)) {
$order->setCardBrand($pm->card->brand ?? null);
$order->setCardLast4($pm->card->last4 ?? null);
}
}
$order->setStripeSessionId($paymentIntentId);
$em->flush();
} catch (\Exception) {
// Stripe failure is non-blocking
}
}
}

View File

@@ -54,6 +54,15 @@ class BilletBuyer
#[ORM\Column(length: 255, nullable: true)]
private ?string $stripeSessionId = null;
#[ORM\Column(length: 50, nullable: true)]
private ?string $paymentMethod = null;
#[ORM\Column(length: 50, nullable: true)]
private ?string $cardBrand = null;
#[ORM\Column(length: 4, nullable: true)]
private ?string $cardLast4 = null;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
@@ -208,6 +217,42 @@ class BilletBuyer
return $this;
}
public function getPaymentMethod(): ?string
{
return $this->paymentMethod;
}
public function setPaymentMethod(?string $paymentMethod): static
{
$this->paymentMethod = $paymentMethod;
return $this;
}
public function getCardBrand(): ?string
{
return $this->cardBrand;
}
public function setCardBrand(?string $cardBrand): static
{
$this->cardBrand = $cardBrand;
return $this;
}
public function getCardLast4(): ?string
{
return $this->cardLast4;
}
public function setCardLast4(?string $cardLast4): static
{
$this->cardLast4 = $cardLast4;
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;

View File

@@ -39,6 +39,14 @@
<span class="badge-red text-xs font-black uppercase">Annulee</span>
{% endif %}
</p>
{% if order.paymentMethod %}
<p class="text-xs font-bold text-gray-400 mt-2">
Paiement : {{ order.paymentMethod }}
{% if order.cardBrand and order.cardLast4 %}
{{ order.cardBrand|upper }} **** {{ order.cardLast4 }}
{% endif %}
</p>
{% endif %}
</div>
</div>

View File

@@ -382,6 +382,7 @@
<td style="width: 50%; vertical-align: top; padding-right: 15px;">
<div style="font-size: 10px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; color: #999; margin-bottom: 4px;">Informations pratiques</div>
<div style="font-size: 9px; color: #555; line-height: 1.5;">
&bull; Il est recommande d'arriver en avance.<br>
&bull; En arrivant, preparez votre billet pour accelerer les controles a l'entree.<br>
&bull; A l'approche des controles de securite, merci de preparer vos affaires pour faciliter la verification.
</div>

View File

@@ -24,6 +24,9 @@ class BilletBuyerTest extends TestCase
self::assertSame(0.0, $buyer->getTotalHTDecimal());
self::assertSame(BilletBuyer::STATUS_PENDING, $buyer->getStatus());
self::assertNull($buyer->getStripeSessionId());
self::assertNull($buyer->getPaymentMethod());
self::assertNull($buyer->getCardBrand());
self::assertNull($buyer->getCardLast4());
self::assertNull($buyer->getPaidAt());
self::assertMatchesRegularExpression('/^ETICKET-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $buyer->getReference());
self::assertSame(32, \strlen($buyer->getAccessToken()));
@@ -94,6 +97,18 @@ class BilletBuyerTest extends TestCase
self::assertSame($buyer, $result);
}
public function testSetAndGetPaymentDetails(): void
{
$buyer = new BilletBuyer();
$buyer->setPaymentMethod('card');
$buyer->setCardBrand('visa');
$buyer->setCardLast4('4242');
self::assertSame('card', $buyer->getPaymentMethod());
self::assertSame('visa', $buyer->getCardBrand());
self::assertSame('4242', $buyer->getCardLast4());
}
public function testSetAndGetPaidAt(): void
{
$buyer = new BilletBuyer();