feat(billing): apply Stripe tax rates to subscriptions (#325)

## Summary
Every subscription now gets Stripe tax rates attached automatically via
Cashier.

## Changes
- `config/subscriptions.php`: new `tax_rates` array, env
`STRIPE_TAX_RATES` (comma-separated), default
`txr_1TPfzrLRCmKA3oWMNWmkQeq2`
- `app/Models/User.php`: `taxRates()` reads from config — Cashier picks
it up automatically on `newSubscription()` checkout + subscription
creation
- `tests/Feature/SubscriptionTest.php`: 2 tests

## Applies to
- New checkout sessions (`SubscriptionController::checkout`)
- New subscriptions created via Cashier

## Existing subscriptions
Not updated automatically. To sync:
```php
$user->subscription('default')->syncTaxRates();
```

## Notes
Using hard-coded tax rate IDs (not Stripe Tax auto-calc). Switch to
`Cashier::calculateTaxes()` later if desired.
This commit is contained in:
Víctor Falcón 2026-04-24 13:07:58 +01:00 committed by GitHub
parent b399aaaa0d
commit 74cbdd42ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 34 additions and 0 deletions

View File

@ -165,6 +165,16 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma
return $this->subscribed('default');
}
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<int, string>
*/
public function taxRates(): array
{
return config('subscriptions.tax_rates', []);
}
public function isDemoAccount(): bool
{
return $this->email === config('app.demo.email');

View File

@ -131,6 +131,18 @@ return [
|
*/
/*
|--------------------------------------------------------------------------
| Tax Rates
|--------------------------------------------------------------------------
|
| Stripe tax rate IDs applied to every subscription created via Cashier.
| Configure tax rates in your Stripe dashboard and reference them here.
|
*/
'tax_rates' => array_values(array_filter(explode(',', (string) env('STRIPE_TAX_RATES', 'txr_1TPfzrLRCmKA3oWMNWmkQeq2')))),
'promo' => [
'enabled' => env('PROMO_ENABLED', true),
'code' => 'FOUNDER',

View File

@ -272,6 +272,18 @@ test('paywall shows canUseFreePlan false when user has a bank connection', funct
);
});
test('taxRates returns configured stripe tax rate ids', function () {
config(['subscriptions.tax_rates' => ['txr_test_1', 'txr_test_2']]);
$user = User::factory()->create();
expect($user->taxRates())->toBe(['txr_test_1', 'txr_test_2']);
});
test('taxRates config defaults to the production tax rate id', function () {
expect(config('subscriptions.tax_rates'))->toContain('txr_1TPfzrLRCmKA3oWMNWmkQeq2');
});
test('billing portal creates stripe customer when user has no stripe id', function () {
$user = Mockery::mock(User::class)->shouldIgnoreMissing();
$user->shouldReceive('isDemoAccount')->andReturn(false);