- /stripe/webhook → /webhooks/stripe/insta (paiements, payouts, disputes, subscriptions) - /stripe/webhook/connect → /webhooks/stripe/leger (gestion comptes Connect) - Rename env vars: STRIPE_WEBHOOK_SECRET → STRIPE_WEBHOOK_SECRET_INSTA, STRIPE_WEBHOOK_SECRET_CONNECT → STRIPE_WEBHOOK_SECRET_LEGER - Update StripeService, CsrfProtectionSubscriber, vault, env files and all tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
852 lines
32 KiB
PHP
852 lines
32 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Controller;
|
|
|
|
use App\Entity\Billet;
|
|
use App\Entity\BilletBuyer;
|
|
use App\Entity\BilletBuyerItem;
|
|
use App\Entity\BilletOrder;
|
|
use App\Entity\Category;
|
|
use App\Entity\Payout;
|
|
use App\Entity\User;
|
|
use App\Service\AuditService;
|
|
use App\Service\BilletOrderService;
|
|
use App\Service\MailerService;
|
|
use App\Service\PayoutPdfService;
|
|
use App\Service\StripeService;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Stripe\Event;
|
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|
|
|
class StripeWebhookControllerTest extends WebTestCase
|
|
{
|
|
public function testWebhookWithValidSignature(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn(new Event());
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], [
|
|
'HTTP_STRIPE_SIGNATURE' => 'valid',
|
|
], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
self::assertSame('OK', $client->getResponse()->getContent());
|
|
}
|
|
|
|
public function testWebhookWithInvalidSignature(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn(null);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], [
|
|
'HTTP_STRIPE_SIGNATURE' => 'invalid',
|
|
], '{}');
|
|
|
|
self::assertResponseStatusCodeSame(400);
|
|
}
|
|
|
|
public function testAccountCreatedSetsStartedStatus(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_created');
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account.created';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode(['related_object' => ['id' => 'acct_created']]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_created']);
|
|
self::assertSame('started', $updated->getStripeStatus());
|
|
}
|
|
|
|
public function testAccountClosedDisablesFlags(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_closed');
|
|
$user->setStripeChargesEnabled(true);
|
|
$user->setStripePayoutsEnabled(true);
|
|
$em->flush();
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account.closed';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode(['related_object' => ['id' => 'acct_closed']]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_closed']);
|
|
self::assertSame('closed', $updated->getStripeStatus());
|
|
self::assertFalse($updated->isStripeChargesEnabled());
|
|
self::assertFalse($updated->isStripePayoutsEnabled());
|
|
}
|
|
|
|
public function testAccountUpdatedSetsStatus(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$this->createOrgaUser($em, 'acct_upd');
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account.updated';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode(['related_object' => ['id' => 'acct_upd']]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_upd']);
|
|
self::assertSame('updated', $updated->getStripeStatus());
|
|
}
|
|
|
|
public function testAccountStatusUnknownUser(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account.created';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode(['related_object' => ['id' => 'acct_nonexistent']]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testAccountStatusNoRelatedObject(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account.created';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testMerchantCapabilityCardPaymentsActive(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$this->createOrgaUser($em, 'acct_merchant');
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account[configuration.merchant].capability_status_updated';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode([
|
|
'related_object' => ['id' => 'acct_merchant'],
|
|
'changes' => ['after' => ['configuration' => [
|
|
'merchant' => ['capabilities' => [
|
|
'card_payments' => ['status' => 'active'],
|
|
]],
|
|
]]],
|
|
]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_merchant']);
|
|
self::assertTrue($updated->isStripeChargesEnabled());
|
|
}
|
|
|
|
public function testRecipientCapabilityPayoutsActive(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$this->createOrgaUser($em, 'acct_recipient');
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account[configuration.recipient].capability_status_updated';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode([
|
|
'related_object' => ['id' => 'acct_recipient'],
|
|
'changes' => ['after' => ['configuration' => [
|
|
'recipient' => ['capabilities' => [
|
|
'stripe_balance' => ['payouts' => ['status' => 'active']],
|
|
]],
|
|
]]],
|
|
]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_recipient']);
|
|
self::assertTrue($updated->isStripePayoutsEnabled());
|
|
}
|
|
|
|
public function testCapabilityBothActiveSetStatusActive(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_both');
|
|
$user->setStripeChargesEnabled(true);
|
|
$em->flush();
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account[configuration.merchant].capability_status_updated';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode([
|
|
'related_object' => ['id' => 'acct_both'],
|
|
'changes' => ['after' => ['configuration' => [
|
|
'merchant' => ['capabilities' => [
|
|
'stripe_balance' => ['payouts' => ['status' => 'active']],
|
|
]],
|
|
]]],
|
|
]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(User::class)->findOneBy(['stripeAccountId' => 'acct_both']);
|
|
self::assertSame('active', $updated->getStripeStatus());
|
|
}
|
|
|
|
public function testCapabilityUnknownUser(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = new Event();
|
|
$event->type = 'v2.core.account[configuration.merchant].capability_status_updated';
|
|
|
|
$this->mockStripe($client, $event);
|
|
|
|
$payload = json_encode([
|
|
'related_object' => ['id' => 'acct_ghost'],
|
|
'changes' => ['after' => ['configuration' => []]],
|
|
]);
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], $payload);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testPayoutCreatedSavesAndSendsEmail(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_payout');
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payout.created',
|
|
'account' => 'acct_payout',
|
|
'data' => ['object' => [
|
|
'id' => 'po_test_wh_'.uniqid(),
|
|
'status' => 'pending',
|
|
'amount' => 5000,
|
|
'currency' => 'eur',
|
|
'destination' => 'ba_xxx',
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::once())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testPayoutPaidSendsEmailWithPdf(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_paid');
|
|
|
|
$payoutId = 'po_paid_'.uniqid();
|
|
$event = Event::constructFrom([
|
|
'type' => 'payout.paid',
|
|
'account' => 'acct_paid',
|
|
'data' => ['object' => [
|
|
'id' => $payoutId,
|
|
'status' => 'paid',
|
|
'amount' => 10000,
|
|
'currency' => 'eur',
|
|
'destination' => 'ba_yyy',
|
|
'arrival_date' => time() + 86400,
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::once())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$pdfService = $this->createMock(PayoutPdfService::class);
|
|
$pdfService->expects(self::once())->method('generateToFile')->willReturn('/tmp/fake.pdf');
|
|
static::getContainer()->set(PayoutPdfService::class, $pdfService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$payout = $freshEm->getRepository(Payout::class)->findOneBy(['stripePayoutId' => $payoutId]);
|
|
self::assertNotNull($payout);
|
|
self::assertSame('paid', $payout->getStatus());
|
|
self::assertSame(10000, $payout->getAmount());
|
|
}
|
|
|
|
public function testPayoutUpdatedExistingPayout(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_upd_po');
|
|
|
|
$payout = new Payout();
|
|
$payout->setOrganizer($user);
|
|
$payout->setStripePayoutId('po_existing_'.uniqid());
|
|
$payout->setStatus('pending');
|
|
$payout->setAmount(3000);
|
|
$payout->setCurrency('eur');
|
|
$payout->setStripeAccountId('acct_upd_po');
|
|
$em->persist($payout);
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payout.updated',
|
|
'account' => 'acct_upd_po',
|
|
'data' => ['object' => [
|
|
'id' => $payout->getStripePayoutId(),
|
|
'status' => 'in_transit',
|
|
'amount' => 3000,
|
|
'currency' => 'eur',
|
|
'destination' => 'ba_zzz',
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::once())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(Payout::class)->findOneBy(['stripePayoutId' => $payout->getStripePayoutId()]);
|
|
self::assertSame('in_transit', $updated->getStatus());
|
|
}
|
|
|
|
public function testPayoutNoPayoutId(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payout.created',
|
|
'data' => ['object' => []],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testPayoutNoUser(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payout.created',
|
|
'account' => 'acct_nouser',
|
|
'data' => ['object' => [
|
|
'id' => 'po_nouser_'.uniqid(),
|
|
'status' => 'pending',
|
|
'amount' => 1000,
|
|
'currency' => 'eur',
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
// === payment_intent.payment_failed ===
|
|
|
|
public function testPaymentFailedCancelsOrder(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_pf_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payment_intent.payment_failed',
|
|
'data' => ['object' => [
|
|
'metadata' => ['order_id' => (string) $order->getId()],
|
|
'last_payment_error' => ['message' => 'Your card was declined.'],
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::once())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$audit = $this->createMock(AuditService::class);
|
|
$audit->expects(self::once())->method('log')->with('payment_failed', 'BilletBuyer', $order->getId(), $this->anything());
|
|
static::getContainer()->set(AuditService::class, $audit);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(BilletBuyer::class)->find($order->getId());
|
|
self::assertSame(BilletBuyer::STATUS_CANCELLED, $updated->getStatus());
|
|
}
|
|
|
|
public function testPaymentFailedNoOrderId(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payment_intent.payment_failed',
|
|
'data' => ['object' => ['metadata' => []]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testPaymentFailedOrderNotFound(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payment_intent.payment_failed',
|
|
'data' => ['object' => [
|
|
'metadata' => ['order_id' => '999999'],
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testPaymentFailedOrderAlreadyPaid(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_pf_paid_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
$order->setStatus(BilletBuyer::STATUS_PAID);
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payment_intent.payment_failed',
|
|
'data' => ['object' => [
|
|
'metadata' => ['order_id' => (string) $order->getId()],
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(BilletBuyer::class)->find($order->getId());
|
|
self::assertSame(BilletBuyer::STATUS_PAID, $updated->getStatus());
|
|
}
|
|
|
|
public function testPaymentFailedDefaultErrorMessage(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_pf_def_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payment_intent.payment_failed',
|
|
'data' => ['object' => [
|
|
'metadata' => ['order_id' => (string) $order->getId()],
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::once())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$audit = $this->createMock(AuditService::class);
|
|
$audit->expects(self::once())->method('log')->with(
|
|
'payment_failed',
|
|
'BilletBuyer',
|
|
$order->getId(),
|
|
$this->callback(fn (array $d) => 'Paiement refuse' === $d['error'])
|
|
);
|
|
static::getContainer()->set(AuditService::class, $audit);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testPaymentFailedNoEmail(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_pf_nomail_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
$order->setEmail(null);
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'payment_intent.payment_failed',
|
|
'data' => ['object' => [
|
|
'metadata' => ['order_id' => (string) $order->getId()],
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::never())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$audit = $this->createMock(AuditService::class);
|
|
static::getContainer()->set(AuditService::class, $audit);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
// === charge.refunded ===
|
|
|
|
public function testChargeRefundedUpdatesOrder(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_cr_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
$order->setStatus(BilletBuyer::STATUS_PAID);
|
|
$order->setStripeSessionId('pi_refund_test_'.uniqid());
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'charge.refunded',
|
|
'data' => ['object' => [
|
|
'payment_intent' => $order->getStripeSessionId(),
|
|
]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::atLeastOnce())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$audit = $this->createMock(AuditService::class);
|
|
$audit->expects(self::once())->method('log')->with('order_refunded_webhook', 'BilletBuyer', $order->getId(), $this->anything());
|
|
static::getContainer()->set(AuditService::class, $audit);
|
|
|
|
$billetOrderService = $this->createMock(BilletOrderService::class);
|
|
$billetOrderService->expects(self::once())->method('notifyOrganizerCancelled')->with($this->anything(), 'remboursee');
|
|
static::getContainer()->set(BilletOrderService::class, $billetOrderService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(BilletBuyer::class)->find($order->getId());
|
|
self::assertSame(BilletBuyer::STATUS_REFUNDED, $updated->getStatus());
|
|
}
|
|
|
|
public function testChargeRefundedInvalidatesTickets(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_cr_tk_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
$order->setStatus(BilletBuyer::STATUS_PAID);
|
|
$piId = 'pi_ticket_refund_'.uniqid();
|
|
$order->setStripeSessionId($piId);
|
|
|
|
$ticket = new BilletOrder();
|
|
$ticket->setBilletBuyer($order);
|
|
$ticket->setBilletName('Entree');
|
|
$ticket->setUnitPriceHT(1500);
|
|
$ticket->setState(BilletOrder::STATE_VALID);
|
|
$em->persist($ticket);
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'charge.refunded',
|
|
'data' => ['object' => ['payment_intent' => $piId]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$audit = $this->createMock(AuditService::class);
|
|
static::getContainer()->set(AuditService::class, $audit);
|
|
|
|
$billetOrderService = $this->createMock(BilletOrderService::class);
|
|
static::getContainer()->set(BilletOrderService::class, $billetOrderService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updatedTicket = $freshEm->getRepository(BilletOrder::class)->find($ticket->getId());
|
|
self::assertSame(BilletOrder::STATE_INVALID, $updatedTicket->getState());
|
|
}
|
|
|
|
public function testChargeRefundedNoPaymentIntent(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'charge.refunded',
|
|
'data' => ['object' => []],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testChargeRefundedOrderNotFound(): void
|
|
{
|
|
$client = static::createClient();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'charge.refunded',
|
|
'data' => ['object' => ['payment_intent' => 'pi_nonexistent_'.uniqid()]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testChargeRefundedAlreadyRefunded(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_cr_dup_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
$piId = 'pi_already_refunded_'.uniqid();
|
|
$order->setStatus(BilletBuyer::STATUS_REFUNDED);
|
|
$order->setStripeSessionId($piId);
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'charge.refunded',
|
|
'data' => ['object' => ['payment_intent' => $piId]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
$updated = $freshEm->getRepository(BilletBuyer::class)->find($order->getId());
|
|
self::assertSame(BilletBuyer::STATUS_REFUNDED, $updated->getStatus());
|
|
}
|
|
|
|
public function testChargeRefundedNoEmail(): void
|
|
{
|
|
$client = static::createClient();
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
$user = $this->createOrgaUser($em, 'acct_cr_nomail_'.uniqid());
|
|
$order = $this->createTestOrder($em, $user);
|
|
$order->setStatus(BilletBuyer::STATUS_PAID);
|
|
$piId = 'pi_nomail_'.uniqid();
|
|
$order->setStripeSessionId($piId);
|
|
$order->setEmail(null);
|
|
$em->flush();
|
|
|
|
$event = Event::constructFrom([
|
|
'type' => 'charge.refunded',
|
|
'data' => ['object' => ['payment_intent' => $piId]],
|
|
]);
|
|
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
|
|
$mailer = $this->createMock(MailerService::class);
|
|
$mailer->expects(self::never())->method('sendEmail');
|
|
static::getContainer()->set(MailerService::class, $mailer);
|
|
|
|
$audit = $this->createMock(AuditService::class);
|
|
static::getContainer()->set(AuditService::class, $audit);
|
|
|
|
$billetOrderService = $this->createMock(BilletOrderService::class);
|
|
static::getContainer()->set(BilletOrderService::class, $billetOrderService);
|
|
|
|
$client->request('POST', '/webhooks/stripe/insta', [], [], ['HTTP_STRIPE_SIGNATURE' => 'v'], '{}');
|
|
self::assertResponseIsSuccessful();
|
|
}
|
|
|
|
private function createTestOrder(EntityManagerInterface $em, User $user): BilletBuyer
|
|
{
|
|
$event = new \App\Entity\Event();
|
|
$event->setAccount($user);
|
|
$event->setTitle('WH Event '.uniqid());
|
|
$event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00'));
|
|
$event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00'));
|
|
$event->setAddress('1 rue');
|
|
$event->setZipcode('75001');
|
|
$event->setCity('Paris');
|
|
$event->setIsOnline(true);
|
|
$em->persist($event);
|
|
|
|
$category = new Category();
|
|
$category->setName('Cat');
|
|
$category->setEvent($event);
|
|
$em->persist($category);
|
|
|
|
$billet = new Billet();
|
|
$billet->setName('Entree');
|
|
$billet->setCategory($category);
|
|
$billet->setPriceHT(1500);
|
|
$em->persist($billet);
|
|
|
|
$count = $em->getRepository(BilletBuyer::class)->count([]) + 1;
|
|
$order = new BilletBuyer();
|
|
$order->setEvent($event);
|
|
$order->setFirstName('Jean');
|
|
$order->setLastName('Dupont');
|
|
$order->setEmail('jean-wh@test.fr');
|
|
$order->setOrderNumber('2026-03-23-'.$count);
|
|
$order->setTotalHT(1500);
|
|
|
|
$item = new BilletBuyerItem();
|
|
$item->setBillet($billet);
|
|
$item->setBilletName('Entree');
|
|
$item->setQuantity(1);
|
|
$item->setUnitPriceHT(1500);
|
|
$order->addItem($item);
|
|
|
|
$em->persist($order);
|
|
$em->flush();
|
|
|
|
return $order;
|
|
}
|
|
|
|
private function createOrgaUser(EntityManagerInterface $em, string $stripeAccountId): User
|
|
{
|
|
$user = new User();
|
|
$user->setEmail('test-wh-'.uniqid().'@example.com');
|
|
$user->setFirstName('WH');
|
|
$user->setLastName('Test');
|
|
$user->setPassword('$2y$13$hashed');
|
|
$user->setRoles(['ROLE_ORGANIZER']);
|
|
$user->setStripeAccountId($stripeAccountId);
|
|
$em->persist($user);
|
|
$em->flush();
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function mockStripe(\Symfony\Bundle\FrameworkBundle\KernelBrowser $client, Event $event): void
|
|
{
|
|
$stripeService = $this->createMock(StripeService::class);
|
|
$stripeService->method('verifyWebhookSignatureInsta')->willReturn($event);
|
|
static::getContainer()->set(StripeService::class, $stripeService);
|
|
}
|
|
}
|