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:
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\StripeSyncCommand;
|
||||
use App\Service\StripeService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StripeSyncCommandTest extends TestCase
|
||||
{
|
||||
public function testCommandIsConfigured(): void
|
||||
{
|
||||
$stripeService = $this->createMock(StripeService::class);
|
||||
$command = new StripeSyncCommand($stripeService);
|
||||
|
||||
self::assertSame('stripe:sync', $command->getName());
|
||||
self::assertSame('Create or update the Stripe webhook endpoint', $command->getDescription());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Service\StripeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
@@ -27,7 +28,225 @@ class AccountControllerTest extends WebTestCase
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
public function testAccountTicketsTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=tickets');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testAccountPurchasesTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=purchases');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testAccountInvoicesTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=invoices');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testAccountSettingsTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=settings');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testAccountSettingsSubmit(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/parametres', [
|
||||
'first_name' => 'Updated',
|
||||
'last_name' => 'Name',
|
||||
'email' => $user->getEmail(),
|
||||
'phone' => '0699887766',
|
||||
'address' => '1 rue Test',
|
||||
'postal_code' => '75001',
|
||||
'city' => 'Paris',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte?tab=settings');
|
||||
}
|
||||
|
||||
public function testOrganizerEventsTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=events');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testOrganizerSubaccountsTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=subaccounts');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testOrganizerPayoutsTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=payouts');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testOrganizerSettingsDisablesNameFields(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/parametres', [
|
||||
'email' => $user->getEmail(),
|
||||
'phone' => '0699887766',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte?tab=settings');
|
||||
}
|
||||
|
||||
public function testOrganizerDefaultTabIsEvents(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testStripeConnectRedirectsForNonOrganizer(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/stripe-connect');
|
||||
|
||||
self::assertResponseRedirects('/mon-compte');
|
||||
}
|
||||
|
||||
public function testOrganizerWithoutStripeShowsSetupMessage(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$crawler = $client->request('GET', '/mon-compte');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertSelectorTextContains('body', 'Configuration Stripe requise');
|
||||
}
|
||||
|
||||
public function testOrganizerWithStripePendingShowsMessage(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
$user->setStripeAccountId('acct_pending');
|
||||
$em->flush();
|
||||
|
||||
$client->loginUser($user);
|
||||
$crawler = $client->request('GET', '/mon-compte');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertSelectorTextContains('body', 'en cours de verification');
|
||||
}
|
||||
|
||||
public function testOrganizerWithStripeActiveShowsSuccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
$user->setStripeAccountId('acct_active');
|
||||
$user->setStripeChargesEnabled(true);
|
||||
$user->setStripePayoutsEnabled(true);
|
||||
$em->flush();
|
||||
|
||||
$client->loginUser($user);
|
||||
$crawler = $client->request('GET', '/mon-compte');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertSelectorTextContains('body', 'Stripe Connect actif');
|
||||
}
|
||||
|
||||
public function testStripeConnectReturn(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/stripe/connect/return');
|
||||
|
||||
self::assertResponseRedirects('/mon-compte');
|
||||
}
|
||||
|
||||
public function testStripeConnectRefresh(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/stripe/connect/refresh');
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/stripe-connect');
|
||||
}
|
||||
|
||||
public function testStripeCancelResetsAccount(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER']);
|
||||
$user->setStripeAccountId('acct_cancel');
|
||||
$em->flush();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/stripe-cancel');
|
||||
|
||||
self::assertResponseRedirects('/mon-compte');
|
||||
|
||||
$em->refresh($user);
|
||||
self::assertNull($user->getStripeAccountId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
private function createUser(array $roles = []): User
|
||||
{
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
@@ -36,6 +255,7 @@ class AccountControllerTest extends WebTestCase
|
||||
$user->setFirstName('Test');
|
||||
$user->setLastName('User');
|
||||
$user->setPassword('$2y$13$hashed');
|
||||
$user->setRoles($roles);
|
||||
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Tests\Controller;
|
||||
use App\Entity\User;
|
||||
use App\Service\StripeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Stripe\Account;
|
||||
use Stripe\Event;
|
||||
use Stripe\StripeClient;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class StripeWebhookControllerTest extends WebTestCase
|
||||
@@ -49,25 +51,38 @@ class StripeWebhookControllerTest extends WebTestCase
|
||||
'type' => 'account.updated',
|
||||
'data' => [
|
||||
'object' => [
|
||||
'id' => 'acct_test123',
|
||||
'id' => 'acct_test_webhook',
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$account = Account::constructFrom([
|
||||
'id' => 'acct_test_webhook',
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
]);
|
||||
|
||||
$accountsService = $this->createMock(\Stripe\Service\AccountService::class);
|
||||
$accountsService->method('retrieve')->willReturn($account);
|
||||
|
||||
$stripeClient = $this->createMock(StripeClient::class);
|
||||
$stripeClient->accounts = $accountsService;
|
||||
|
||||
$stripeService = $this->createMock(StripeService::class);
|
||||
$stripeService->method('verifyWebhookSignature')->willReturn($event);
|
||||
$stripeService->method('getClient')->willReturn($stripeClient);
|
||||
static::getContainer()->set(StripeService::class, $stripeService);
|
||||
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail('test-stripe-'.uniqid().'@example.com');
|
||||
$user->setEmail('test-stripe-wh-'.uniqid().'@example.com');
|
||||
$user->setFirstName('Stripe');
|
||||
$user->setLastName('Test');
|
||||
$user->setPassword('$2y$13$hashed');
|
||||
$user->setStripeAccountId('acct_test123');
|
||||
$user->setStripeAccountId('acct_test_webhook');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
@@ -78,7 +93,7 @@ class StripeWebhookControllerTest extends WebTestCase
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$updatedUser = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_test123']);
|
||||
$updatedUser = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_test_webhook']);
|
||||
self::assertNotNull($updatedUser);
|
||||
self::assertTrue($updatedUser->isStripeChargesEnabled());
|
||||
self::assertTrue($updatedUser->isStripePayoutsEnabled());
|
||||
@@ -93,8 +108,6 @@ class StripeWebhookControllerTest extends WebTestCase
|
||||
'data' => [
|
||||
'object' => [
|
||||
'id' => 'acct_unknown',
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -7,57 +7,13 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StripeServiceTest extends TestCase
|
||||
{
|
||||
public function testGetWebhookUrl(): void
|
||||
private function createService(): StripeService
|
||||
{
|
||||
$service = new StripeService('sk_test', 'whsec_test', 'https://example.com', '/tmp');
|
||||
|
||||
self::assertSame('https://example.com/stripe/webhook', $service->getWebhookUrl());
|
||||
}
|
||||
|
||||
public function testGetWebhookUrlTrimsTrailingSlash(): void
|
||||
{
|
||||
$service = new StripeService('sk_test', 'whsec_test', 'https://example.com/', '/tmp');
|
||||
|
||||
self::assertSame('https://example.com/stripe/webhook', $service->getWebhookUrl());
|
||||
return new StripeService('sk_test', 'whsec_test', 'https://example.com');
|
||||
}
|
||||
|
||||
public function testVerifyWebhookSignatureReturnsNullOnInvalid(): void
|
||||
{
|
||||
$service = new StripeService('sk_test', 'whsec_test', 'https://example.com', '/tmp');
|
||||
|
||||
self::assertNull($service->verifyWebhookSignature('{}', 'invalid'));
|
||||
}
|
||||
|
||||
public function testSaveWebhookSecretCreatesEntry(): void
|
||||
{
|
||||
$tmpDir = sys_get_temp_dir().'/stripe_test_'.uniqid();
|
||||
mkdir($tmpDir);
|
||||
file_put_contents($tmpDir.'/.env.local', "APP_ENV=test\n");
|
||||
|
||||
$service = new StripeService('sk_test', 'whsec_test', 'https://example.com', $tmpDir);
|
||||
$service->saveWebhookSecret('whsec_new123');
|
||||
|
||||
$content = file_get_contents($tmpDir.'/.env.local');
|
||||
self::assertStringContainsString('STRIPE_WEBHOOK_SECRET=whsec_new123', $content);
|
||||
|
||||
unlink($tmpDir.'/.env.local');
|
||||
rmdir($tmpDir);
|
||||
}
|
||||
|
||||
public function testSaveWebhookSecretUpdatesExisting(): void
|
||||
{
|
||||
$tmpDir = sys_get_temp_dir().'/stripe_test_'.uniqid();
|
||||
mkdir($tmpDir);
|
||||
file_put_contents($tmpDir.'/.env.local', "APP_ENV=test\nSTRIPE_WEBHOOK_SECRET=old_secret\n");
|
||||
|
||||
$service = new StripeService('sk_test', 'whsec_test', 'https://example.com', $tmpDir);
|
||||
$service->saveWebhookSecret('whsec_updated');
|
||||
|
||||
$content = file_get_contents($tmpDir.'/.env.local');
|
||||
self::assertStringContainsString('STRIPE_WEBHOOK_SECRET=whsec_updated', $content);
|
||||
self::assertStringNotContainsString('old_secret', $content);
|
||||
|
||||
unlink($tmpDir.'/.env.local');
|
||||
rmdir($tmpDir);
|
||||
self::assertNull($this->createService()->verifyWebhookSignature('{}', 'invalid'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user