From 3500eaa46985f1be70b07f24888d0b3927b97615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 20 Apr 2026 13:31:49 +0100 Subject: [PATCH] refactor(real-estate): remove Pennant gating (#308) ## Summary - remove the `real-estate` Pennant flag definition, request gating, local auto-activation middleware, and frontend shared flag plumbing - make real-estate account creation and onboarding availability the default path for all users - update feature and browser tests to cover default availability instead of flag enable/disable behavior ## Testing - vendor/bin/pint --dirty --format agent - php artisan test --compact tests/Feature/RealEstateTest.php - php artisan test --compact tests/Feature/RealEstateAvailabilityTest.php - php artisan test --compact tests/Browser/RealEstateAccountTest.php - php artisan test --compact tests/Browser/OnboardingFlowTest.php - php artisan test --compact --filter=\"can create a real estate account linked to an existing loan\" tests/Browser/BankAccountsTest.php ## Notes - `npm run build` completed the client build, but the Sentry sourcemap upload step failed with an SSL/TLS error from the configured plugin - deploy follow-up: run `php artisan pennant:purge real-estate` to remove stale stored flag values --- .../Commands/Concerns/ResolvesFeatures.php | 2 +- .../ActivateDevelopmentFeatures.php | 25 ------ app/Http/Middleware/HandleInertiaRequests.php | 15 +--- .../Requests/Settings/StoreAccountRequest.php | 5 -- app/Providers/AppServiceProvider.php | 4 - bootstrap/app.php | 2 - .../accounts/create-account-dialog.tsx | 5 -- .../onboarding/step-account-types.tsx | 9 +- .../onboarding/step-create-account.tsx | 7 +- resources/js/types/index.d.ts | 1 - tests/Browser/BankAccountsTest.php | 3 - tests/Browser/OnboardingFlowTest.php | 8 +- tests/Browser/RealEstateAccountTest.php | 2 - tests/Feature/RealEstateAvailabilityTest.php | 44 ++++++++++ tests/Feature/RealEstateFeatureFlagTest.php | 88 ------------------- tests/Feature/RealEstateTest.php | 2 - 16 files changed, 51 insertions(+), 171 deletions(-) delete mode 100644 app/Http/Middleware/ActivateDevelopmentFeatures.php create mode 100644 tests/Feature/RealEstateAvailabilityTest.php delete mode 100644 tests/Feature/RealEstateFeatureFlagTest.php diff --git a/app/Console/Commands/Concerns/ResolvesFeatures.php b/app/Console/Commands/Concerns/ResolvesFeatures.php index 9c145dd6..63340a72 100644 --- a/app/Console/Commands/Concerns/ResolvesFeatures.php +++ b/app/Console/Commands/Concerns/ResolvesFeatures.php @@ -49,6 +49,6 @@ trait ResolvesFeatures private function getStringBasedFeatures(): array { - return ['real-estate']; + return []; } } diff --git a/app/Http/Middleware/ActivateDevelopmentFeatures.php b/app/Http/Middleware/ActivateDevelopmentFeatures.php deleted file mode 100644 index 46a51ac5..00000000 --- a/app/Http/Middleware/ActivateDevelopmentFeatures.php +++ /dev/null @@ -1,25 +0,0 @@ -isLocal() && $request->user()) { - Feature::for($request->user())->activate(['real-estate']); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index b4690a33..3bfb9ee8 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -7,7 +7,6 @@ use App\Services\CurrencyOptions; use Illuminate\Foundation\Inspiring; use Illuminate\Http\Request; use Inertia\Middleware; -use Laravel\Pennant\Feature; class HandleInertiaRequests extends Middleware { @@ -92,7 +91,7 @@ class HandleInertiaRequests extends Middleware 'includeLoansInNetWorthChart' => $user?->setting->include_loans_in_net_worth_chart ?? true, 'includeRealEstateInNetWorthChart' => $user?->setting->include_real_estate_in_net_worth_chart ?? true, 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', - 'features' => $this->resolveFeatureFlags($user), + 'features' => $this->resolveFeatureFlags(), 'accounts' => fn () => $user ? $user->accounts() ->with(['bank:id,name,logo', 'realEstateDetail:account_id,linked_loan_account_id']) ->orderBy('name') @@ -149,20 +148,10 @@ class HandleInertiaRequests extends Middleware /** * @return array */ - protected function resolveFeatureFlags(?object $user): array + protected function resolveFeatureFlags(): array { - if (! $user) { - return [ - 'cashflow' => true, - 'real-estate' => false, - ]; - } - - Feature::for($user)->load(['real-estate']); - return [ 'cashflow' => true, - 'real-estate' => Feature::for($user)->active('real-estate'), ]; } diff --git a/app/Http/Requests/Settings/StoreAccountRequest.php b/app/Http/Requests/Settings/StoreAccountRequest.php index 1735b56d..730d83d9 100644 --- a/app/Http/Requests/Settings/StoreAccountRequest.php +++ b/app/Http/Requests/Settings/StoreAccountRequest.php @@ -9,7 +9,6 @@ use App\Services\CurrencyOptions; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -use Laravel\Pennant\Feature; class StoreAccountRequest extends FormRequest { @@ -18,10 +17,6 @@ class StoreAccountRequest extends FormRequest */ public function authorize(): bool { - if ($this->input('type') === AccountType::RealEstate->value) { - return Feature::for($this->user())->active('real-estate'); - } - return true; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d06df284..11de14ff 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -10,14 +10,12 @@ use App\Http\Responses\RegisterResponse; use App\Listeners\ApplyAutomationRules; use App\Listeners\AssignTransactionToBudget; use App\Listeners\UnassignTransactionFromBudget; -use App\Models\User; use App\Services\Banking\EnableBankingProvider; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract; -use Laravel\Pennant\Feature; class AppServiceProvider extends ServiceProvider { @@ -49,7 +47,5 @@ class AppServiceProvider extends ServiceProvider RateLimiter::for('emails', function (object $job): Limit { return Limit::perSecond(30); }); - - Feature::define('real-estate', fn (User $user) => false); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 9d40d556..41a83c47 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,6 +1,5 @@ alias([ diff --git a/resources/js/components/accounts/create-account-dialog.tsx b/resources/js/components/accounts/create-account-dialog.tsx index 83cd5f3f..93ddf09d 100644 --- a/resources/js/components/accounts/create-account-dialog.tsx +++ b/resources/js/components/accounts/create-account-dialog.tsx @@ -30,12 +30,10 @@ export function CreateAccountDialog({ trigger?: React.ReactNode; }) { const { - features, auth, subscriptionsEnabled, accounts: sharedAccounts, } = usePage().props; - const realEstateEnabled = features['real-estate']; const isFreePlan = subscriptionsEnabled && !auth?.hasProPlan; const sharedAccountsList = useMemo( () => (sharedAccounts as Account[]) || [], @@ -305,9 +303,6 @@ export function CreateAccountDialog({ diff --git a/resources/js/components/onboarding/step-account-types.tsx b/resources/js/components/onboarding/step-account-types.tsx index 6f746a5f..11f35760 100644 --- a/resources/js/components/onboarding/step-account-types.tsx +++ b/resources/js/components/onboarding/step-account-types.tsx @@ -1,9 +1,7 @@ import { StepButton } from '@/components/onboarding/step-button'; import { StepHeader } from '@/components/onboarding/step-header'; -import { type SharedData } from '@/types'; import { accountIconByType } from '@/types/account'; import { __ } from '@/utils/i18n'; -import { usePage } from '@inertiajs/react'; import { Banknote } from 'lucide-react'; interface StepAccountTypesProps { @@ -56,11 +54,6 @@ const accountTypes = [ ]; export function StepAccountTypes({ onContinue }: StepAccountTypesProps) { - const { features } = usePage().props; - const visibleAccountTypes = features['real-estate'] - ? accountTypes - : accountTypes.filter((account) => account.type !== 'real_estate'); - return (
- {visibleAccountTypes.map((account) => { + {accountTypes.map((account) => { const Icon = accountIconByType(account.type); return ( diff --git a/resources/js/components/onboarding/step-create-account.tsx b/resources/js/components/onboarding/step-create-account.tsx index 0c7842c8..8cf60afb 100644 --- a/resources/js/components/onboarding/step-create-account.tsx +++ b/resources/js/components/onboarding/step-create-account.tsx @@ -71,9 +71,7 @@ export function StepCreateAccount({ onAccountCreated, onContinue, }: StepCreateAccountProps) { - const { pricing, subscriptionsEnabled, features, locale } = - usePage().props; - const realEstateEnabled = features['real-estate']; + const { pricing, subscriptionsEnabled, locale } = usePage().props; const [mode, setMode] = useState('select'); const [selectedMode, setSelectedMode] = useState<'manual' | 'connected'>( 'connected', @@ -543,9 +541,6 @@ export function StepCreateAccount({ > onboarded()->create(); - Feature::for($user)->activate('real-estate'); - $loanBank = Bank::factory()->create(['name' => 'Linked Mortgage Bank', 'logo' => null]); $loanAccount = Account::factory()->create([ diff --git a/tests/Browser/OnboardingFlowTest.php b/tests/Browser/OnboardingFlowTest.php index 0b670bd3..95e9d0c1 100644 --- a/tests/Browser/OnboardingFlowTest.php +++ b/tests/Browser/OnboardingFlowTest.php @@ -87,13 +87,11 @@ it('navigates from welcome to account types', function () { ->assertNoJavascriptErrors(); }); -it('shows real estate on onboarding account types when feature is enabled', function () { +it('shows real estate on onboarding account types by default', function () { $user = User::factory()->create([ 'onboarded_at' => null, ]); - Feature::for($user)->activate('real-estate'); - $this->actingAs($user); $page = visit('/onboarding'); @@ -229,13 +227,11 @@ it('shows add another account form without first account restriction', function ->assertNoJavascriptErrors(); }); -it('creates a real estate account during onboarding when feature is enabled', function () { +it('creates a real estate account during onboarding by default', function () { $user = User::factory()->create([ 'onboarded_at' => null, ]); - Feature::for($user)->activate('real-estate'); - $this->actingAs($user); $page = visit('/onboarding'); diff --git a/tests/Browser/RealEstateAccountTest.php b/tests/Browser/RealEstateAccountTest.php index a81f237a..54000a95 100644 --- a/tests/Browser/RealEstateAccountTest.php +++ b/tests/Browser/RealEstateAccountTest.php @@ -6,13 +6,11 @@ use App\Models\AccountBalance; use App\Models\RealEstateDetail; use App\Models\User; use Carbon\Carbon; -use Laravel\Pennant\Feature; use function Pest\Laravel\actingAs; beforeEach(function () { $this->user = User::factory()->onboarded()->create(); - Feature::for($this->user)->activate('real-estate'); actingAs($this->user); }); diff --git a/tests/Feature/RealEstateAvailabilityTest.php b/tests/Feature/RealEstateAvailabilityTest.php new file mode 100644 index 00000000..ee458b79 --- /dev/null +++ b/tests/Feature/RealEstateAvailabilityTest.php @@ -0,0 +1,44 @@ +onboarded()->create(); + + $response = $this->actingAs($user)->post(route('accounts.store'), [ + 'name' => 'My Property', + 'currency_code' => 'EUR', + 'type' => AccountType::RealEstate->value, + 'property_type' => PropertyType::Residential->value, + ]); + + $response->assertSessionHasNoErrors()->assertRedirect(); +}); + +test('users can still create non-real-estate accounts', function () { + $user = User::factory()->onboarded()->create(); + $bank = Bank::factory()->create(); + + $response = $this->actingAs($user)->post(route('accounts.store'), [ + 'name' => 'My Savings', + 'bank_id' => $bank->id, + 'currency_code' => 'EUR', + 'type' => AccountType::Savings->value, + ]); + + $response->assertSessionHasNoErrors()->assertRedirect(); +}); + +test('real-estate flag is not shared with frontend anymore', function () { + $user = User::factory()->onboarded()->create(); + + $response = $this->actingAs($user)->get('/dashboard'); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->missing('features.real-estate') + ); +}); diff --git a/tests/Feature/RealEstateFeatureFlagTest.php b/tests/Feature/RealEstateFeatureFlagTest.php deleted file mode 100644 index 1d80af7c..00000000 --- a/tests/Feature/RealEstateFeatureFlagTest.php +++ /dev/null @@ -1,88 +0,0 @@ -onboarded()->create(); - - Feature::for($user)->deactivate('real-estate'); - - $response = $this->actingAs($user)->post(route('accounts.store'), [ - 'name' => 'My Property', - 'currency_code' => 'EUR', - 'type' => AccountType::RealEstate->value, - 'property_type' => PropertyType::Residential->value, - ]); - - $response->assertForbidden(); -}); - -test('users with real-estate feature can create real estate accounts', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->activate('real-estate'); - - $response = $this->actingAs($user)->post(route('accounts.store'), [ - 'name' => 'My Property', - 'currency_code' => 'EUR', - 'type' => AccountType::RealEstate->value, - 'property_type' => PropertyType::Residential->value, - ]); - - $response->assertSessionHasNoErrors()->assertRedirect(); -}); - -test('users without real-estate feature can still create non-real-estate accounts', function () { - $user = User::factory()->onboarded()->create(); - $bank = Bank::factory()->create(); - - Feature::for($user)->deactivate('real-estate'); - - $response = $this->actingAs($user)->post(route('accounts.store'), [ - 'name' => 'My Savings', - 'bank_id' => $bank->id, - 'currency_code' => 'EUR', - 'type' => AccountType::Savings->value, - ]); - - $response->assertSessionHasNoErrors()->assertRedirect(); -}); - -test('real-estate feature flag is shared with frontend when enabled', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->activate('real-estate'); - - $response = $this->actingAs($user)->get('/dashboard'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.real-estate', true) - ); -}); - -test('real-estate feature flag is shared with frontend when disabled', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->deactivate('real-estate'); - - $response = $this->actingAs($user)->get('/dashboard'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.real-estate', false) - ); -}); - -test('guests see real-estate feature as false', function () { - $response = $this->get('/'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.real-estate', false) - ); -}); diff --git a/tests/Feature/RealEstateTest.php b/tests/Feature/RealEstateTest.php index b42bea26..357c1951 100644 --- a/tests/Feature/RealEstateTest.php +++ b/tests/Feature/RealEstateTest.php @@ -7,7 +7,6 @@ use App\Models\Account; use App\Models\Bank; use App\Models\RealEstateDetail; use App\Models\User; -use Laravel\Pennant\Feature; use function Pest\Laravel\actingAs; use function Pest\Laravel\assertDatabaseHas; @@ -15,7 +14,6 @@ use function Pest\Laravel\assertDatabaseHas; beforeEach(function () { $this->user = User::factory()->onboarded()->create(); $this->bank = Bank::factory()->create(); - Feature::for($this->user)->activate('real-estate'); }); // -------------------------------------------------------------------