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:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user