From c33364db71e58a988db9ee00ee1d7c8e1e56de6c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 16:49:47 +0200 Subject: [PATCH 1/2] fix(auth): remove first login notification on password reset --- app/Livewire/ForcePasswordReset.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index e6392497f..2463c68e4 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -47,14 +47,10 @@ public function submit() try { $this->rateLimit(10); $this->validate(); - $firstLogin = auth()->user()->created_at == auth()->user()->updated_at; auth()->user()->fill([ 'password' => Hash::make($this->password), 'force_password_reset' => false, ])->save(); - if ($firstLogin) { - send_internal_notification('First login for '.auth()->user()->email); - } return redirect()->route('dashboard'); } catch (\Throwable $e) { From ff149b8daa1275e8a18df87ba2f7b862e5ac1f53 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 16:56:00 +0200 Subject: [PATCH 2/2] 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. --- app/Jobs/StripeProcessJob.php | 15 +++-- .../Subscription/StripeProcessJobTest.php | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php index 3485ffe32..b031b9c7d 100644 --- a/app/Jobs/StripeProcessJob.php +++ b/app/Jobs/StripeProcessJob.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Str; +use Stripe\StripeClient; class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue { @@ -35,7 +36,7 @@ public function handle(): void $data = data_get($this->event, 'data.object'); switch ($type) { 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'); $charge = data_get($data, 'charge'); if ($charge) { @@ -94,12 +95,12 @@ public function handle(): void } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - throw new \RuntimeException("No subscription found for customer: {$customerId}"); + break; } if ($subscription->stripe_subscription_id) { try { - $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $stripe = new StripeClient(config('subscription.stripe_api_key')); $stripeSubscription = $stripe->subscriptions->retrieve( $subscription->stripe_subscription_id ); @@ -154,7 +155,7 @@ public function handle(): void $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { // 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'); if (! $team) { @@ -165,7 +166,7 @@ public function handle(): void // Verify payment status with Stripe API before sending failure notification if ($paymentIntentId) { try { - $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $stripe = new StripeClient(config('subscription.stripe_api_key')); $paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId); 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(); if (! $subscription) { // 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) { // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); @@ -334,7 +335,7 @@ public function handle(): void } } else { // 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; default: diff --git a/tests/Feature/Subscription/StripeProcessJobTest.php b/tests/Feature/Subscription/StripeProcessJobTest.php index 0a93f858c..a95e08338 100644 --- a/tests/Feature/Subscription/StripeProcessJobTest.php +++ b/tests/Feature/Subscription/StripeProcessJobTest.php @@ -2,10 +2,14 @@ use App\Jobs\ServerLimitCheckJob; use App\Jobs\StripeProcessJob; +use App\Jobs\SubscriptionInvoiceFailedJob; +use App\Jobs\VerifyStripeSubscriptionStatusJob; use App\Models\Subscription; use App\Models\Team; use App\Models\User; +use App\Notifications\Internal\GeneralNotification; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Queue; uses(RefreshDatabase::class); @@ -228,3 +232,65 @@ 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', + ], + ], + ]], + ]); +});