fix(stripe): ignore missing subscriptions in webhook jobs
Avoid failing Stripe webhook processing when local subscriptions are missing, and cover ignored invoice/payment/subscription events with feature tests.
This commit is contained in:
parent
c33364db71
commit
ff149b8daa
2 changed files with 74 additions and 7 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Stripe\StripeClient;
|
||||||
|
|
||||||
class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue
|
class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -35,7 +36,7 @@ public function handle(): void
|
||||||
$data = data_get($this->event, 'data.object');
|
$data = data_get($this->event, 'data.object');
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'radar.early_fraud_warning.created':
|
case 'radar.early_fraud_warning.created':
|
||||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
$stripe = new StripeClient(config('subscription.stripe_api_key'));
|
||||||
$id = data_get($data, 'id');
|
$id = data_get($data, 'id');
|
||||||
$charge = data_get($data, 'charge');
|
$charge = data_get($data, 'charge');
|
||||||
if ($charge) {
|
if ($charge) {
|
||||||
|
|
@ -94,12 +95,12 @@ public function handle(): void
|
||||||
}
|
}
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||||
if (! $subscription) {
|
if (! $subscription) {
|
||||||
throw new \RuntimeException("No subscription found for customer: {$customerId}");
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($subscription->stripe_subscription_id) {
|
if ($subscription->stripe_subscription_id) {
|
||||||
try {
|
try {
|
||||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
$stripe = new StripeClient(config('subscription.stripe_api_key'));
|
||||||
$stripeSubscription = $stripe->subscriptions->retrieve(
|
$stripeSubscription = $stripe->subscriptions->retrieve(
|
||||||
$subscription->stripe_subscription_id
|
$subscription->stripe_subscription_id
|
||||||
);
|
);
|
||||||
|
|
@ -154,7 +155,7 @@ public function handle(): void
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||||
if (! $subscription) {
|
if (! $subscription) {
|
||||||
// send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
|
// send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
|
||||||
throw new \RuntimeException("No subscription found for customer: {$customerId}");
|
break;
|
||||||
}
|
}
|
||||||
$team = data_get($subscription, 'team');
|
$team = data_get($subscription, 'team');
|
||||||
if (! $team) {
|
if (! $team) {
|
||||||
|
|
@ -165,7 +166,7 @@ public function handle(): void
|
||||||
// Verify payment status with Stripe API before sending failure notification
|
// Verify payment status with Stripe API before sending failure notification
|
||||||
if ($paymentIntentId) {
|
if ($paymentIntentId) {
|
||||||
try {
|
try {
|
||||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
$stripe = new StripeClient(config('subscription.stripe_api_key'));
|
||||||
$paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId);
|
$paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId);
|
||||||
|
|
||||||
if (in_array($paymentIntent->status, ['processing', 'succeeded', 'requires_action', 'requires_confirmation'])) {
|
if (in_array($paymentIntent->status, ['processing', 'succeeded', 'requires_action', 'requires_confirmation'])) {
|
||||||
|
|
@ -190,7 +191,7 @@ public function handle(): void
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||||
if (! $subscription) {
|
if (! $subscription) {
|
||||||
// send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
|
// send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
|
||||||
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
|
break;
|
||||||
}
|
}
|
||||||
if ($subscription->stripe_invoice_paid) {
|
if ($subscription->stripe_invoice_paid) {
|
||||||
// send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
|
// send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
|
||||||
|
|
@ -334,7 +335,7 @@ public function handle(): void
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// send_internal_notification('Subscription deleted but no subscription found in Coolify for customer: '.$customerId);
|
// send_internal_notification('Subscription deleted but no subscription found in Coolify for customer: '.$customerId);
|
||||||
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@
|
||||||
|
|
||||||
use App\Jobs\ServerLimitCheckJob;
|
use App\Jobs\ServerLimitCheckJob;
|
||||||
use App\Jobs\StripeProcessJob;
|
use App\Jobs\StripeProcessJob;
|
||||||
|
use App\Jobs\SubscriptionInvoiceFailedJob;
|
||||||
|
use App\Jobs\VerifyStripeSubscriptionStatusJob;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications\Internal\GeneralNotification;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
@ -228,3 +232,65 @@
|
||||||
Queue::assertNotPushed(ServerLimitCheckJob::class);
|
Queue::assertNotPushed(ServerLimitCheckJob::class);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('missing subscription Stripe webhooks are ignored', function () {
|
||||||
|
test('does not send internal notifications or queue follow-up jobs', function (array $event) {
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
$rootTeam = Team::factory()->create(['id' => 0]);
|
||||||
|
$rootTeam->discordNotificationSettings()->update(['discord_enabled' => true]);
|
||||||
|
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$job = new StripeProcessJob($event);
|
||||||
|
$job->handle();
|
||||||
|
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
Notification::assertNotSentTo($rootTeam, GeneralNotification::class);
|
||||||
|
Queue::assertNotPushed(SubscriptionInvoiceFailedJob::class);
|
||||||
|
Queue::assertNotPushed(VerifyStripeSubscriptionStatusJob::class);
|
||||||
|
})->with([
|
||||||
|
'invoice paid' => [[
|
||||||
|
'type' => 'invoice.paid',
|
||||||
|
'data' => [
|
||||||
|
'object' => [
|
||||||
|
'customer' => 'cus_missing_invoice_paid',
|
||||||
|
'amount_paid' => 1000,
|
||||||
|
'subscription' => 'sub_missing_invoice_paid',
|
||||||
|
'lines' => [
|
||||||
|
'data' => [[
|
||||||
|
'plan' => ['id' => 'price_dynamic_monthly'],
|
||||||
|
]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
'invoice payment failed' => [[
|
||||||
|
'type' => 'invoice.payment_failed',
|
||||||
|
'data' => [
|
||||||
|
'object' => [
|
||||||
|
'customer' => 'cus_missing_invoice_payment_failed',
|
||||||
|
'id' => 'in_missing_invoice_payment_failed',
|
||||||
|
'payment_intent' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
'payment intent payment failed' => [[
|
||||||
|
'type' => 'payment_intent.payment_failed',
|
||||||
|
'data' => [
|
||||||
|
'object' => [
|
||||||
|
'customer' => 'cus_missing_payment_intent_failed',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
'customer subscription deleted' => [[
|
||||||
|
'type' => 'customer.subscription.deleted',
|
||||||
|
'data' => [
|
||||||
|
'object' => [
|
||||||
|
'customer' => 'cus_missing_subscription_deleted',
|
||||||
|
'id' => 'sub_missing_subscription_deleted',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue