Add stock management, order notifications, webhooks, expiration cron, and billet type validation
- Decrement billet quantity after purchase in BilletOrderService::generateOrderTickets - Block purchase when stock is exhausted (quantity <= 0) in OrderController::buildOrderItems - Add organizer email notification on new order (order_notification_orga template) - Add organizer email notification on cancel/refund (order_cancelled_orga template) - Add ExpirePendingOrdersCommand (app:orders:expire-pending) cron every 5min via Ansible - Cancels pending orders older than 30 minutes, restores stock, invalidates tickets - Includes BilletBuyerRepository::findExpiredPending query method - 3 unit tests covering: no expired orders, stock restoration, unlimited billets - Add payment_intent.payment_failed webhook: cancels order, logs audit, emails buyer - Add charge.refunded webhook: sets order to refunded, invalidates tickets, notifies orga and buyer - Validate billet type (billet/reservation_brocante/vote) against organizer offer - getAllowedBilletTypes: gratuit=billet only, basic/sur-mesure=all types - Server-side validation in hydrateBilletFromRequest, UI filtering in templates - Update TASK_CHECKUP.md: all Billetterie & Commandes items now complete Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -653,6 +653,7 @@ class AccountController extends AbstractController
|
||||
return $this->render('account/add_billet.html.twig', [
|
||||
'event' => $event,
|
||||
'category' => $category,
|
||||
'allowedTypes' => self::getAllowedBilletTypes($user->getOffer()),
|
||||
'breadcrumbs' => [
|
||||
self::BREADCRUMB_HOME,
|
||||
self::BREADCRUMB_ACCOUNT,
|
||||
@@ -691,6 +692,7 @@ class AccountController extends AbstractController
|
||||
return $this->render('account/edit_billet.html.twig', [
|
||||
'event' => $event,
|
||||
'billet' => $billet,
|
||||
'allowedTypes' => self::getAllowedBilletTypes($user->getOffer()),
|
||||
'breadcrumbs' => [
|
||||
self::BREADCRUMB_HOME,
|
||||
self::BREADCRUMB_ACCOUNT,
|
||||
@@ -849,7 +851,7 @@ class AccountController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/mon-compte/evenement/{id}/commande/{orderId}/annuler', name: 'app_account_event_cancel_order', methods: ['POST'])]
|
||||
public function cancelOrder(Event $event, int $orderId, EntityManagerInterface $em, AuditService $audit): Response
|
||||
public function cancelOrder(Event $event, int $orderId, EntityManagerInterface $em, AuditService $audit, BilletOrderService $billetOrderService): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
||||
|
||||
@@ -878,6 +880,8 @@ class AccountController extends AbstractController
|
||||
'event' => $event->getTitle(),
|
||||
]);
|
||||
|
||||
$billetOrderService->notifyOrganizerCancelled($order, 'annulee');
|
||||
|
||||
$this->addFlash('success', 'Commande '.$order->getOrderNumber().' annulee.');
|
||||
|
||||
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'stats']);
|
||||
@@ -887,7 +891,7 @@ class AccountController extends AbstractController
|
||||
* @codeCoverageIgnore Requires live Stripe API
|
||||
*/
|
||||
#[Route('/mon-compte/evenement/{id}/commande/{orderId}/rembourser', name: 'app_account_event_refund_order', methods: ['POST'])]
|
||||
public function refundOrder(Event $event, int $orderId, EntityManagerInterface $em, StripeService $stripeService, AuditService $audit): Response
|
||||
public function refundOrder(Event $event, int $orderId, EntityManagerInterface $em, StripeService $stripeService, AuditService $audit, BilletOrderService $billetOrderService): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
||||
|
||||
@@ -927,6 +931,8 @@ class AccountController extends AbstractController
|
||||
'totalHT' => $order->getTotalHTDecimal(),
|
||||
]);
|
||||
|
||||
$billetOrderService->notifyOrganizerCancelled($order, 'remboursee');
|
||||
|
||||
$this->addFlash('success', 'Commande '.$order->getOrderNumber().' remboursee.');
|
||||
|
||||
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'stats']);
|
||||
@@ -1238,6 +1244,17 @@ class AccountController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getAllowedBilletTypes(?string $offer): array
|
||||
{
|
||||
return match ($offer) {
|
||||
'basic', 'sur-mesure' => ['billet', 'reservation_brocante', 'vote'],
|
||||
default => ['billet'],
|
||||
};
|
||||
}
|
||||
|
||||
private function hydrateBilletFromRequest(Billet $billet, Request $request): void
|
||||
{
|
||||
$billet->setName(trim($request->request->getString('name')));
|
||||
@@ -1247,7 +1264,16 @@ class AccountController extends AbstractController
|
||||
$billet->setIsGeneratedBillet($request->request->getBoolean('is_generated_billet'));
|
||||
$billet->setHasDefinedExit($request->request->getBoolean('has_defined_exit'));
|
||||
$billet->setNotBuyable($request->request->getBoolean('not_buyable'));
|
||||
$billet->setType($request->request->getString('type', 'billet'));
|
||||
|
||||
$type = $request->request->getString('type', 'billet');
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$allowedTypes = self::getAllowedBilletTypes($user->getOffer());
|
||||
if (!\in_array($type, $allowedTypes, true)) {
|
||||
$type = 'billet';
|
||||
}
|
||||
$billet->setType($type);
|
||||
|
||||
$billet->setDescription(trim($request->request->getString('description')) ?: null);
|
||||
|
||||
$pictureFile = $request->files->get('picture');
|
||||
|
||||
Reference in New Issue
Block a user