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:
30
migrations/Version20260321250000.php
Normal file
30
migrations/Version20260321250000.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -185,7 +185,7 @@ class OrderController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/commande/{id}/confirmation', name: 'app_order_success', requirements: ['id' => '\d+'], methods: ['GET'])]
|
#[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);
|
$order = $em->getRepository(BilletBuyer::class)->find($id);
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
@@ -193,8 +193,10 @@ class OrderController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$redirectStatus = $request->query->getString('redirect_status');
|
$redirectStatus = $request->query->getString('redirect_status');
|
||||||
|
$paymentIntentId = $request->query->getString('payment_intent');
|
||||||
|
|
||||||
if ('succeeded' === $redirectStatus && BilletBuyer::STATUS_PENDING === $order->getStatus()) {
|
if ('succeeded' === $redirectStatus && BilletBuyer::STATUS_PENDING === $order->getStatus()) {
|
||||||
|
$this->savePaymentDetails($order, $paymentIntentId, $stripeService, $em);
|
||||||
$billetOrderService->generateOrderTickets($order);
|
$billetOrderService->generateOrderTickets($order);
|
||||||
$billetOrderService->generateAndSendTickets($order);
|
$billetOrderService->generateAndSendTickets($order);
|
||||||
}
|
}
|
||||||
@@ -294,4 +296,41 @@ class OrderController extends AbstractController
|
|||||||
|
|
||||||
return $totalHT;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ class BilletBuyer
|
|||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $stripeSessionId = null;
|
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]
|
#[ORM\Column]
|
||||||
private \DateTimeImmutable $createdAt;
|
private \DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
@@ -208,6 +217,42 @@ class BilletBuyer
|
|||||||
return $this;
|
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
|
public function getCreatedAt(): \DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
|
|||||||
@@ -39,6 +39,14 @@
|
|||||||
<span class="badge-red text-xs font-black uppercase">Annulee</span>
|
<span class="badge-red text-xs font-black uppercase">Annulee</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -382,6 +382,7 @@
|
|||||||
<td style="width: 50%; vertical-align: top; padding-right: 15px;">
|
<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: 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;">
|
<div style="font-size: 9px; color: #555; line-height: 1.5;">
|
||||||
|
• Il est recommande d'arriver en avance.<br>
|
||||||
• En arrivant, preparez votre billet pour accelerer les controles a l'entree.<br>
|
• En arrivant, preparez votre billet pour accelerer les controles a l'entree.<br>
|
||||||
• A l'approche des controles de securite, merci de preparer vos affaires pour faciliter la verification.
|
• A l'approche des controles de securite, merci de preparer vos affaires pour faciliter la verification.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ class BilletBuyerTest extends TestCase
|
|||||||
self::assertSame(0.0, $buyer->getTotalHTDecimal());
|
self::assertSame(0.0, $buyer->getTotalHTDecimal());
|
||||||
self::assertSame(BilletBuyer::STATUS_PENDING, $buyer->getStatus());
|
self::assertSame(BilletBuyer::STATUS_PENDING, $buyer->getStatus());
|
||||||
self::assertNull($buyer->getStripeSessionId());
|
self::assertNull($buyer->getStripeSessionId());
|
||||||
|
self::assertNull($buyer->getPaymentMethod());
|
||||||
|
self::assertNull($buyer->getCardBrand());
|
||||||
|
self::assertNull($buyer->getCardLast4());
|
||||||
self::assertNull($buyer->getPaidAt());
|
self::assertNull($buyer->getPaidAt());
|
||||||
self::assertMatchesRegularExpression('/^ETICKET-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $buyer->getReference());
|
self::assertMatchesRegularExpression('/^ETICKET-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $buyer->getReference());
|
||||||
self::assertSame(32, \strlen($buyer->getAccessToken()));
|
self::assertSame(32, \strlen($buyer->getAccessToken()));
|
||||||
@@ -94,6 +97,18 @@ class BilletBuyerTest extends TestCase
|
|||||||
self::assertSame($buyer, $result);
|
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
|
public function testSetAndGetPaidAt(): void
|
||||||
{
|
{
|
||||||
$buyer = new BilletBuyer();
|
$buyer = new BilletBuyer();
|
||||||
|
|||||||
Reference in New Issue
Block a user