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:
parent
75736f3e59
commit
3500eaa469
|
|
@ -49,6 +49,6 @@ trait ResolvesFeatures
|
|||
|
||||
private function getStringBasedFeatures(): array
|
||||
{
|
||||
return ['real-estate'];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export interface NavDivider {
|
|||
|
||||
export interface Features {
|
||||
cashflow: boolean;
|
||||
'real-estate': boolean;
|
||||
}
|
||||
|
||||
export interface Flash {
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
);
|
||||
});
|
||||
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue