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
This commit is contained in:
Víctor Falcón 2026-04-20 13:31:49 +01:00 committed by GitHub
parent 75736f3e59
commit 3500eaa469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 51 additions and 171 deletions

View File

@ -49,6 +49,6 @@ trait ResolvesFeatures
private function getStringBasedFeatures(): array
{
return ['real-estate'];
return [];
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Laravel\Pennant\Feature;
use Symfony\Component\HttpFoundation\Response;
class ActivateDevelopmentFeatures
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (app()->isLocal() && $request->user()) {
Feature::for($request->user())->activate(['real-estate']);
}
return $next($request);
}
}

View File

@ -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<string, bool>
*/
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'),
];
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -1,6 +1,5 @@
<?php
use App\Http\Middleware\ActivateDevelopmentFeatures;
use App\Http\Middleware\BlockDemoAccountActions;
use App\Http\Middleware\EnsureOnboardingComplete;
use App\Http\Middleware\EnsureUserIsSubscribed;
@ -38,7 +37,6 @@ return Application::configure(basePath: dirname(__DIR__))
HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class,
BlockDemoAccountActions::class.':auto',
ActivateDevelopmentFeatures::class,
]);
$middleware->alias([

View File

@ -30,12 +30,10 @@ export function CreateAccountDialog({
trigger?: React.ReactNode;
}) {
const {
features,
auth,
subscriptionsEnabled,
accounts: sharedAccounts,
} = usePage<SharedData>().props;
const realEstateEnabled = features['real-estate'];
const isFreePlan = subscriptionsEnabled && !auth?.hasProPlan;
const sharedAccountsList = useMemo(
() => (sharedAccounts as Account[]) || [],
@ -305,9 +303,6 @@ export function CreateAccountDialog({
<AccountForm
onChange={handleFormChange}
availableLoanAccounts={availableLoanAccounts}
hiddenAccountTypes={
realEstateEnabled ? [] : ['real_estate']
}
usePrimaryCurrenciesOnly={isFirstAccount}
/>

View File

@ -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<SharedData>().props;
const visibleAccountTypes = features['real-estate']
? accountTypes
: accountTypes.filter((account) => account.type !== 'real_estate');
return (
<div className="flex animate-in flex-col items-center duration-500 fade-in slide-in-from-bottom-4">
<StepHeader
@ -73,7 +66,7 @@ export function StepAccountTypes({ onContinue }: StepAccountTypesProps) {
/>
<div className="grid w-full max-w-2xl gap-3 sm:grid-cols-2">
{visibleAccountTypes.map((account) => {
{accountTypes.map((account) => {
const Icon = accountIconByType(account.type);
return (

View File

@ -71,9 +71,7 @@ export function StepCreateAccount({
onAccountCreated,
onContinue,
}: StepCreateAccountProps) {
const { pricing, subscriptionsEnabled, features, locale } =
usePage<SharedData>().props;
const realEstateEnabled = features['real-estate'];
const { pricing, subscriptionsEnabled, locale } = usePage<SharedData>().props;
const [mode, setMode] = useState<AccountMode>('select');
const [selectedMode, setSelectedMode] = useState<'manual' | 'connected'>(
'connected',
@ -543,9 +541,6 @@ export function StepCreateAccount({
>
<AccountForm
onChange={handleFormChange}
hiddenAccountTypes={
realEstateEnabled ? [] : ['real_estate']
}
usePrimaryCurrenciesOnly={
isFirstAccount &&
existingAccounts.length === 0 &&

View File

@ -41,7 +41,6 @@ export interface NavDivider {
export interface Features {
cashflow: boolean;
'real-estate': boolean;
}
export interface Flash {

View File

@ -7,7 +7,6 @@ use App\Models\Bank;
use App\Models\BankingConnection;
use App\Models\RealEstateDetail;
use App\Models\User;
use Laravel\Pennant\Feature;
use function Pest\Laravel\actingAs;
@ -250,8 +249,6 @@ it('can create the remaining manual account types', function (string $typeLabel,
it('can create a real estate account linked to an existing loan', function () {
$user = User::factory()->onboarded()->create();
Feature::for($user)->activate('real-estate');
$loanBank = Bank::factory()->create(['name' => 'Linked Mortgage Bank', 'logo' => null]);
$loanAccount = Account::factory()->create([

View File

@ -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');

View File

@ -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);
});

View File

@ -0,0 +1,44 @@
<?php
use App\Enums\AccountType;
use App\Enums\PropertyType;
use App\Models\Bank;
use App\Models\User;
test('users can create real estate accounts by default', function () {
$user = User::factory()->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')
);
});

View File

@ -1,88 +0,0 @@
<?php
use App\Enums\AccountType;
use App\Enums\PropertyType;
use App\Models\Bank;
use App\Models\User;
use Laravel\Pennant\Feature;
test('users without real-estate feature cannot create real estate accounts', function () {
$user = User::factory()->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)
);
});

View File

@ -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');
});
// -------------------------------------------------------------------