stripeService = $this->createMock(StripeService::class); $this->billetOrderService = $this->createMock(BilletOrderService::class); $this->mailerService = $this->createMock(MailerService::class); $this->audit = $this->createMock(AuditService::class); $this->userRepo = $this->createMock(EntityRepository::class); $this->buyerRepo = $this->createMock(EntityRepository::class); $this->buyerRepo->method('findBy')->willReturn([]); $this->em = $this->createMock(EntityManagerInterface::class); $this->em->method('getRepository')->willReturnCallback(function (string $class) { return match ($class) { User::class => $this->userRepo, BilletBuyer::class => $this->buyerRepo, default => $this->createMock(EntityRepository::class), }; }); } private function createOrganizer(string $stripeId, bool $charges = false, bool $payouts = false): User { $user = new User(); $user->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 createPendingOrder(?string $paymentIntentId = null): BilletBuyer { $event = $this->createMock(Event::class); $event->method('getTitle')->willReturn('Test Event'); $event->method('getAccount')->willReturn($this->createOrganizer('acct_org')); $order = new BilletBuyer(); $order->setStatus(BilletBuyer::STATUS_PENDING); $order->setStripeSessionId($paymentIntentId); $order->setEmail('buyer@test.fr'); $order->setEvent($event); return $order; } private function createCommandTester(): CommandTester { $command = new StripeSyncCommand( $this->em, $this->stripeService, $this->billetOrderService, $this->mailerService, $this->audit, ); $app = new Application(); $app->addCommand($command); return new CommandTester($app->find('app:stripe:sync')); } private function setBuyerRepo(array $orders): void { $this->buyerRepo = $this->createMock(EntityRepository::class); $this->buyerRepo->method('findBy')->willReturn($orders); $this->em = $this->createMock(EntityManagerInterface::class); $this->em->method('getRepository')->willReturnCallback(function (string $class) { return match ($class) { User::class => $this->userRepo, BilletBuyer::class => $this->buyerRepo, default => $this->createMock(EntityRepository::class), }; }); } // --- Account sync tests --- public function testSyncUpdatesStripeStatus(): void { $user = $this->createOrganizer('acct_123'); $this->userRepo->method('findAll')->willReturn([$user]); $this->stripeService->method('retrieveAccountStatus') ->with('acct_123') ->willReturn(['charges_enabled' => true, 'payouts_enabled' => true]); $tester = $this->createCommandTester(); $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); $this->userRepo->method('findAll')->willReturn([$user]); $this->stripeService->method('retrieveAccountStatus') ->willReturn(['charges_enabled' => true, 'payouts_enabled' => false]); $tester = $this->createCommandTester(); $tester->execute([]); self::assertTrue($user->isStripeChargesEnabled()); self::assertFalse($user->isStripePayoutsEnabled()); self::assertStringContainsString('UPDATED', $tester->getDisplay()); } public function testSyncWithNoOrganizers(): void { $this->userRepo->method('findAll')->willReturn([]); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('No organizers', $tester->getDisplay()); self::assertSame(0, $tester->getStatusCode()); } public function testSyncHandlesStripeError(): void { $user = $this->createOrganizer('acct_bad'); $this->userRepo->method('findAll')->willReturn([$user]); $this->stripeService->method('retrieveAccountStatus') ->willThrowException(new \RuntimeException('Account not found')); $tester = $this->createCommandTester(); $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']); $this->userRepo->method('findAll')->willReturn([$userWithStripe, $userWithoutStripe]); $this->stripeService->method('retrieveAccountStatus') ->willReturn(['charges_enabled' => true, 'payouts_enabled' => false]); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('Syncing 1 organizer', $tester->getDisplay()); self::assertStringContainsString('1 synced', $tester->getDisplay()); self::assertTrue($userWithStripe->isStripeChargesEnabled()); self::assertFalse($userWithStripe->isStripePayoutsEnabled()); } // --- Pending orders sync tests --- public function testNoPendingOrders(): void { $this->userRepo->method('findAll')->willReturn([]); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('No pending orders', $tester->getDisplay()); } public function testPendingOrderSucceeded(): void { $this->userRepo->method('findAll')->willReturn([]); $order = $this->createPendingOrder('pi_succeeded'); $this->setBuyerRepo([$order]); $paymentIntent = \Stripe\PaymentIntent::constructFrom([ 'id' => 'pi_succeeded', 'status' => 'succeeded', 'amount' => 5000, 'metadata' => [], ]); $this->stripeService->method('retrievePaymentIntent') ->with('pi_succeeded') ->willReturn($paymentIntent); $this->billetOrderService->expects(self::once())->method('generateOrderTickets')->with($order); $this->billetOrderService->expects(self::once())->method('generateAndSendTickets')->with($order); $this->billetOrderService->expects(self::once())->method('notifyOrganizer')->with($order); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('PAID', $tester->getDisplay()); } public function testPendingOrderSucceededWithDebtOrganizer(): void { $organizer = $this->createOrganizer('acct_debt'); $this->userRepo->method('findAll')->willReturn([]); $this->userRepo->method('find')->with(42)->willReturn($organizer); $order = $this->createPendingOrder('pi_debt'); $this->setBuyerRepo([$order]); $paymentIntent = \Stripe\PaymentIntent::constructFrom([ 'id' => 'pi_debt', 'status' => 'succeeded', 'amount' => 3000, 'metadata' => ['debt_organizer_id' => '42'], ]); $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('PAID', $tester->getDisplay()); } public function testPendingOrderCanceled(): void { $this->userRepo->method('findAll')->willReturn([]); $order = $this->createPendingOrder('pi_canceled'); $this->setBuyerRepo([$order]); $paymentIntent = \Stripe\PaymentIntent::constructFrom([ 'id' => 'pi_canceled', 'status' => 'canceled', ]); $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent); $this->audit->expects(self::once())->method('log') ->with('payment_cancelled_sync', 'BilletBuyer', self::anything(), self::anything()); $tester = $this->createCommandTester(); $tester->execute([]); self::assertSame(BilletBuyer::STATUS_CANCELLED, $order->getStatus()); self::assertStringContainsString('CANCELLED', $tester->getDisplay()); } public function testPendingOrderFailed(): void { $this->userRepo->method('findAll')->willReturn([]); $order = $this->createPendingOrder('pi_failed'); $this->setBuyerRepo([$order]); $paymentIntent = \Stripe\PaymentIntent::constructFrom([ 'id' => 'pi_failed', 'status' => 'requires_payment_method', 'last_payment_error' => ['message' => 'Card declined'], ]); $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent); $this->audit->expects(self::once())->method('log') ->with('payment_failed_sync', 'BilletBuyer', self::anything(), self::anything()); $this->mailerService->expects(self::once())->method('sendEmail'); $tester = $this->createCommandTester(); $tester->execute([]); self::assertSame(BilletBuyer::STATUS_CANCELLED, $order->getStatus()); self::assertStringContainsString('FAILED', $tester->getDisplay()); } public function testPendingOrderSkippedWithoutPaymentIntentId(): void { $this->userRepo->method('findAll')->willReturn([]); $order = $this->createPendingOrder(null); $this->setBuyerRepo([$order]); $this->stripeService->expects(self::never())->method('retrievePaymentIntent'); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('SKIP', $tester->getDisplay()); } public function testPendingOrderStillPending(): void { $this->userRepo->method('findAll')->willReturn([]); $order = $this->createPendingOrder('pi_processing'); $this->setBuyerRepo([$order]); $paymentIntent = \Stripe\PaymentIntent::constructFrom([ 'id' => 'pi_processing', 'status' => 'processing', ]); $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent); $tester = $this->createCommandTester(); $tester->execute([]); self::assertSame(BilletBuyer::STATUS_PENDING, $order->getStatus()); self::assertStringContainsString('PENDING', $tester->getDisplay()); } public function testPendingOrderStripeApiError(): void { $this->userRepo->method('findAll')->willReturn([]); $order = $this->createPendingOrder('pi_error'); $this->setBuyerRepo([$order]); $this->stripeService->method('retrievePaymentIntent') ->willThrowException(new \RuntimeException('Stripe API error')); $tester = $this->createCommandTester(); $tester->execute([]); self::assertStringContainsString('ERROR', $tester->getDisplay()); self::assertSame(1, $tester->getStatusCode()); } public function testPendingOrderFailedWithoutEmail(): void { $this->userRepo->method('findAll')->willReturn([]); $event = $this->createMock(Event::class); $event->method('getTitle')->willReturn('Test Event'); $order = new BilletBuyer(); $order->setStatus(BilletBuyer::STATUS_PENDING); $order->setStripeSessionId('pi_no_email'); $order->setEvent($event); $this->setBuyerRepo([$order]); $paymentIntent = \Stripe\PaymentIntent::constructFrom([ 'id' => 'pi_no_email', 'status' => 'requires_payment_method', 'last_payment_error' => null, ]); $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent); $this->mailerService->expects(self::never())->method('sendEmail'); $tester = $this->createCommandTester(); $tester->execute([]); self::assertSame(BilletBuyer::STATUS_CANCELLED, $order->getStatus()); self::assertStringContainsString('FAILED', $tester->getDisplay()); } }