Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook: - Single webhook endpoint /stripe/webhook for Connect + payment events - v2 Connect events configured manually in Stripe Dashboard (not via API) - account.updated syncs charges_enabled/payouts_enabled via API retrieve - Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault) Account page (/mon-compte): - Buyer tabs: Billets, Achats, Factures, Parametres - Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs - Stripe Connect status banner: setup required, pending verification, active, refused - Stripe Connect onboarding: create account, complete verification (GET links) - Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard) - Cancel/close Stripe account: deletes via API + resets local fields - Stripe required message on events/subaccounts/payouts tabs when not active - Settings: organizer fields locked (name, address), email/phone editable - Return/refresh routes for Stripe Connect onboarding flow - Error handling with flash messages on all Stripe operations - Auto-sync Stripe status on /mon-compte visit StripeService cleanup: - Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir - Add deleteAccount method - Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink Security: - Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist - Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,12 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Service\StripeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
@@ -11,8 +16,142 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
class AccountController extends AbstractController
|
||||
{
|
||||
#[Route('/mon-compte', name: 'app_account')]
|
||||
public function index(): Response
|
||||
public function index(Request $request, StripeService $stripeService, EntityManagerInterface $em): Response
|
||||
{
|
||||
return $this->render('account/index.html.twig');
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$isOrganizer = $this->isGranted('ROLE_ORGANIZER');
|
||||
$defaultTab = $isOrganizer ? 'events' : 'tickets';
|
||||
$tab = $request->query->getString('tab', $defaultTab);
|
||||
|
||||
if ($isOrganizer && $user->getStripeAccountId() && (!$user->isStripeChargesEnabled() || !$user->isStripePayoutsEnabled())) {
|
||||
try {
|
||||
$account = $stripeService->getClient()->accounts->retrieve($user->getStripeAccountId());
|
||||
$user->setStripeChargesEnabled((bool) $account->charges_enabled);
|
||||
$user->setStripePayoutsEnabled((bool) $account->payouts_enabled);
|
||||
$em->flush();
|
||||
} catch (\Throwable) {
|
||||
// Stripe API unavailable, keep current status
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('account/index.html.twig', [
|
||||
'tab' => $tab,
|
||||
'isOrganizer' => $isOrganizer,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/mon-compte/parametres', name: 'app_account_settings', methods: ['POST'])]
|
||||
public function settings(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$isOrganizer = $this->isGranted('ROLE_ORGANIZER');
|
||||
|
||||
if (!$isOrganizer) {
|
||||
$user->setFirstName(trim($request->request->getString('first_name')));
|
||||
$user->setLastName(trim($request->request->getString('last_name')));
|
||||
}
|
||||
|
||||
$user->setEmail(trim($request->request->getString('email')));
|
||||
$user->setPhone(trim($request->request->getString('phone')));
|
||||
|
||||
if (!$isOrganizer) {
|
||||
$user->setAddress(trim($request->request->getString('address')));
|
||||
$user->setPostalCode(trim($request->request->getString('postal_code')));
|
||||
$user->setCity(trim($request->request->getString('city')));
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Parametres mis a jour.');
|
||||
|
||||
return $this->redirectToRoute('app_account', ['tab' => 'settings']);
|
||||
}
|
||||
|
||||
#[Route('/mon-compte/stripe-connect', name: 'app_account_stripe_connect')]
|
||||
public function stripeConnect(StripeService $stripeService, EntityManagerInterface $em): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$this->isGranted('ROLE_ORGANIZER')) {
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$user->getStripeAccountId()) {
|
||||
$accountId = $stripeService->createAccountConnect($user);
|
||||
$user->setStripeAccountId($accountId);
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
$link = $stripeService->createAccountLink($user->getStripeAccountId());
|
||||
|
||||
return $this->redirect($link);
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('error', 'Erreur lors de la connexion a Stripe : '.$e->getMessage());
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/mon-compte/stripe-cancel', name: 'app_account_stripe_cancel', methods: ['POST'])]
|
||||
public function stripeCancel(StripeService $stripeService, EntityManagerInterface $em): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if ($this->isGranted('ROLE_ORGANIZER') && $user->getStripeAccountId()) {
|
||||
try {
|
||||
$stripeService->deleteAccount($user->getStripeAccountId());
|
||||
} catch (\Throwable) {
|
||||
// Account may already be deleted on Stripe side
|
||||
}
|
||||
|
||||
$user->setStripeAccountId(null);
|
||||
$user->setStripeChargesEnabled(false);
|
||||
$user->setStripePayoutsEnabled(false);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Compte Stripe cloture.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
#[Route('/stripe/connect/return', name: 'app_stripe_connect_return')]
|
||||
public function stripeConnectReturn(): Response
|
||||
{
|
||||
$this->addFlash('success', 'Configuration Stripe terminee.');
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
#[Route('/stripe/connect/refresh', name: 'app_stripe_connect_refresh')]
|
||||
public function stripeConnectRefresh(): Response
|
||||
{
|
||||
return $this->redirectToRoute('app_account_stripe_connect');
|
||||
}
|
||||
|
||||
#[Route('/mon-compte/stripe-dashboard', name: 'app_account_stripe_dashboard')]
|
||||
public function stripeDashboard(StripeService $stripeService): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$this->isGranted('ROLE_ORGANIZER') || !$user->getStripeAccountId()) {
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
try {
|
||||
$link = $stripeService->createLoginLink($user->getStripeAccountId());
|
||||
|
||||
return $this->redirect($link);
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('error', 'Erreur Stripe : '.$e->getMessage());
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user