From e483e38f53c78853dcc51a8e563693195b4e9403 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:00:38 +0200 Subject: [PATCH] feat(stripe): enhance subscription handling and verification process - Updated StripeProcessJob to include detailed handling of subscription statuses during invoice payment events. - Introduced VerifyStripeSubscriptionStatusJob to manage subscription status verification and updates, improving error handling and notification for various subscription states. - Enhanced logic to handle cases where subscription IDs are missing, ensuring robust subscription management. --- app/Jobs/StripeProcessJob.php | 58 ++++++++-- .../VerifyStripeSubscriptionStatusJob.php | 102 ++++++++++++++++++ 2 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/VerifyStripeSubscriptionStatusJob.php diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php index 088b6c67d..aebceaa6d 100644 --- a/app/Jobs/StripeProcessJob.php +++ b/app/Jobs/StripeProcessJob.php @@ -93,20 +93,66 @@ class StripeProcessJob implements ShouldQueue break; case 'invoice.paid': $customerId = data_get($data, 'customer'); + $invoiceAmount = data_get($data, 'amount_paid', 0); + $subscriptionId = data_get($data, 'subscription'); $planId = data_get($data, 'lines.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if ($subscription) { - $subscription->update([ - 'stripe_invoice_paid' => true, - 'stripe_past_due' => false, - ]); - } else { + if (! $subscription) { throw new \RuntimeException("No subscription found for customer: {$customerId}"); } + + if ($subscription->stripe_subscription_id) { + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $stripeSubscription = $stripe->subscriptions->retrieve( + $subscription->stripe_subscription_id + ); + + switch ($stripeSubscription->status) { + case 'active': + $subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => false, + ]); + break; + + case 'past_due': + $subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => true, + ]); + break; + + case 'canceled': + case 'incomplete_expired': + case 'unpaid': + send_internal_notification( + "Invoice paid for {$stripeSubscription->status} subscription. ". + "Customer: {$customerId}, Amount: \${$invoiceAmount}" + ); + break; + + default: + VerifyStripeSubscriptionStatusJob::dispatch($subscription) + ->delay(now()->addSeconds(20)); + break; + } + } catch (\Exception $e) { + VerifyStripeSubscriptionStatusJob::dispatch($subscription) + ->delay(now()->addSeconds(20)); + + send_internal_notification( + 'Failed to verify subscription status in invoice.paid: '.$e->getMessage() + ); + } + } else { + VerifyStripeSubscriptionStatusJob::dispatch($subscription) + ->delay(now()->addSeconds(20)); + } break; case 'invoice.payment_failed': $customerId = data_get($data, 'customer'); diff --git a/app/Jobs/VerifyStripeSubscriptionStatusJob.php b/app/Jobs/VerifyStripeSubscriptionStatusJob.php new file mode 100644 index 000000000..35b219287 --- /dev/null +++ b/app/Jobs/VerifyStripeSubscriptionStatusJob.php @@ -0,0 +1,102 @@ +onQueue('high'); + } + + public function handle(): void + { + // If no subscription ID yet, try to find it via customer + if (! $this->subscription->stripe_subscription_id && + $this->subscription->stripe_customer_id) { + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $subscriptions = $stripe->subscriptions->all([ + 'customer' => $this->subscription->stripe_customer_id, + 'limit' => 1, + ]); + + if ($subscriptions->data) { + $this->subscription->update([ + 'stripe_subscription_id' => $subscriptions->data[0]->id, + ]); + } + } catch (\Exception $e) { + // Continue without subscription ID + } + } + + if (! $this->subscription->stripe_subscription_id) { + return; + } + + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $stripeSubscription = $stripe->subscriptions->retrieve( + $this->subscription->stripe_subscription_id + ); + + switch ($stripeSubscription->status) { + case 'active': + $this->subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => false, + 'stripe_cancel_at_period_end' => $stripeSubscription->cancel_at_period_end, + ]); + break; + + case 'past_due': + // Keep subscription active but mark as past_due + $this->subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => true, + 'stripe_cancel_at_period_end' => $stripeSubscription->cancel_at_period_end, + ]); + break; + + case 'canceled': + case 'incomplete_expired': + case 'unpaid': + // Ensure subscription is marked as inactive + $this->subscription->update([ + 'stripe_invoice_paid' => false, + 'stripe_past_due' => false, + ]); + + // Trigger subscription ended logic if canceled + if ($stripeSubscription->status === 'canceled') { + $team = $this->subscription->team; + if ($team) { + $team->subscriptionEnded(); + } + } + break; + + default: + send_internal_notification( + 'Unknown subscription status in VerifyStripeSubscriptionStatusJob: '.$stripeSubscription->status. + ' for customer: '.$this->subscription->stripe_customer_id + ); + break; + } + } catch (\Exception $e) { + send_internal_notification( + 'VerifyStripeSubscriptionStatusJob failed for subscription ID '.$this->subscription->id.': '.$e->getMessage() + ); + } + } +}