Remove budgets feature flag (#108)

## Summary
- Remove the `budgets` Pennant feature flag — budgets is now enabled for
all users
- Delete `EnsureBudgetsFeature` middleware and its route guard
- Remove `budgets` from shared Inertia features and the `Features`
TypeScript interface
- Remove all `Feature::for($user)->activate('budgets')` calls from tests
- Delete `BudgetFeatureFlagTest` and feature-disabled test cases from
`BudgetsFeatureNavigationTest`
This commit is contained in:
Víctor Falcón 2026-02-12 09:58:01 +01:00 committed by GitHub
parent db7b6e4da7
commit d1d1be7586
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 7 additions and 228 deletions

View File

@ -49,6 +49,6 @@ trait ResolvesFeatures
private function getStringBasedFeatures(): array
{
return ['budgets', 'plaintext-transactions', 'open-banking', 'account-mapping'];
return ['plaintext-transactions', 'open-banking', 'account-mapping'];
}
}

View File

@ -21,7 +21,6 @@ use App\Services\Demo\DemoLabelsProvider;
use App\Services\Demo\DemoTransactionsProvider;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Laravel\Pennant\Feature;
class ResetDemoAccountCommand extends Command
{
@ -77,8 +76,6 @@ class ResetDemoAccountCommand extends Command
$this->createBudgets($user);
$this->enableBudgetsFeature($user);
$this->assignTransactionsToBudgets($user);
$this->createSubscription($user);
@ -510,11 +507,6 @@ class ResetDemoAccountCommand extends Command
}
}
private function enableBudgetsFeature(User $user): void
{
Feature::for($user)->activate('budgets');
}
private function assignTransactionsToBudgets(User $user): void
{
$transactions = $user->transactions()->get();

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Laravel\Pennant\Feature;
use Symfony\Component\HttpFoundation\Response;
class EnsureBudgetsFeature
{
/**
* Handle an incoming request.
*
* Returns 404 if user doesn't have budgets feature enabled.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (! $user || ! Feature::for($user)->active('budgets')) {
abort(404);
}
return $next($request);
}
}

View File

@ -69,7 +69,6 @@ class HandleInertiaRequests extends Middleware
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'features' => [
'cashflow' => true,
'budgets' => $user ? Feature::for($user)->active('budgets') : false,
'plaintext-transactions' => $user ? Feature::for($user)->active('plaintext-transactions') : false,
'open-banking' => $user ? Feature::for($user)->active('open-banking') : false,
'account-mapping' => $user ? Feature::for($user)->active('account-mapping') : false,

View File

@ -6,7 +6,6 @@ use App\Events\TransactionCreated;
use App\Events\TransactionUpdated;
use App\Services\BudgetTransactionService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Laravel\Pennant\Feature;
class AssignTransactionToBudget implements ShouldQueue
{
@ -15,9 +14,8 @@ class AssignTransactionToBudget implements ShouldQueue
public function handle(TransactionCreated|TransactionUpdated $event): void
{
$transaction = $event->transaction;
$user = $transaction->user;
if (! $user || ! Feature::for($user)->active('budgets')) {
if (! $transaction->user) {
return;
}

View File

@ -5,7 +5,6 @@ namespace App\Listeners;
use App\Events\TransactionDeleted;
use App\Services\BudgetTransactionService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Laravel\Pennant\Feature;
class UnassignTransactionFromBudget implements ShouldQueue
{
@ -14,9 +13,8 @@ class UnassignTransactionFromBudget implements ShouldQueue
public function handle(TransactionDeleted $event): void
{
$transaction = $event->transaction;
$user = $transaction->user;
if (! $user || ! Feature::for($user)->active('budgets')) {
if (! $transaction->user) {
return;
}

View File

@ -48,7 +48,6 @@ class AppServiceProvider extends ServiceProvider
return Limit::perSecond(30);
});
Feature::define('budgets', fn (User $user) => true);
Feature::define('plaintext-transactions', fn (User $user) => false);
Feature::define('open-banking', fn (User $user) => false);
Feature::define('account-mapping', fn (User $user) => false);

View File

@ -1,6 +1,5 @@
<?php
use App\Http\Middleware\EnsureBudgetsFeature;
use App\Http\Middleware\EnsureOpenBankingFeature;
use App\Http\Middleware\EnsureUserIsSubscribed;
use App\Http\Middleware\HandleAppearance;
@ -42,7 +41,6 @@ return Application::configure(basePath: dirname(__DIR__))
'subscribed' => EnsureUserIsSubscribed::class,
'onboarded' => \App\Http\Middleware\EnsureOnboardingComplete::class,
'block-demo' => \App\Http\Middleware\BlockDemoAccountActions::class,
'budgets' => EnsureBudgetsFeature::class,
'open-banking' => EnsureOpenBankingFeature::class,
]);
})

View File

@ -44,16 +44,13 @@ export function getMainNavItems(features: Features): NavItem[] {
href: transactionsIndex(),
icon: Receipt,
},
);
if (features.budgets) {
items.push({
{
type: 'nav-item',
title: 'Budgets',
href: budgetsIndex(),
icon: PiggyBank,
});
}
},
);
return items;
}

View File

@ -40,7 +40,6 @@ export interface NavDivider {
export interface Features {
cashflow: boolean;
budgets: boolean;
'plaintext-transactions': boolean;
'open-banking': boolean;
'account-mapping': boolean;

View File

@ -72,7 +72,7 @@ Route::middleware(['auth', 'verified', 'onboarded', 'subscribed', 'open-banking'
Route::post('connections/{connection}/map-accounts', [AccountMappingController::class, 'store'])->name('open-banking.map-accounts.store');
});
Route::middleware(['auth', 'verified', 'onboarded', 'subscribed', 'budgets'])->group(function () {
Route::middleware(['auth', 'verified', 'onboarded', 'subscribed'])->group(function () {
Route::get('budgets', [BudgetController::class, 'index'])->name('budgets.index');
Route::post('budgets', [BudgetController::class, 'store'])->name('budgets.store');
Route::get('budgets/{budget}', [BudgetController::class, 'show'])->name('budgets.show');

View File

@ -3,11 +3,9 @@
use App\Models\Budget;
use App\Models\Category;
use App\Models\User;
use Laravel\Pennant\Feature;
test('user can create a budget with category', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create([
'user_id' => $user->id,
@ -58,7 +56,6 @@ test('user can create a budget with category', function () {
test('user can update budget name', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -96,7 +93,6 @@ test('user can update budget name', function () {
test('user can delete a budget', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -133,7 +129,6 @@ test('user can delete a budget', function () {
test('budget creation validates required fields', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$page = $this->actingAs($user)->visit('/budgets');
$page->wait(2); // Wait for page to fully load
@ -151,7 +146,6 @@ test('budget creation validates required fields', function () {
test('budget shows current period information', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -168,7 +162,6 @@ test('budget shows current period information', function () {
test('user can navigate back to budgets list from budget detail', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([

View File

@ -3,21 +3,9 @@
use App\Models\Budget;
use App\Models\Category;
use App\Models\User;
use Laravel\Pennant\Feature;
test('budgets menu item hidden when feature disabled', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->deactivate('budgets');
$page = $this->actingAs($user)->visit('/dashboard');
$page->assertDontSee('Budgets')
->assertNoJavascriptErrors();
});
test('budgets menu item visible when feature enabled', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$page = $this->actingAs($user)->visit('/dashboard');
@ -25,18 +13,8 @@ test('budgets menu item visible when feature enabled', function () {
->assertNoJavascriptErrors();
});
test('user cannot access budgets page when feature disabled', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->get('/budgets');
$response->assertNotFound();
});
test('user can navigate to budgets index page', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$page = $this->actingAs($user)->visit('/budgets');
@ -47,7 +25,6 @@ test('user can navigate to budgets index page', function () {
test('user can view empty budgets list', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$page = $this->actingAs($user)->visit('/budgets');
@ -57,7 +34,6 @@ test('user can view empty budgets list', function () {
test('user can view budgets list with existing budgets', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -75,7 +51,6 @@ test('user can view budgets list with existing budgets', function () {
test('user can open create budget dialog', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
@ -90,7 +65,6 @@ test('user can open create budget dialog', function () {
test('user can view a specific budget', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -111,7 +85,6 @@ test('user can view a specific budget', function () {
test('user can open edit budget dialog', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -131,7 +104,6 @@ test('user can open edit budget dialog', function () {
test('user can open delete budget dialog', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -154,8 +126,6 @@ test('user can open delete budget dialog', function () {
test('user cannot access another users budget', function () {
$user1 = User::factory()->create(['onboarded_at' => now()]);
$user2 = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user1)->activate('budgets');
Feature::for($user2)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user1->id]);
$budget = Budget::factory()->create([
@ -170,7 +140,6 @@ test('user cannot access another users budget', function () {
test('budgets navigation works from sidebar', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$page = $this->actingAs($user)->visit('/dashboard');
@ -180,7 +149,6 @@ test('budgets navigation works from sidebar', function () {
test('budgets page shows correct feature flag state', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$page = $this->actingAs($user)->visit('/budgets');

View File

@ -1,114 +0,0 @@
<?php
use App\Models\Budget;
use App\Models\Category;
use App\Models\User;
use Laravel\Pennant\Feature;
test('users without budgets feature get 404 on budgets index', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->get('/budgets');
$response->assertNotFound();
});
test('users with budgets feature can access budgets index', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$response = $this->actingAs($user)->get('/budgets');
$response->assertOk();
});
test('users without budgets feature get 404 on budgets show', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
$budget = Budget::factory()->create(['user_id' => $user->id]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->get("/budgets/{$budget->id}");
$response->assertNotFound();
});
test('users without budgets feature get 404 on budgets store', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
$category = Category::factory()->create(['user_id' => $user->id]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->post('/budgets', [
'name' => 'Test Budget',
'period_type' => 'monthly',
'period_start_day' => 1,
'category_id' => $category->id,
'rollover_type' => 'reset',
'allocated_amount' => 100000,
]);
$response->assertNotFound();
});
test('users without budgets feature get 404 on budgets update', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
$budget = Budget::factory()->create(['user_id' => $user->id]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->patch("/budgets/{$budget->id}", [
'name' => 'Updated Budget',
]);
$response->assertNotFound();
});
test('users without budgets feature get 404 on budgets destroy', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
$budget = Budget::factory()->create(['user_id' => $user->id]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->delete("/budgets/{$budget->id}");
$response->assertNotFound();
});
test('budgets feature flag is shared with frontend when enabled', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$response = $this->actingAs($user)->get('/dashboard');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->where('features.budgets', true)
);
});
test('budgets feature flag is shared with frontend when disabled', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->deactivate('budgets');
$response = $this->actingAs($user)->get('/dashboard');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->where('features.budgets', false)
);
});
test('guests see budgets feature as false', function () {
$response = $this->get('/');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->where('features.budgets', false)
);
});

View File

@ -7,13 +7,11 @@ use App\Models\Label;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Support\Facades\Queue;
use Laravel\Pennant\Feature;
use function Pest\Laravel\assertDatabaseHas;
beforeEach(function () {
$this->user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($this->user)->activate('budgets');
});
test('budget creation dispatches the historical assignment job', function () {

View File

@ -5,7 +5,6 @@ use App\Models\Budget;
use App\Models\BudgetPeriod;
use App\Models\User;
use Carbon\Carbon;
use Laravel\Pennant\Feature;
test('getCurrentPeriod finds period on its last day regardless of time', function () {
Carbon::setTestNow(Carbon::parse('2026-01-31 15:30:00'));
@ -62,7 +61,6 @@ test('budget index loads current period on last day of period', function () {
Carbon::setTestNow(Carbon::parse('2026-01-31 18:00:00'));
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->create([
'user_id' => $user->id,
@ -93,7 +91,6 @@ test('budget show finds current period on last day of period', function () {
Carbon::setTestNow(Carbon::parse('2026-01-31 20:00:00'));
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->create([
'user_id' => $user->id,

View File

@ -3,11 +3,9 @@
use App\Models\Budget;
use App\Models\Category;
use App\Models\User;
use Laravel\Pennant\Feature;
test('user can create a budget', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
@ -36,7 +34,6 @@ test('user can create a budget', function () {
test('user can view their budgets', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->create(['user_id' => $user->id]);
@ -51,7 +48,6 @@ test('user can view their budgets', function () {
test('user can view a specific budget', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);
$budget = Budget::factory()->create([
@ -72,8 +68,6 @@ test('user can view a specific budget', function () {
test('user cannot view another users budget', function () {
$user1 = User::factory()->create(['onboarded_at' => now()]);
$user2 = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user1)->activate('budgets');
Feature::for($user2)->activate('budgets');
$budget = Budget::factory()->create(['user_id' => $user1->id]);
@ -84,7 +78,6 @@ test('user cannot view another users budget', function () {
test('user can update their budget', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->create(['user_id' => $user->id]);
@ -102,7 +95,6 @@ test('user can update their budget', function () {
test('user can delete their budget', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->create(['user_id' => $user->id]);
@ -117,7 +109,6 @@ test('user can delete their budget', function () {
test('budget show returns previous period when it exists', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->monthly()->create([
'user_id' => $user->id,
@ -153,7 +144,6 @@ test('budget show returns previous period when it exists', function () {
test('budget show returns null previous period when it is the first period', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$budget = Budget::factory()->monthly()->create([
'user_id' => $user->id,
@ -180,7 +170,6 @@ test('budget show returns null previous period when it is the first period', fun
test('budget period is automatically generated', function () {
$user = User::factory()->create(['onboarded_at' => now()]);
Feature::for($user)->activate('budgets');
$category = Category::factory()->create(['user_id' => $user->id]);

View File

@ -8,7 +8,6 @@ use App\Models\Category;
use App\Models\Label;
use App\Models\Transaction;
use App\Models\User;
use Laravel\Pennant\Feature;
use function Pest\Laravel\actingAs;
@ -613,8 +612,6 @@ test('updating transaction labels via API works correctly', function () {
test('when budget with label exists, updating transaction with that label assigns it to budget', function () {
$user = User::factory()->onboarded()->create();
Feature::for($user)->activate('budgets');
$account = Account::factory()->create(['user_id' => $user->id]);
$label = Label::factory()->create(['user_id' => $user->id, 'name' => 'Work']);