trypost/tests/Feature/Models/AccountModelTest.php
Paulo Castellano f72a97f676 refactor: explicit Pennant cache reset on plan_id change
Replaces the implicit Account::booted() observer with an explicit
Account::forgetPlanFeatureCache() method called from each plan_id
mutation site (StripeEventListener x3, BillingController x2). Self-hosted
installs naturally never reach any of these callsites — Stripe webhooks
do not fire and the billing controllers redirect to /calendar before any
plan mutation happens — so the Pennant flush is now guaranteed to be a
cloud-only operation.

Adds integration coverage proving the full chain webhook -> plan_id
update -> Pennant flush -> next Feature::value resolves against the new
plan limit.
2026-05-07 10:59:48 -03:00

62 lines
2.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Features\MemberLimit;
use App\Features\MonthlyCreditsLimit;
use App\Features\SocialAccountLimit;
use App\Features\WorkspaceLimit;
use App\Models\Account;
use App\Models\Plan;
use Illuminate\Support\Facades\DB;
use Laravel\Pennant\Feature;
test('forgetPlanFeatureCache drops the cached plan-scoped features', function () {
$starter = Plan::where('slug', 'starter')->first();
$plus = Plan::where('slug', 'plus')->first();
$account = Account::factory()->create(['plan_id' => $starter->id]);
// Prime the Pennant cache against the starter plan.
Feature::for($account)->value(WorkspaceLimit::class);
Feature::for($account)->value(SocialAccountLimit::class);
Feature::for($account)->value(MemberLimit::class);
Feature::for($account)->value(MonthlyCreditsLimit::class);
expect(DB::table('features')->where('scope', 'account|'.$account->id)->count())
->toBe(4);
// Move the account to a plan with different limits and forget the cache.
$account->update(['plan_id' => $plus->id]);
$account->forgetPlanFeatureCache();
$account->load('plan');
expect(DB::table('features')->where('scope', 'account|'.$account->id)->count())
->toBe(0);
expect(Feature::for($account)->value(WorkspaceLimit::class))->toBe($plus->workspace_limit);
expect(Feature::for($account)->value(SocialAccountLimit::class))->toBe($plus->social_account_limit);
expect(Feature::for($account)->value(MemberLimit::class))->toBe($plus->member_limit);
expect(Feature::for($account)->value(MonthlyCreditsLimit::class))->toBe($plus->monthly_credits_limit);
});
test('updating non-plan fields does not flush the pennant cache', function () {
$plan = Plan::where('slug', 'plus')->first();
$account = Account::factory()->create(['plan_id' => $plan->id]);
Feature::for($account)->value(WorkspaceLimit::class);
$cachedRow = DB::table('features')
->where('scope', 'account|'.$account->id)
->first();
expect($cachedRow)->not->toBeNull();
$account->update(['name' => 'Updated Name']);
$stillCachedRow = DB::table('features')
->where('scope', 'account|'.$account->id)
->first();
expect($stillCachedRow->id)->toBe($cachedRow->id);
});