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..dd0e5855 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) { + 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)); + } } 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); + } + }); + } }