Add force validate button in admin orders + fix Stripe Connect account in sync

- Add POST /admin/commandes/{id}/forcer-validation to force validate pending
  orders (generates tickets, sends emails, notifies organizer)
- Add "Forcer validation" button in orders template for pending orders
- Fix retrievePaymentIntent to query on organizer's Connect account
- Update stripe:sync to pass organizer stripeAccountId when checking payments
- Add 3 tests for force validation (pending, non-pending, not found)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-01 14:22:56 +02:00
parent 02519dcfa8
commit 626510e692
5 changed files with 122 additions and 6 deletions

View File

@@ -121,13 +121,15 @@ class StripeSyncCommand extends Command
foreach ($pendingOrders as $order) { foreach ($pendingOrders as $order) {
$paymentIntentId = $order->getStripeSessionId(); $paymentIntentId = $order->getStripeSessionId();
if (!$paymentIntentId) { $stripeAccountId = $order->getEvent()?->getAccount()?->getStripeAccountId();
$io->text(sprintf(' [<fg=yellow>SKIP</>] Order #%s — no payment intent ID', $order->getOrderNumber()));
if (!$paymentIntentId || !$stripeAccountId) {
$io->text(sprintf(' [<fg=yellow>SKIP</>] Order #%s — no payment intent ID or Stripe account', $order->getOrderNumber()));
continue; continue;
} }
try { try {
$paymentIntent = $this->stripeService->retrievePaymentIntent($paymentIntentId); $paymentIntent = $this->stripeService->retrievePaymentIntent($paymentIntentId, $stripeAccountId);
$stripeStatus = $paymentIntent->status; $stripeStatus = $paymentIntent->status;
match ($stripeStatus) { match ($stripeStatus) {

View File

@@ -582,6 +582,29 @@ class AdminController extends AbstractController
]); ]);
} }
#[Route('/commandes/{id}/forcer-validation', name: 'app_admin_order_force_validate', requirements: ['id' => '\d+'], methods: ['POST'])]
public function forceValidateOrder(int $id, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
{
$order = $em->getRepository(BilletBuyer::class)->find($id);
if (!$order) {
throw $this->createNotFoundException();
}
if (BilletBuyer::STATUS_PENDING !== $order->getStatus()) {
$this->addFlash('error', 'Seules les commandes en attente peuvent etre forcees.');
return $this->redirectToRoute('app_admin_orders');
}
$billetOrderService->generateOrderTickets($order);
$billetOrderService->generateAndSendTickets($order);
$billetOrderService->notifyOrganizer($order);
$this->addFlash('success', 'Commande '.$order->getOrderNumber().' validee avec succes. Billets generes et envoyes.');
return $this->redirectToRoute('app_admin_orders');
}
#[Route('/commandes/{id}/billets', name: 'app_admin_order_tickets', requirements: ['id' => '\d+'], methods: ['GET'])] #[Route('/commandes/{id}/billets', name: 'app_admin_order_tickets', requirements: ['id' => '\d+'], methods: ['GET'])]
public function orderTickets(int $id, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response public function orderTickets(int $id, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
{ {

View File

@@ -167,9 +167,11 @@ class StripeService
/** /**
* @codeCoverageIgnore Requires live Stripe API * @codeCoverageIgnore Requires live Stripe API
*/ */
public function retrievePaymentIntent(string $paymentIntentId): \Stripe\PaymentIntent public function retrievePaymentIntent(string $paymentIntentId, ?string $stripeAccountId = null): \Stripe\PaymentIntent
{ {
return $this->stripe->paymentIntents->retrieve($paymentIntentId); $options = $stripeAccountId ? ['stripe_account' => $stripeAccountId] : [];
return $this->stripe->paymentIntents->retrieve($paymentIntentId, null, $options);
} }
/** /**

View File

@@ -100,9 +100,13 @@
<span class="admin-badge-indigo text-xs font-black uppercase ml-1">Invitation</span> <span class="admin-badge-indigo text-xs font-black uppercase ml-1">Invitation</span>
{% endif %} {% endif %}
</td> </td>
<td class="text-right"> <td class="text-right whitespace-nowrap">
{% if order.status == 'paid' %} {% if order.status == 'paid' %}
<a href="{{ path('app_admin_order_tickets', {id: order.id}) }}" target="_blank" class="inline-block text-[10px] font-black uppercase tracking-widest text-indigo-600 hover:text-indigo-800 transition-all" title="Telecharger les billets">Billets</a> <a href="{{ path('app_admin_order_tickets', {id: order.id}) }}" target="_blank" class="inline-block text-[10px] font-black uppercase tracking-widest text-indigo-600 hover:text-indigo-800 transition-all" title="Telecharger les billets">Billets</a>
{% elseif order.status == 'pending' %}
<form method="post" action="{{ path('app_admin_order_force_validate', {id: order.id}) }}" onsubmit="return confirm('Forcer la validation de la commande {{ order.orderNumber }} ? Les billets seront generes et envoyes au client.')">
<button type="submit" class="text-[10px] font-black uppercase tracking-widest text-green-600 hover:text-green-800 transition-all">Forcer validation</button>
</form>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@@ -2,8 +2,10 @@
namespace App\Tests\Controller; namespace App\Tests\Controller;
use App\Entity\BilletBuyer;
use App\Entity\Event; use App\Entity\Event;
use App\Entity\User; use App\Entity\User;
use App\Service\BilletOrderService;
use App\Service\EventIndexService; use App\Service\EventIndexService;
use App\Service\MailerService; use App\Service\MailerService;
use App\Service\MeilisearchService; use App\Service\MeilisearchService;
@@ -743,6 +745,89 @@ class AdminControllerTest extends WebTestCase
self::assertResponseIsSuccessful(); self::assertResponseIsSuccessful();
} }
public function testForceValidateOrderPending(): void
{
$client = static::createClient();
$admin = $this->createUser(['ROLE_ROOT']);
$em = static::getContainer()->get(EntityManagerInterface::class);
$organizer = $this->createUser(['ROLE_ORGANIZER']);
$event = new Event();
$event->setTitle('Test Event Force');
$event->setAccount($organizer);
$event->setStartAt(new \DateTimeImmutable('+1 day'));
$event->setEndAt(new \DateTimeImmutable('+2 days'));
$event->setAddress('1 rue test');
$event->setZipcode('75001');
$event->setCity('Paris');
$em->persist($event);
$order = new BilletBuyer();
$order->setEvent($event);
$order->setEmail('buyer@test.fr');
$order->setFirstName('Jean');
$order->setLastName('Test');
$order->setStatus(BilletBuyer::STATUS_PENDING);
$em->persist($order);
$em->flush();
$billetOrderService = $this->createMock(BilletOrderService::class);
$billetOrderService->expects(self::once())->method('generateOrderTickets');
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
$billetOrderService->expects(self::once())->method('notifyOrganizer');
static::getContainer()->set(BilletOrderService::class, $billetOrderService);
$client->loginUser($admin);
$client->request('POST', '/admin/commandes/'.$order->getId().'/forcer-validation');
self::assertResponseRedirects('/admin/commandes');
}
public function testForceValidateOrderNonPendingFails(): void
{
$client = static::createClient();
$admin = $this->createUser(['ROLE_ROOT']);
$em = static::getContainer()->get(EntityManagerInterface::class);
$organizer = $this->createUser(['ROLE_ORGANIZER']);
$event = new Event();
$event->setTitle('Test Event Paid');
$event->setAccount($organizer);
$event->setStartAt(new \DateTimeImmutable('+1 day'));
$event->setEndAt(new \DateTimeImmutable('+2 days'));
$event->setAddress('1 rue test');
$event->setZipcode('75001');
$event->setCity('Paris');
$em->persist($event);
$order = new BilletBuyer();
$order->setEvent($event);
$order->setEmail('buyer2@test.fr');
$order->setFirstName('Pierre');
$order->setLastName('Test');
$order->setStatus(BilletBuyer::STATUS_PAID);
$em->persist($order);
$em->flush();
$client->loginUser($admin);
$client->request('POST', '/admin/commandes/'.$order->getId().'/forcer-validation');
self::assertResponseRedirects('/admin/commandes');
}
public function testForceValidateOrderNotFound(): void
{
$client = static::createClient();
$admin = $this->createUser(['ROLE_ROOT']);
$client->loginUser($admin);
$client->request('POST', '/admin/commandes/999999/forcer-validation');
self::assertResponseStatusCodeSame(404);
}
public function testLogsPage(): void public function testLogsPage(): void
{ {
$client = static::createClient(); $client = static::createClient();