fix(billing): create Stripe customer before redirecting to billing portal (#206)

## Summary

- Users without a Stripe customer ID (`stripe_id = null`) would hit an
`InvalidCustomer` exception when visiting the billing portal
- Added a `hasStripeId()` check before calling
`redirectToBillingPortal()`, creating the Stripe customer on-the-fly if
needed
- Added two tests covering both branches (with and without an existing
Stripe customer ID)
This commit is contained in:
Víctor Falcón 2026-03-05 11:58:04 +00:00 committed by GitHub
parent ac1476eeff
commit e8bc5fd786
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 1 deletions

View File

@ -136,6 +136,12 @@ class SubscriptionController extends Controller
->withErrors(['demo' => 'Billing management is not available on the demo account.']);
}
return $request->user()->redirectToBillingPortal(route('settings.billing'));
$user = $request->user();
if (! $user->hasStripeId()) {
$user->createAsStripeCustomer();
}
return $user->redirectToBillingPortal(route('settings.billing'));
}
}

View File

@ -1,11 +1,13 @@
<?php
use App\Http\Middleware\HandleInertiaRequests;
use App\Models\Account;
use App\Models\AccountBalance;
use App\Models\BankingConnection;
use App\Models\Category;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Laravel\Pennant\Feature;
beforeEach(function () {
@ -277,3 +279,35 @@ test('paywall shows canUseFreePlan false when open banking is not active', funct
->where('canUseFreePlan', false)
);
});
test('billing portal creates stripe customer when user has no stripe id', function () {
$user = Mockery::mock(User::class)->shouldIgnoreMissing();
$user->shouldReceive('isDemoAccount')->andReturn(false);
$user->shouldReceive('hasStripeId')->once()->andReturn(false);
$user->shouldReceive('createAsStripeCustomer')->once();
$user->shouldReceive('redirectToBillingPortal')
->with(route('settings.billing'))
->once()
->andReturn(new RedirectResponse(route('settings.billing')));
$this->withoutMiddleware(HandleInertiaRequests::class);
$this->actingAs($user);
$this->get(route('settings.billing.portal'))->assertRedirect();
});
test('billing portal skips stripe customer creation when user already has a stripe id', function () {
$user = Mockery::mock(User::class)->shouldIgnoreMissing();
$user->shouldReceive('isDemoAccount')->andReturn(false);
$user->shouldReceive('hasStripeId')->once()->andReturn(true);
$user->shouldNotReceive('createAsStripeCustomer');
$user->shouldReceive('redirectToBillingPortal')
->with(route('settings.billing'))
->once()
->andReturn(new RedirectResponse(route('settings.billing')));
$this->withoutMiddleware(HandleInertiaRequests::class);
$this->actingAs($user);
$this->get(route('settings.billing.portal'))->assertRedirect();
});