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'])]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;">
|
||||
• Il est recommande d'arriver en avance.<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.
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user