From a6af895c79ee5446e13bc11cdb0e60d9ecdaed95 Mon Sep 17 00:00:00 2001 From: Steven Fox <62109327+steven-fox@users.noreply.github.com> Date: Wed, 14 May 2025 11:57:05 -0400 Subject: [PATCH] only create a new user upon customer.subscription.created webhook --- app/Jobs/CreateAnystackLicenseJob.php | 2 +- .../StripeWebhookReceivedListener.php | 6 -- tests/Feature/StripePurchaseHandlingTest.php | 95 +++++++++++++++++-- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/app/Jobs/CreateAnystackLicenseJob.php b/app/Jobs/CreateAnystackLicenseJob.php index afbfe4e5..27c3818a 100644 --- a/app/Jobs/CreateAnystackLicenseJob.php +++ b/app/Jobs/CreateAnystackLicenseJob.php @@ -21,7 +21,7 @@ class CreateAnystackLicenseJob implements ShouldQueue public function __construct( public User $user, public Subscription $subscription, - public ?string $subscriptionItemId = null, + public ?int $subscriptionItemId = null, public ?string $firstName = null, public ?string $lastName = null, ) {} diff --git a/app/Listeners/StripeWebhookReceivedListener.php b/app/Listeners/StripeWebhookReceivedListener.php index 8a1b3ee1..4b853c2a 100644 --- a/app/Listeners/StripeWebhookReceivedListener.php +++ b/app/Listeners/StripeWebhookReceivedListener.php @@ -7,7 +7,6 @@ use Illuminate\Support\Facades\Log; use Laravel\Cashier\Cashier; use Laravel\Cashier\Events\WebhookReceived; -use Stripe\Customer; class StripeWebhookReceivedListener { @@ -16,11 +15,6 @@ public function handle(WebhookReceived $event): void Log::debug('Webhook received', $event->payload); match ($event->payload['type']) { - // 'customer.created' must be dispatched sync so the user is - // created before the cashier webhook handling is executed. - 'customer.created' => dispatch_sync(new CreateUserFromStripeCustomer( - Customer::constructFrom($event->payload['data']['object']) - )), 'customer.subscription.created' => $this->createUserIfNotExists($event->payload['data']['object']['customer']), default => null, }; diff --git a/tests/Feature/StripePurchaseHandlingTest.php b/tests/Feature/StripePurchaseHandlingTest.php index d3954b06..bc486cca 100644 --- a/tests/Feature/StripePurchaseHandlingTest.php +++ b/tests/Feature/StripePurchaseHandlingTest.php @@ -31,7 +31,7 @@ protected function setUp(): void } #[Test] - public function a_user_is_created_when_a_stripe_customer_is_created() + public function a_user_is_not_created_when_a_stripe_customer_is_created() { Bus::fake(); @@ -49,9 +49,89 @@ public function a_user_is_created_when_a_stripe_customer_is_created() $this->postJson('/stripe/webhook', $payload); + Bus::assertNotDispatched(CreateUserFromStripeCustomer::class); + } + + #[Test] + public function a_user_is_created_when_a_stripe_customer_subscription_is_created_and_a_matching_user_doesnt_exist() + { + Bus::fake(); + + $this->mockStripeClient(); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_test123', + 'customer' => 'cus_test123', + 'status' => 'active', + 'items' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'si_test', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'product' => 'prod_test', + ], + 'quantity' => 1, + ], + ], + ], + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + Bus::assertDispatched(CreateUserFromStripeCustomer::class); } + #[Test] + public function a_user_is_not_created_when_a_stripe_customer_subscription_is_created_if_a_matching_user_already_exists() + { + Bus::fake(); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_test123', + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + + $this->mockStripeClient($user); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_test123', + 'customer' => $user->stripe_id, + 'status' => 'active', + 'items' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'si_test', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'product' => 'prod_test', + ], + 'quantity' => 1, + ], + ], + ], + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + + Bus::assertNotDispatched(CreateUserFromStripeCustomer::class); + } + #[Test] public function a_license_is_created_when_a_stripe_subscription_is_created() { @@ -95,6 +175,7 @@ public function a_license_is_created_when_a_stripe_subscription_is_created() Bus::assertDispatched(CreateAnystackLicenseJob::class, function (CreateAnystackLicenseJob $job) { return $job->user->email === 'john@example.com' && $job->subscription === Subscription::Max && + $job->subscriptionItemId === $job->user->subscriptions->first()->items()->first()->id && $job->firstName === 'John' && $job->lastName === 'Doe'; }); @@ -105,7 +186,7 @@ public function a_license_is_created_when_a_stripe_subscription_is_created() $this->assertNotEmpty($user->subscriptions->first()->items); } - protected function mockStripeClient(User $user): void + protected function mockStripeClient(?User $user = null): void { $mockStripeClient = $this->createMock(StripeClient::class); $mockStripeClient->customers = new class($user) @@ -120,13 +201,15 @@ public function __construct($user) public function retrieve() { return Customer::constructFrom([ - 'id' => $this->user->stripe_id, - 'name' => $this->user->name, - 'email' => $this->user->email, + 'id' => $this->user?->stripe_id ?: 'cus_test123', + 'name' => $this->user?->name ?: 'Test Customer', + 'email' => $this->user?->email ?: 'test@example.com', ]); } }; - $this->app->instance(StripeClient::class, $mockStripeClient); + $this->app->bind(StripeClient::class, function ($app, $parameters) use ($mockStripeClient) { + return $mockStripeClient; + }); } }