Add Stripe Connect account support and account.updated webhook handler
- Add stripeChargesEnabled and stripePayoutsEnabled fields to User entity + migration - Handle account.updated webhook: sync charges_enabled and payouts_enabled from Stripe - Add createAccountConnect() and createAccountLink() to StripeService - Update organizer approved email with Stripe verification notice - Tests: webhook account.updated with flags, unknown account, User stripe fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
33
migrations/Version20260319194243.php
Normal file
33
migrations/Version20260319194243.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260319194243 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE "user" ADD stripe_charges_enabled BOOLEAN NOT NULL DEFAULT FALSE');
|
||||
$this->addSql('ALTER TABLE "user" ADD stripe_payouts_enabled BOOLEAN NOT NULL DEFAULT FALSE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE "user" DROP stripe_charges_enabled');
|
||||
$this->addSql('ALTER TABLE "user" DROP stripe_payouts_enabled');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
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\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -11,7 +13,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
class StripeWebhookController extends AbstractController
|
||||
{
|
||||
#[Route('/stripe/webhook', name: 'app_stripe_webhook', methods: ['POST'])]
|
||||
public function webhook(Request $request, StripeService $stripeService): Response
|
||||
public function webhook(Request $request, StripeService $stripeService, EntityManagerInterface $em): Response
|
||||
{
|
||||
$payload = $request->getContent();
|
||||
$signature = $request->headers->get('Stripe-Signature', '');
|
||||
@@ -22,6 +24,21 @@ class StripeWebhookController extends AbstractController
|
||||
return new Response('Invalid signature', 400);
|
||||
}
|
||||
|
||||
if ('account.updated' === $event->type) {
|
||||
$account = $event->data->object;
|
||||
$accountId = $account->id ?? null;
|
||||
|
||||
if ($accountId) {
|
||||
$user = $em->getRepository(User::class)->findOneBy(['stripeAccountId' => $accountId]);
|
||||
|
||||
if ($user) {
|
||||
$user->setStripeChargesEnabled((bool) ($account->charges_enabled ?? false));
|
||||
$user->setStripePayoutsEnabled((bool) ($account->payouts_enabled ?? false));
|
||||
$em->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('OK', 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $stripeAccountId = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $stripeChargesEnabled = false;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $stripePayoutsEnabled = false;
|
||||
|
||||
#[ORM\Column(length: 64, nullable: true)]
|
||||
private ?string $emailVerificationToken = null;
|
||||
|
||||
@@ -311,6 +317,30 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isStripeChargesEnabled(): bool
|
||||
{
|
||||
return $this->stripeChargesEnabled;
|
||||
}
|
||||
|
||||
public function setStripeChargesEnabled(bool $stripeChargesEnabled): static
|
||||
{
|
||||
$this->stripeChargesEnabled = $stripeChargesEnabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isStripePayoutsEnabled(): bool
|
||||
{
|
||||
return $this->stripePayoutsEnabled;
|
||||
}
|
||||
|
||||
public function setStripePayoutsEnabled(bool $stripePayoutsEnabled): static
|
||||
{
|
||||
$this->stripePayoutsEnabled = $stripePayoutsEnabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getResetCode(): ?string
|
||||
{
|
||||
return $this->resetCode;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\User;
|
||||
use Stripe\Event;
|
||||
use Stripe\Exception\SignatureVerificationException;
|
||||
use Stripe\StripeClient;
|
||||
@@ -75,6 +76,38 @@ class StripeService
|
||||
}
|
||||
}
|
||||
|
||||
public function createAccountConnect(User $user): string
|
||||
{
|
||||
$account = $this->stripe->accounts->create([
|
||||
'type' => 'express',
|
||||
'country' => 'FR',
|
||||
'email' => $user->getEmail(),
|
||||
'capabilities' => [
|
||||
'card_payments' => ['requested' => true],
|
||||
'transfers' => ['requested' => true],
|
||||
],
|
||||
'business_type' => \in_array('ROLE_ORGANIZER', $user->getRoles(), true) ? 'company' : 'individual',
|
||||
'business_profile' => [
|
||||
'name' => $user->getCompanyName(),
|
||||
'url' => $this->outsideUrl,
|
||||
],
|
||||
]);
|
||||
|
||||
return $account->id;
|
||||
}
|
||||
|
||||
public function createAccountLink(string $accountId): string
|
||||
{
|
||||
$link = $this->stripe->accountLinks->create([
|
||||
'account' => $accountId,
|
||||
'refresh_url' => $this->outsideUrl.'/stripe/connect/refresh',
|
||||
'return_url' => $this->outsideUrl.'/stripe/connect/return',
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
return $link->url;
|
||||
}
|
||||
|
||||
public function getClient(): StripeClient
|
||||
{
|
||||
return $this->stripe;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<h2>Felicitations {{ firstName }} !</h2>
|
||||
<p>Votre demande de compte organisateur a ete <strong>approuvee</strong> par l'equipe E-Ticket.</p>
|
||||
<p>Vous pouvez desormais vous connecter et commencer a creer vos evenements.</p>
|
||||
<div style="background:#fef3c7;border-left:4px solid #f59e0b;padding:12px 16px;margin:16px 0;">
|
||||
<p style="font-weight:700;font-size:13px;color:#92400e;margin:0 0 4px;">Important :</p>
|
||||
<p style="margin:0;font-size:14px;color:#3f3f46;">Une fois connecte a votre compte, vous devrez effectuer la verification Stripe pour pouvoir recevoir les paiements de vos evenements. Cette etape est obligatoire pour activer les virements sur votre compte bancaire.</p>
|
||||
</div>
|
||||
<p style="text-align:center;margin:32px 0;">
|
||||
<a href="{{ loginUrl }}" class="btn">Se connecter</a>
|
||||
</p>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Service\StripeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Stripe\Event;
|
||||
use Stripe\StripeObject;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class StripeWebhookControllerTest extends WebTestCase
|
||||
@@ -38,4 +41,68 @@ class StripeWebhookControllerTest extends WebTestCase
|
||||
|
||||
self::assertResponseStatusCodeSame(400);
|
||||
}
|
||||
|
||||
public function testWebhookAccountUpdatedSetsFlags(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail('test-stripe-'.uniqid().'@example.com');
|
||||
$user->setFirstName('Stripe');
|
||||
$user->setLastName('Test');
|
||||
$user->setPassword('$2y$13$hashed');
|
||||
$user->setStripeAccountId('acct_test123');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
$account = StripeObject::constructFrom([
|
||||
'id' => 'acct_test123',
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
]);
|
||||
|
||||
$event = new Event();
|
||||
$event->type = 'account.updated';
|
||||
$event->data = StripeObject::constructFrom(['object' => $account]);
|
||||
|
||||
$stripeService = $this->createMock(StripeService::class);
|
||||
$stripeService->method('verifyWebhookSignature')->willReturn($event);
|
||||
static::getContainer()->set(StripeService::class, $stripeService);
|
||||
|
||||
$client->request('POST', '/stripe/webhook', [], [], [
|
||||
'HTTP_STRIPE_SIGNATURE' => 'valid',
|
||||
], '{}');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$em->refresh($user);
|
||||
self::assertTrue($user->isStripeChargesEnabled());
|
||||
self::assertTrue($user->isStripePayoutsEnabled());
|
||||
}
|
||||
|
||||
public function testWebhookAccountUpdatedUnknownAccount(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$account = StripeObject::constructFrom([
|
||||
'id' => 'acct_unknown',
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
]);
|
||||
|
||||
$event = new Event();
|
||||
$event->type = 'account.updated';
|
||||
$event->data = StripeObject::constructFrom(['object' => $account]);
|
||||
|
||||
$stripeService = $this->createMock(StripeService::class);
|
||||
$stripeService->method('verifyWebhookSignature')->willReturn($event);
|
||||
static::getContainer()->set(StripeService::class, $stripeService);
|
||||
|
||||
$client->request('POST', '/stripe/webhook', [], [], [
|
||||
'HTTP_STRIPE_SIGNATURE' => 'valid',
|
||||
], '{}');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,15 +146,22 @@ class UserTest extends TestCase
|
||||
self::assertSame(1.5, $user->getCommissionRate());
|
||||
}
|
||||
|
||||
public function testStripeAccountIdField(): void
|
||||
public function testStripeFields(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
self::assertNull($user->getStripeAccountId());
|
||||
self::assertFalse($user->isStripeChargesEnabled());
|
||||
self::assertFalse($user->isStripePayoutsEnabled());
|
||||
|
||||
$result = $user->setStripeAccountId('acct_1234567890')
|
||||
->setStripeChargesEnabled(true)
|
||||
->setStripePayoutsEnabled(true);
|
||||
|
||||
$result = $user->setStripeAccountId('acct_1234567890');
|
||||
self::assertSame($user, $result);
|
||||
self::assertSame('acct_1234567890', $user->getStripeAccountId());
|
||||
self::assertTrue($user->isStripeChargesEnabled());
|
||||
self::assertTrue($user->isStripePayoutsEnabled());
|
||||
}
|
||||
|
||||
public function testEmailVerificationFields(): void
|
||||
|
||||
Reference in New Issue
Block a user