From 598157dfccf0f475a4969f9fb51b1f71e65f59d0 Mon Sep 17 00:00:00 2001 From: Steven Fox <62109327+steven-fox@users.noreply.github.com> Date: Fri, 9 May 2025 21:06:50 -0400 Subject: [PATCH 1/3] send failing jobs to sentry --- app/Providers/AppServiceProvider.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f58138ee..66fb3ecf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,8 +3,14 @@ namespace App\Providers; use App\Support\GitHub; +use Illuminate\Queue\Events\JobFailed; +use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; +use Sentry\State\Scope; + +use function Sentry\captureException; +use function Sentry\configureScope; class AppServiceProvider extends ServiceProvider { @@ -22,6 +28,8 @@ public function register(): void public function boot(): void { $this->registerSharedViewVariables(); + + $this->sendFailingJobsToSentry(); } private function registerSharedViewVariables(): void @@ -35,4 +43,22 @@ private function registerSharedViewVariables(): void View::share('openCollectiveLink', 'https://opencollective.com/nativephp'); View::share('githubLink', 'https://github.com/NativePHP'); } + + private function sendFailingJobsToSentry(): void + { + Queue::failing(static function (JobFailed $event) { + if (app()->bound('sentry')) { + configureScope(function (Scope $scope) use ($event): void { + $scope->setContext('job', [ + 'connection' => $event->connectionName, + 'queue' => $event->job->getQueue(), + 'name' => $event->job->resolveName(), + 'payload' => $event->job->payload(), + ]); + }); + + captureException($event->exception); + } + }); + } } From 2de169731291358e7a7de662d7b3367d47904595 Mon Sep 17 00:00:00 2001 From: Steven Fox <62109327+steven-fox@users.noreply.github.com> Date: Fri, 9 May 2025 21:07:35 -0400 Subject: [PATCH 2/3] improve handling & timing of new stripe customers --- app/Jobs/CreateUserFromStripeCustomer.php | 14 +++++++++---- .../StripeWebhookReceivedListener.php | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/Jobs/CreateUserFromStripeCustomer.php b/app/Jobs/CreateUserFromStripeCustomer.php index e383259c..a54a5057 100644 --- a/app/Jobs/CreateUserFromStripeCustomer.php +++ b/app/Jobs/CreateUserFromStripeCustomer.php @@ -9,6 +9,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Laravel\Cashier\Cashier; use Stripe\Customer; @@ -21,14 +22,19 @@ public function __construct(public Customer $customer) {} public function handle(): void { - if (Cashier::findBillable($this->customer)) { - $this->fail("A user already exists for Stripe customer [{$this->customer->id}]."); + /** @var User $user */ + if ($user = Cashier::findBillable($this->customer)) { + Log::debug("A user [{$user->id} | {$user->email}] with stripe_id [{$this->customer->id}] already exists."); return; } - if (User::query()->where('email', $this->customer->email)->exists()) { - $this->fail("A user already exists for email [{$this->customer->email}]."); + if ($user = User::query()->where('email', $this->customer->email)->first()) { + // This could occur if a user performs/attempts multiple checkouts with the same email address. + // In the event all existing stripe customers for this email address do NOT have an active + // subscription, we could theoretically update the stripe_id for the existing user + // and continue. However, for now, we will throw an exception. + $this->fail("A user with email [{$user->email}] already exists but the current stripe_id [{$user->stripe_id}] does not match the new customer id [{$this->customer->id}]."); return; } diff --git a/app/Listeners/StripeWebhookReceivedListener.php b/app/Listeners/StripeWebhookReceivedListener.php index 7ccfd1a3..2dc8c3b1 100644 --- a/app/Listeners/StripeWebhookReceivedListener.php +++ b/app/Listeners/StripeWebhookReceivedListener.php @@ -3,7 +3,9 @@ namespace App\Listeners; use App\Jobs\CreateUserFromStripeCustomer; +use Exception; use Illuminate\Support\Facades\Log; +use Laravel\Cashier\Cashier; use Laravel\Cashier\Events\WebhookReceived; use Stripe\Customer; @@ -19,7 +21,25 @@ public function handle(WebhookReceived $event): void 'customer.created' => dispatch_sync(new CreateUserFromStripeCustomer( Customer::constructFrom($event->payload['data']['object']) )), + 'customer.subscription.created' => $this->createUserIfNotExists($event->payload['data']['object']['customer']), default => null, }; } + + private function createUserIfNotExists(string $stripeCustomerId): void + { + if (Cashier::findBillable($stripeCustomerId)) { + return; + } + + $customer = Customer::retrieve($stripeCustomerId); + + if (! $customer->email) { + throw new Exception( + 'A user needed to be created for customer.subscription.created but was unable to retrieve the customer from Stripe.' + ); + } + + dispatch_sync(new CreateUserFromStripeCustomer($customer)); + } } From 4379cb401f1203f9a7e2d94f3010cbd9adc20b6d Mon Sep 17 00:00:00 2001 From: Steven Fox <62109327+steven-fox@users.noreply.github.com> Date: Fri, 9 May 2025 21:10:52 -0400 Subject: [PATCH 3/3] fix conditional --- app/Listeners/StripeWebhookReceivedListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Listeners/StripeWebhookReceivedListener.php b/app/Listeners/StripeWebhookReceivedListener.php index 2dc8c3b1..dd0e5855 100644 --- a/app/Listeners/StripeWebhookReceivedListener.php +++ b/app/Listeners/StripeWebhookReceivedListener.php @@ -34,7 +34,7 @@ private function createUserIfNotExists(string $stripeCustomerId): void $customer = Customer::retrieve($stripeCustomerId); - if (! $customer->email) { + if (! $customer) { throw new Exception( 'A user needed to be created for customer.subscription.created but was unable to retrieve the customer from Stripe.' );