From c207fd31b1793e1f33144c4e0c341741a6128a2e Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 26 Mar 2026 09:56:11 +0100 Subject: [PATCH] Add app:stripe:sync command to sync Stripe status for all organizers Fetches charges_enabled and payouts_enabled from Stripe API for each organizer with a connected account and updates the local database. Also adds retrieveAccountStatus() to StripeService for testability. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Command/StripeSyncCommand.php | 88 ++++++++++++++++ src/Service/StripeService.php | 15 +++ tests/Command/StripeSyncCommandTest.php | 130 ++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 src/Command/StripeSyncCommand.php create mode 100644 tests/Command/StripeSyncCommandTest.php diff --git a/src/Command/StripeSyncCommand.php b/src/Command/StripeSyncCommand.php new file mode 100644 index 0000000..3d6b8e9 --- /dev/null +++ b/src/Command/StripeSyncCommand.php @@ -0,0 +1,88 @@ +em->getRepository(User::class)->findAll(), + fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && null !== $u->getStripeAccountId(), + ); + + if (0 === \count($organizers)) { + $io->info('No organizers with Stripe accounts found.'); + + return Command::SUCCESS; + } + + $io->info(sprintf('Syncing %d organizer(s)...', \count($organizers))); + + $synced = 0; + $errors = 0; + + foreach ($organizers as $user) { + try { + $status = $this->stripeService->retrieveAccountStatus($user->getStripeAccountId()); + + $chargesBefore = $user->isStripeChargesEnabled(); + $payoutsBefore = $user->isStripePayoutsEnabled(); + + $user->setStripeChargesEnabled($status['charges_enabled']); + $user->setStripePayoutsEnabled($status['payouts_enabled']); + + $changed = $chargesBefore !== $user->isStripeChargesEnabled() + || $payoutsBefore !== $user->isStripePayoutsEnabled(); + + $status = $changed ? 'UPDATED' : 'OK'; + $io->text(sprintf( + ' [%s] %s (%s) — charges: %s, payouts: %s', + $status, + $user->getCompanyName() ?? $user->getEmail(), + $user->getStripeAccountId(), + $user->isStripeChargesEnabled() ? 'yes' : 'no', + $user->isStripePayoutsEnabled() ? 'yes' : 'no', + )); + + ++$synced; + } catch (\Throwable $e) { + $io->text(sprintf( + ' [ERROR] %s (%s) — %s', + $user->getCompanyName() ?? $user->getEmail(), + $user->getStripeAccountId(), + $e->getMessage(), + )); + ++$errors; + } + } + + $this->em->flush(); + + $io->success(sprintf('Done: %d synced, %d error(s).', $synced, $errors)); + + return $errors > 0 ? Command::FAILURE : Command::SUCCESS; + } +} diff --git a/src/Service/StripeService.php b/src/Service/StripeService.php index 180f5ef..3a1985e 100644 --- a/src/Service/StripeService.php +++ b/src/Service/StripeService.php @@ -171,4 +171,19 @@ class StripeService { return $this->stripe; } + + /** + * @codeCoverageIgnore Requires live Stripe API + * + * @return array{charges_enabled: bool, payouts_enabled: bool} + */ + public function retrieveAccountStatus(string $accountId): array + { + $account = $this->stripe->accounts->retrieve($accountId); + + return [ + 'charges_enabled' => (bool) $account->charges_enabled, + 'payouts_enabled' => (bool) $account->payouts_enabled, + ]; + } } diff --git a/tests/Command/StripeSyncCommandTest.php b/tests/Command/StripeSyncCommandTest.php new file mode 100644 index 0000000..d9c01e2 --- /dev/null +++ b/tests/Command/StripeSyncCommandTest.php @@ -0,0 +1,130 @@ +setEmail('orga-'.uniqid().'@test.fr'); + $user->setFirstName('Test'); + $user->setLastName('Orga'); + $user->setPassword('hashed'); + $user->setRoles(['ROLE_ORGANIZER']); + $user->setStripeAccountId($stripeId); + $user->setStripeChargesEnabled($charges); + $user->setStripePayoutsEnabled($payouts); + $user->setCompanyName('Asso Test'); + + return $user; + } + + private function createCommandTester(array $users, StripeService $stripeService): CommandTester + { + $repo = $this->createMock(EntityRepository::class); + $repo->method('findAll')->willReturn($users); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repo); + + $command = new StripeSyncCommand($em, $stripeService); + $app = new Application(); + $app->addCommand($command); + + return new CommandTester($app->find('app:stripe:sync')); + } + + public function testSyncUpdatesStripeStatus(): void + { + $user = $this->createOrganizer('acct_123'); + + $stripeService = $this->createMock(StripeService::class); + $stripeService->method('retrieveAccountStatus') + ->with('acct_123') + ->willReturn(['charges_enabled' => true, 'payouts_enabled' => true]); + + $tester = $this->createCommandTester([$user], $stripeService); + $tester->execute([]); + + self::assertTrue($user->isStripeChargesEnabled()); + self::assertTrue($user->isStripePayoutsEnabled()); + self::assertStringContainsString('1 synced', $tester->getDisplay()); + self::assertSame(0, $tester->getStatusCode()); + } + + public function testSyncDetectsChanges(): void + { + $user = $this->createOrganizer('acct_456', true, true); + + $stripeService = $this->createMock(StripeService::class); + $stripeService->method('retrieveAccountStatus') + ->willReturn(['charges_enabled' => true, 'payouts_enabled' => false]); + + $tester = $this->createCommandTester([$user], $stripeService); + $tester->execute([]); + + self::assertTrue($user->isStripeChargesEnabled()); + self::assertFalse($user->isStripePayoutsEnabled()); + self::assertStringContainsString('UPDATED', $tester->getDisplay()); + } + + public function testSyncWithNoOrganizers(): void + { + $stripeService = $this->createMock(StripeService::class); + + $tester = $this->createCommandTester([], $stripeService); + $tester->execute([]); + + self::assertStringContainsString('No organizers', $tester->getDisplay()); + self::assertSame(0, $tester->getStatusCode()); + } + + public function testSyncHandlesStripeError(): void + { + $user = $this->createOrganizer('acct_bad'); + + $stripeService = $this->createMock(StripeService::class); + $stripeService->method('retrieveAccountStatus') + ->willThrowException(new \RuntimeException('Account not found')); + + $tester = $this->createCommandTester([$user], $stripeService); + $tester->execute([]); + + self::assertStringContainsString('1 error', $tester->getDisplay()); + self::assertSame(1, $tester->getStatusCode()); + } + + public function testSyncSkipsOrganizersWithoutStripeAccount(): void + { + $userWithStripe = $this->createOrganizer('acct_ok'); + + $userWithoutStripe = new User(); + $userWithoutStripe->setEmail('no-stripe@test.fr'); + $userWithoutStripe->setFirstName('No'); + $userWithoutStripe->setLastName('Stripe'); + $userWithoutStripe->setPassword('hashed'); + $userWithoutStripe->setRoles(['ROLE_ORGANIZER']); + + $stripeService = $this->createMock(StripeService::class); + $stripeService->method('retrieveAccountStatus') + ->willReturn(['charges_enabled' => true, 'payouts_enabled' => false]); + + $tester = $this->createCommandTester([$userWithStripe, $userWithoutStripe], $stripeService); + $tester->execute([]); + + self::assertStringContainsString('Syncing 1 organizer', $tester->getDisplay()); + self::assertStringContainsString('1 synced', $tester->getDisplay()); + self::assertTrue($userWithStripe->isStripeChargesEnabled()); + self::assertFalse($userWithStripe->isStripePayoutsEnabled()); + } +}