diff --git a/app/Console/Commands/Concerns/ResolvesFeatures.php b/app/Console/Commands/Concerns/ResolvesFeatures.php index c6c5b592..9c145dd6 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 ['open-banking', 'real-estate']; + return ['real-estate']; } } diff --git a/app/Http/Controllers/OpenBanking/AuthorizationController.php b/app/Http/Controllers/OpenBanking/AuthorizationController.php index 585343b4..6733ba72 100644 --- a/app/Http/Controllers/OpenBanking/AuthorizationController.php +++ b/app/Http/Controllers/OpenBanking/AuthorizationController.php @@ -6,6 +6,7 @@ use App\Contracts\BankingProviderInterface; use App\Enums\BankingConnectionStatus; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; +use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; use App\Http\Requests\OpenBanking\StartAuthorizationRequest; use App\Jobs\SyncBankingConnectionJob; use App\Models\BankingConnection; @@ -17,6 +18,7 @@ use Illuminate\Support\Facades\Log; class AuthorizationController extends Controller { use CreatesAccountsFromPending; + use HandlesSubscriptionGate; /** * Start the bank authorization flow. @@ -25,8 +27,8 @@ class AuthorizationController extends Controller { $user = auth()->user(); - if (config('subscriptions.enabled') && ! $user->hasProPlan()) { - return response()->json(['redirect' => route('subscribe')], 402); + if ($this->shouldBlockOpenBankingAccess($user)) { + return $this->subscribeJsonResponse(); } $validated = $request->validated(); @@ -63,6 +65,10 @@ class AuthorizationController extends Controller abort(403); } + if ($this->shouldBlockOpenBankingAccess($request->user())) { + return $this->subscribeJsonResponse(); + } + if (! $connection->isEnableBanking()) { return response()->json(['error' => 'Only EnableBanking connections can be re-authorized.'], 422); } diff --git a/app/Http/Controllers/OpenBanking/BinanceController.php b/app/Http/Controllers/OpenBanking/BinanceController.php index ea073385..7982f07f 100644 --- a/app/Http/Controllers/OpenBanking/BinanceController.php +++ b/app/Http/Controllers/OpenBanking/BinanceController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; +use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; use App\Http\Requests\OpenBanking\ConnectBinanceRequest; use App\Jobs\SyncBankingConnectionJob; use App\Models\Bank; @@ -15,6 +16,7 @@ use Illuminate\Support\Facades\Log; class BinanceController extends Controller { use CreatesAccountsFromPending; + use HandlesSubscriptionGate; /** * Validate Binance API credentials and create a connection. @@ -24,6 +26,10 @@ class BinanceController extends Controller $validated = $request->validated(); $user = auth()->user(); + if ($this->shouldBlockOpenBankingAccess($user)) { + return $this->subscribeJsonResponse(); + } + $client = new BinanceClient($validated['api_key'], $validated['api_secret']); try { diff --git a/app/Http/Controllers/OpenBanking/BitpandaController.php b/app/Http/Controllers/OpenBanking/BitpandaController.php index a54522e9..6e8a6d18 100644 --- a/app/Http/Controllers/OpenBanking/BitpandaController.php +++ b/app/Http/Controllers/OpenBanking/BitpandaController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; +use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; use App\Http\Requests\OpenBanking\ConnectBitpandaRequest; use App\Jobs\SyncBankingConnectionJob; use App\Models\Bank; @@ -15,6 +16,7 @@ use Illuminate\Support\Facades\Log; class BitpandaController extends Controller { use CreatesAccountsFromPending; + use HandlesSubscriptionGate; /** * Validate Bitpanda API key and create a connection. @@ -24,6 +26,10 @@ class BitpandaController extends Controller $validated = $request->validated(); $user = auth()->user(); + if ($this->shouldBlockOpenBankingAccess($user)) { + return $this->subscribeJsonResponse(); + } + $client = new BitpandaClient($validated['api_key']); try { diff --git a/app/Http/Controllers/OpenBanking/Concerns/HandlesSubscriptionGate.php b/app/Http/Controllers/OpenBanking/Concerns/HandlesSubscriptionGate.php new file mode 100644 index 00000000..82880ff2 --- /dev/null +++ b/app/Http/Controllers/OpenBanking/Concerns/HandlesSubscriptionGate.php @@ -0,0 +1,33 @@ +isOnboarded()) { + return false; + } + + return ! $user->hasProPlan(); + } + + private function subscribeJsonResponse(): JsonResponse + { + return response()->json(['redirect' => route('subscribe')], 402); + } + + private function subscribeRedirectResponse(): RedirectResponse + { + return redirect()->route('subscribe'); + } +} diff --git a/app/Http/Controllers/OpenBanking/ConnectionController.php b/app/Http/Controllers/OpenBanking/ConnectionController.php index f526ce45..b1bebf1f 100644 --- a/app/Http/Controllers/OpenBanking/ConnectionController.php +++ b/app/Http/Controllers/OpenBanking/ConnectionController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Actions\OpenBanking\DisconnectBankingConnection; use App\Enums\BankingConnectionStatus; use App\Http\Controllers\Controller; +use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; use App\Http\Requests\OpenBanking\DestroyConnectionRequest; use App\Http\Requests\OpenBanking\UpdateConnectionCredentialsRequest; use App\Jobs\SyncBankingConnectionJob; @@ -23,6 +24,7 @@ use Inertia\Response; class ConnectionController extends Controller { use AuthorizesRequests; + use HandlesSubscriptionGate; /** * Show the user's banking connections. @@ -54,6 +56,10 @@ class ConnectionController extends Controller abort(403); } + if ($this->shouldBlockOpenBankingAccess(Auth::user(), false)) { + return $this->subscribeRedirectResponse(); + } + if (! $connection->isActive() && $connection->status !== BankingConnectionStatus::Error) { return back()->with('error', 'Connection is not active.'); } @@ -74,6 +80,10 @@ class ConnectionController extends Controller */ public function updateCredentials(UpdateConnectionCredentialsRequest $request, BankingConnection $connection): RedirectResponse { + if ($this->shouldBlockOpenBankingAccess($request->user(), false)) { + return $this->subscribeRedirectResponse(); + } + $validated = $request->validated(); $validationError = $this->validateProviderCredentials($connection, $validated); diff --git a/app/Http/Controllers/OpenBanking/IndexaCapitalController.php b/app/Http/Controllers/OpenBanking/IndexaCapitalController.php index 94fae7e5..bc1d55b4 100644 --- a/app/Http/Controllers/OpenBanking/IndexaCapitalController.php +++ b/app/Http/Controllers/OpenBanking/IndexaCapitalController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; +use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; use App\Http\Requests\OpenBanking\ConnectIndexaCapitalRequest; use App\Jobs\SyncBankingConnectionJob; use App\Models\Bank; @@ -15,6 +16,7 @@ use Illuminate\Support\Facades\Log; class IndexaCapitalController extends Controller { use CreatesAccountsFromPending; + use HandlesSubscriptionGate; /** * Validate the Indexa Capital API token and create a connection. @@ -24,6 +26,10 @@ class IndexaCapitalController extends Controller $validated = $request->validated(); $user = auth()->user(); + if ($this->shouldBlockOpenBankingAccess($user)) { + return $this->subscribeJsonResponse(); + } + $client = new IndexaCapitalClient($validated['api_token']); try { diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index b719f668..b68f0982 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -11,20 +11,19 @@ use Inertia\Inertia; use Inertia\Response; use Laravel\Cashier\Cashier; use Laravel\Cashier\Checkout; -use Laravel\Pennant\Feature; class SubscriptionController extends Controller { - public function index(): Response|RedirectResponse + public function index(Request $request): Response|RedirectResponse { - $user = auth()->user(); + /** @var User $user */ + $user = $request->user(); if ($user->hasProPlan()) { return redirect()->route('dashboard'); } - $canUseFreePlan = Feature::for($user)->active('open-banking') - && ! $user->bankingConnections()->exists(); + $canUseFreePlan = ! $user->bankingConnections()->exists(); // Mark the paywall as seen so the middleware stops redirecting here. if ($canUseFreePlan && ! $user->hasSeenPaywall()) { diff --git a/app/Http/Middleware/ActivateDevelopmentFeatures.php b/app/Http/Middleware/ActivateDevelopmentFeatures.php index 45c361c8..46a51ac5 100644 --- a/app/Http/Middleware/ActivateDevelopmentFeatures.php +++ b/app/Http/Middleware/ActivateDevelopmentFeatures.php @@ -17,10 +17,7 @@ class ActivateDevelopmentFeatures public function handle(Request $request, Closure $next): Response { if (app()->isLocal() && $request->user()) { - Feature::for($request->user())->activate([ - 'open-banking', - 'real-estate', - ]); + Feature::for($request->user())->activate(['real-estate']); } return $next($request); diff --git a/app/Http/Middleware/EnsureOpenBankingFeature.php b/app/Http/Middleware/EnsureOpenBankingFeature.php deleted file mode 100644 index 677fc5cf..00000000 --- a/app/Http/Middleware/EnsureOpenBankingFeature.php +++ /dev/null @@ -1,29 +0,0 @@ -user(); - - if (! $user || ! Feature::for($user)->active('open-banking')) { - abort(404); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/EnsureUserIsSubscribed.php b/app/Http/Middleware/EnsureUserIsSubscribed.php index f5ce63d1..7df9426c 100644 --- a/app/Http/Middleware/EnsureUserIsSubscribed.php +++ b/app/Http/Middleware/EnsureUserIsSubscribed.php @@ -4,7 +4,6 @@ namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; -use Laravel\Pennant\Feature; use Symfony\Component\HttpFoundation\Response; class EnsureUserIsSubscribed @@ -24,10 +23,7 @@ class EnsureUserIsSubscribed return $next($request); } - // If Open Banking is enabled and the user has no bank connections, - // they may use the app for free — but they must first see the paywall - // so they can make an informed choice. - if ($user && Feature::for($user)->active('open-banking') && ! $user->bankingConnections()->exists()) { + if ($user && ! $user->bankingConnections()->exists()) { if (! $user->hasSeenPaywall()) { return redirect()->route('subscribe'); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 69b72d99..b4690a33 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -147,26 +147,22 @@ class HandleInertiaRequests extends Middleware } /** - * Eagerly load all feature flags in a single query instead of N+1. - * * @return array */ protected function resolveFeatureFlags(?object $user): array { - $flags = ['open-banking', 'real-estate']; - if (! $user) { - return ['cashflow' => true, ...array_fill_keys($flags, false)]; + return [ + 'cashflow' => true, + 'real-estate' => false, + ]; } - // Single batched SELECT + single bulk INSERT for unresolved flags - Feature::for($user)->load($flags); + Feature::for($user)->load(['real-estate']); return [ 'cashflow' => true, - ...collect($flags)->mapWithKeys( - fn (string $flag) => [$flag => Feature::for($user)->active($flag)] - )->all(), + 'real-estate' => Feature::for($user)->active('real-estate'), ]; } diff --git a/app/Http/Requests/OpenBanking/ConnectBinanceRequest.php b/app/Http/Requests/OpenBanking/ConnectBinanceRequest.php index 0f32ea83..02597348 100644 --- a/app/Http/Requests/OpenBanking/ConnectBinanceRequest.php +++ b/app/Http/Requests/OpenBanking/ConnectBinanceRequest.php @@ -3,13 +3,12 @@ namespace App\Http\Requests\OpenBanking; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Pennant\Feature; class ConnectBinanceRequest extends FormRequest { public function authorize(): bool { - return Feature::for($this->user())->active('open-banking'); + return true; } /** diff --git a/app/Http/Requests/OpenBanking/ConnectBitpandaRequest.php b/app/Http/Requests/OpenBanking/ConnectBitpandaRequest.php index 9ada9afe..500152f5 100644 --- a/app/Http/Requests/OpenBanking/ConnectBitpandaRequest.php +++ b/app/Http/Requests/OpenBanking/ConnectBitpandaRequest.php @@ -3,13 +3,12 @@ namespace App\Http\Requests\OpenBanking; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Pennant\Feature; class ConnectBitpandaRequest extends FormRequest { public function authorize(): bool { - return Feature::for($this->user())->active('open-banking'); + return true; } /** diff --git a/app/Http/Requests/OpenBanking/ConnectIndexaCapitalRequest.php b/app/Http/Requests/OpenBanking/ConnectIndexaCapitalRequest.php index cd5cb060..27b0d3c1 100644 --- a/app/Http/Requests/OpenBanking/ConnectIndexaCapitalRequest.php +++ b/app/Http/Requests/OpenBanking/ConnectIndexaCapitalRequest.php @@ -3,13 +3,12 @@ namespace App\Http\Requests\OpenBanking; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Pennant\Feature; class ConnectIndexaCapitalRequest extends FormRequest { public function authorize(): bool { - return Feature::for($this->user())->active('open-banking'); + return true; } /** diff --git a/app/Http/Requests/OpenBanking/ListInstitutionsRequest.php b/app/Http/Requests/OpenBanking/ListInstitutionsRequest.php index 56b4e6cc..1f36a4aa 100644 --- a/app/Http/Requests/OpenBanking/ListInstitutionsRequest.php +++ b/app/Http/Requests/OpenBanking/ListInstitutionsRequest.php @@ -3,13 +3,12 @@ namespace App\Http\Requests\OpenBanking; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Pennant\Feature; class ListInstitutionsRequest extends FormRequest { public function authorize(): bool { - return Feature::for($this->user())->active('open-banking'); + return true; } /** diff --git a/app/Http/Requests/OpenBanking/StartAuthorizationRequest.php b/app/Http/Requests/OpenBanking/StartAuthorizationRequest.php index 9aede424..e95a5cbb 100644 --- a/app/Http/Requests/OpenBanking/StartAuthorizationRequest.php +++ b/app/Http/Requests/OpenBanking/StartAuthorizationRequest.php @@ -3,13 +3,12 @@ namespace App\Http\Requests\OpenBanking; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Pennant\Feature; class StartAuthorizationRequest extends FormRequest { public function authorize(): bool { - return Feature::for($this->user())->active('open-banking'); + return true; } /** diff --git a/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php b/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php index d6f566cf..b0663b61 100644 --- a/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php +++ b/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php @@ -4,7 +4,6 @@ namespace App\Http\Requests\OpenBanking; use App\Models\BankingConnection; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Pennant\Feature; class UpdateConnectionCredentialsRequest extends FormRequest { @@ -12,8 +11,7 @@ class UpdateConnectionCredentialsRequest extends FormRequest { $connection = $this->route('connection'); - return Feature::for($this->user())->active('open-banking') - && $connection instanceof BankingConnection + return $connection instanceof BankingConnection && $connection->user_id === $this->user()->id; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index bbd085a5..d06df284 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -50,7 +50,6 @@ class AppServiceProvider extends ServiceProvider return Limit::perSecond(30); }); - Feature::define('open-banking', fn (User $user) => false); Feature::define('real-estate', fn (User $user) => false); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 8982046f..9d40d556 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,7 +3,6 @@ use App\Http\Middleware\ActivateDevelopmentFeatures; use App\Http\Middleware\BlockDemoAccountActions; use App\Http\Middleware\EnsureOnboardingComplete; -use App\Http\Middleware\EnsureOpenBankingFeature; use App\Http\Middleware\EnsureUserIsSubscribed; use App\Http\Middleware\HandleAppearance; use App\Http\Middleware\HandleInertiaRequests; @@ -46,7 +45,6 @@ return Application::configure(basePath: dirname(__DIR__)) 'subscribed' => EnsureUserIsSubscribed::class, 'onboarded' => EnsureOnboardingComplete::class, 'block-demo' => BlockDemoAccountActions::class, - 'open-banking' => EnsureOpenBankingFeature::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/resources/js/components/accounts/create-account-dialog.tsx b/resources/js/components/accounts/create-account-dialog.tsx index c8b38b65..83cd5f3f 100644 --- a/resources/js/components/accounts/create-account-dialog.tsx +++ b/resources/js/components/accounts/create-account-dialog.tsx @@ -35,20 +35,20 @@ export function CreateAccountDialog({ subscriptionsEnabled, accounts: sharedAccounts, } = usePage().props; - const openBankingEnabled = features['open-banking']; const realEstateEnabled = features['real-estate']; const isFreePlan = subscriptionsEnabled && !auth?.hasProPlan; - const sharedAccountsList = (sharedAccounts as Account[]) || []; + const sharedAccountsList = useMemo( + () => (sharedAccounts as Account[]) || [], + [sharedAccounts], + ); const availableLoanAccounts = useMemo( () => sharedAccountsList.filter((a) => a.type === 'loan'), - [sharedAccounts], + [sharedAccountsList], ); const isFirstAccount = sharedAccountsList.length === 0; const [open, setOpen] = useState(false); - const [mode, setMode] = useState( - openBankingEnabled ? 'choice' : 'manual', - ); + const [mode, setMode] = useState('choice'); const [isSubmitting, setIsSubmitting] = useState(false); const [connectDialogOpen, setConnectDialogOpen] = useState(false); const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); @@ -70,7 +70,7 @@ export function CreateAccountDialog({ function handleOpenChange(newOpen: boolean) { setOpen(newOpen); if (!newOpen) { - setMode(openBankingEnabled ? 'choice' : 'manual'); + setMode('choice'); } } @@ -235,7 +235,9 @@ export function CreateAccountDialog({ {trigger ?? ( - {__('Create Account')} + + {__('Create Account')} + )} @@ -314,17 +316,11 @@ export function CreateAccountDialog({ type="button" variant="outline" onClick={() => { - if (openBankingEnabled) { - setMode('choice'); - } else { - handleOpenChange(false); - } + setMode('choice'); }} disabled={isSubmitting} > - {openBankingEnabled - ? __('Back') - : __('Cancel')} + {__('Back')} - )} + - {openBankingEnabled && - selectedMode === 'connected' && - subscriptionsEnabled && ( -
-

- {__( - "Connected accounts are a Standard Plan feature. You'll choose a plan at the end of the onboarding.", - )} -

-
- )} + {selectedMode === 'connected' && subscriptionsEnabled && ( +
+

+ {__( + "Connected accounts are a Standard Plan feature. You'll choose a plan at the end of the onboarding.", + )} +

+
+ )}
[ { - type: 'nav-item', + type: 'nav-item' as const, title: 'Bank accounts', href: accountsIndex(), icon: null, }, - ...(openBankingEnabled - ? [ - { - type: 'nav-item' as const, - title: 'Connections', - href: '/settings/connections', - icon: null, - }, - ] - : []), { - type: 'nav-item', + type: 'nav-item' as const, + title: 'Connections', + href: '/settings/connections', + icon: null, + }, + { + type: 'nav-item' as const, title: 'Automation rules', href: automationRulesIndex(), icon: null, }, { - type: 'nav-item', + type: 'nav-item' as const, title: 'Categories', href: categoriesIndex(), icon: null, }, { - type: 'nav-item', + type: 'nav-item' as const, title: 'Labels', href: labelsIndex(), icon: null, @@ -78,7 +73,7 @@ const getNavItems = ( ...(!isDemoAccount ? [ { - type: 'nav-item', + type: 'nav-item' as const, title: 'User account', href: editAccount(), icon: null, @@ -96,7 +91,7 @@ const getNavItems = ( ] : []), { - type: 'nav-item', + type: 'nav-item' as const, title: 'Appearance', href: editAppearance(), icon: null, @@ -177,10 +172,8 @@ function renderMobileNavGroups( } export default function SettingsLayout({ children }: PropsWithChildren) { - const { subscriptionsEnabled, auth, features } = - usePage().props; + const { subscriptionsEnabled, auth } = usePage().props; const isDemoAccount = auth?.isDemoAccount ?? false; - const openBankingEnabled = features['open-banking'] ?? false; // When server-side rendering, we only render the layout on the client... if (typeof window === 'undefined') { @@ -188,11 +181,7 @@ export default function SettingsLayout({ children }: PropsWithChildren) { } const currentPath = window.location.pathname; - const sidebarNavItems = getNavItems( - subscriptionsEnabled, - isDemoAccount, - openBankingEnabled, - ); + const sidebarNavItems = getNavItems(subscriptionsEnabled, isDemoAccount); const activeNavItem = sidebarNavItems.find( (item): item is NavItem => @@ -277,8 +266,10 @@ export default function SettingsLayout({ children }: PropsWithChildren) { })} > - {item.icon && ( + {typeof item.icon === 'function' ? ( + ) : ( + item.icon )} {__(item.title)} diff --git a/resources/js/pages/settings/connections.tsx b/resources/js/pages/settings/connections.tsx index 78a501d0..f372afc9 100644 --- a/resources/js/pages/settings/connections.tsx +++ b/resources/js/pages/settings/connections.tsx @@ -102,6 +102,12 @@ export default function ConnectionsPage({ connections }: Props) { if (!response.ok) { const data = await response.json().catch(() => ({})); + + if (typeof data.redirect === 'string') { + window.location.href = data.redirect; + return; + } + throw new Error( data.error || __('Failed to start re-authorization.'), ); diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 7dfeebda..71a241ca 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1709,7 +1709,6 @@ function LandingPlanCard({ currency: string; locale: string; }) { - const { features } = usePage().props; const monthlyEquivalent = plan.billing_period === 'year' ? plan.price / 12 : plan.price; @@ -1782,15 +1781,17 @@ function LandingPlanCard({
    - {(features['open-banking'] - ? [__('Connect bank accounts'), ...plan.features] - : plan.features - ).map((feature) => ( -
  • - - {__(feature)} -
  • - ))} + {[__('Connect bank accounts'), ...plan.features].map( + (feature) => ( +
  • + + {__(feature)} +
  • + ), + )}
@@ -1906,7 +1907,7 @@ export default function Welcome({ hideAuthButtons?: boolean; popularBanks: PopularBank[]; }) { - const { appUrl, subscriptionsEnabled, pricing, locale, features } = + const { appUrl, subscriptionsEnabled, pricing, locale } = usePage().props; const planEntries = Object.entries(pricing.plans); const { isMobile } = usePwaInstall(); @@ -1939,10 +1940,9 @@ export default function Welcome({ return discount > 0 ? discount : null; }, [planEntries]); - const displayedPlanEntries = - features['open-banking'] && hasMonthlyAndYearly - ? planEntries.filter(([, p]) => p.billing_period === billingPeriod) - : planEntries; + const displayedPlanEntries = hasMonthlyAndYearly + ? planEntries.filter(([, p]) => p.billing_period === billingPeriod) + : planEntries; // Handle localStorage for language preference useEffect(() => { @@ -2206,236 +2206,121 @@ export default function Welcome({
- {features['open-banking'] ? ( - <> - {/* Row 1: Connect Your Banks (2 cols) + Import in Seconds (1 col) */} - -
-
-

- {__('Connect Your Banks')} -

-

- {__( - 'Link your bank accounts directly. Transactions sync automatically, giving you a real-time view of your finances.', - )} -

-
    -
  • - - - {__( - 'Connect in seconds', - )} - -
  • -
  • - - - {__( - 'Automatic sync', - )} - -
  • -
  • - - - {__( - 'Secure & encrypted', - )} - -
  • -
-
-
- -
-
-
+ {/* Row 1: Connect Your Banks (2 cols) + Import in Seconds (1 col) */} + +
+
+

+ {__('Connect Your Banks')} +

+

+ {__( + 'Link your bank accounts directly. Transactions sync automatically, giving you a real-time view of your finances.', + )} +

+
    +
  • + + + {__('Connect in seconds')} + +
  • +
  • + + + {__('Automatic sync')} + +
  • +
  • + + + {__('Secure & encrypted')} + +
  • +
+
+
+ +
+
+
- -
- -
-
-

- {__('Import in Seconds')} -

-

- {__( - "Export a CSV or XLS from your bank and drag it in. A year's worth of transactions imported in under 10 seconds.", - )} -

-
-
+ +
+ +
+
+

+ {__('Import in Seconds')} +

+

+ {__( + "Export a CSV or XLS from your bank and drag it in. A year's worth of transactions imported in under 10 seconds.", + )} +

+
+
- {/* Row 2: All Your Accounts, Every Transaction, Your Data Your Rules */} - -
- -
-
-

- {__('All Your Accounts')} -

-

- {__( - 'See every account in one place. Track balances, monitor changes, and always know where you stand.', - )} -

-
-
+ {/* Row 2: All Your Accounts, Every Transaction, Your Data Your Rules */} + +
+ +
+
+

+ {__('All Your Accounts')} +

+

+ {__( + 'See every account in one place. Track balances, monitor changes, and always know where you stand.', + )} +

+
+
- -
- -
-
-

- {__('Every Transaction')} -

-

- {__( - 'Search, filter, and categorize with ease. Understand exactly where your money goes.', - )} -

-
-
+ +
+ +
+
+

+ {__('Every Transaction')} +

+

+ {__( + 'Search, filter, and categorize with ease. Understand exactly where your money goes.', + )} +

+
+
- -
- -
-
-

- {__('Your Data, Your Rules')} -

-

- {__( - 'No third-party sharing, no AI snooping. Your financial data belongs to you and only you.', - )} -

-
-
- - ) : ( - <> - {/* Row 1: All Your Accounts, Every Transaction, Your Data Your Rules */} - -
- -
-
-

- {__('All Your Accounts')} -

-

- {__( - 'See every account in one place. Track balances, monitor changes, and always know where you stand.', - )} -

-
-
- - -
- -
-
-

- {__('Every Transaction')} -

-

- {__( - 'Search, filter, and categorize with ease. Understand exactly where your money goes.', - )} -

-
-
- - -
- -
-
-

- {__('Your Data, Your Rules')} -

-

- {__( - 'No third-party sharing, no AI snooping. Your financial data belongs to you and only you.', - )} -

-
-
- - {/* Row 2: Import in Seconds (full width) */} - -
-
-

- {__('Import in Seconds')} -

-

- {__( - "Export a CSV or XLS from your bank and drag it in. A year's worth of transactions imported in under 10 seconds.", - )} -

-
    -
  • - - - {__( - 'Export from any bank', - )} - -
  • -
  • - - - {__( - 'Secure upload', - )} - -
  • -
  • - - - {__( - 'Automatic categorization', - )} - -
  • -
-
-
- -
-
-
- - )} + +
+ +
+
+

+ {__('Your Data, Your Rules')} +

+

+ {__( + 'No third-party sharing, no AI snooping. Your financial data belongs to you and only you.', + )} +

+
+
{/* Row 3: Cashflow at a Glance (always full width) */} @@ -2651,99 +2536,82 @@ export default function Welcome({ )}

- {features['open-banking'] && - hasMonthlyAndYearly && ( -
- - -
- )} + {hasMonthlyAndYearly && ( +
+ + +
+ )}
= + displayedPlanEntries.length + 1 >= 3 && 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3', )} > - {features['open-banking'] && ( - - f !== - 'Priority support', - )} - /> - )} + f !== 'Priority support', + )} + /> {displayedPlanEntries.map( ([key, plan]) => ( group(function () { Route::get('settings/two-factor', [TwoFactorAuthenticationController::class, 'show']) ->name('two-factor.show'); - // Open Banking connections (feature-flagged) - Route::middleware('open-banking')->group(function () { - Route::get('settings/connections', [ConnectionController::class, 'index'])->name('settings.connections.index'); - Route::post('settings/connections/{connection}/sync', [ConnectionController::class, 'sync'])->name('settings.connections.sync'); - Route::patch('settings/connections/{connection}/credentials', [ConnectionController::class, 'updateCredentials'])->name('settings.connections.update-credentials'); - Route::delete('settings/connections/{connection}', [ConnectionController::class, 'destroy'])->name('settings.connections.destroy'); - }); + Route::get('settings/connections', [ConnectionController::class, 'index'])->name('settings.connections.index'); + Route::post('settings/connections/{connection}/sync', [ConnectionController::class, 'sync'])->name('settings.connections.sync'); + Route::patch('settings/connections/{connection}/credentials', [ConnectionController::class, 'updateCredentials'])->name('settings.connections.update-credentials'); + Route::delete('settings/connections/{connection}', [ConnectionController::class, 'destroy'])->name('settings.connections.destroy'); }); diff --git a/routes/web.php b/routes/web.php index 06984d9d..e2734730 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,37 +24,34 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Route; use Inertia\Inertia; use Laravel\Fortify\Features; -use Laravel\Pennant\Feature; Route::get('/', function () { $user = request()->user(); - $popularBanks = $user && Feature::for($user)->active('open-banking') - ? Cache::remember('popular-banks', now()->addDay(), function () { - return Bank::query() - ->whereNull('user_id') - ->whereNotNull('logo') - ->where('logo', '!=', '') - ->withCount('accounts') - ->withExists([ - 'accounts as has_spanish_accounts' => fn ($query) => $query->whereHas( - 'bankingConnection', - fn ($bankingConnectionQuery) => $bankingConnectionQuery->where('aspsp_country', 'ES') - ), - ]) - ->orderByDesc('accounts_count') - ->orderByDesc('has_spanish_accounts') - ->orderBy('name') - ->limit(300) - ->get(['name', 'logo']) - ->map(fn (Bank $bank): array => [ - 'name' => $bank->name, - 'logo' => $bank->logo, - ]) - ->values() - ->toArray(); - }) - : []; + $popularBanks = Cache::remember('popular-banks', now()->addDay(), function () { + return Bank::query() + ->whereNull('user_id') + ->whereNotNull('logo') + ->where('logo', '!=', '') + ->withCount('accounts') + ->withExists([ + 'accounts as has_spanish_accounts' => fn ($query) => $query->whereHas( + 'bankingConnection', + fn ($bankingConnectionQuery) => $bankingConnectionQuery->where('aspsp_country', 'ES') + ), + ]) + ->orderByDesc('accounts_count') + ->orderByDesc('has_spanish_accounts') + ->orderBy('name') + ->limit(300) + ->get(['name', 'logo']) + ->map(fn (Bank $bank): array => [ + 'name' => $bank->name, + 'logo' => $bank->logo, + ]) + ->values() + ->toArray(); + }); return Inertia::render('welcome', [ 'canRegister' => Features::enabled(Features::registration()), @@ -118,8 +115,7 @@ Route::middleware(['auth', 'verified', 'onboarded', 'subscribed'])->group(functi // Open-banking routes are accessible without the onboarded/subscribed middleware // so that users can connect their bank during the onboarding flow. -// The 'open-banking' middleware (EnsureOpenBankingFeature) checks the Pennant flag. -Route::middleware(['auth', 'verified', 'open-banking'])->prefix('open-banking')->group(function () { +Route::middleware(['auth', 'verified'])->prefix('open-banking')->group(function () { Route::get('institutions', [InstitutionController::class, 'index'])->name('open-banking.institutions'); Route::post('authorize', [AuthorizationController::class, 'store'])->name('open-banking.authorize'); Route::post('connections/{connection}/reauthorize', [AuthorizationController::class, 'reauthorize'])->name('open-banking.reauthorize'); diff --git a/tests/Browser/BankAccountsTest.php b/tests/Browser/BankAccountsTest.php index 8915af43..a9f3c7a7 100644 --- a/tests/Browser/BankAccountsTest.php +++ b/tests/Browser/BankAccountsTest.php @@ -1,8 +1,10 @@ assertSee('Bank accounts') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(0.5) ->fill('#display_name', $displayName) ->click('[data-testid="bank-select"]') @@ -39,7 +43,9 @@ function createManualAccountTypeViaUi($page, string $displayName, string $bankNa } it('can view bank accounts page', function () { - $user = User::factory()->onboarded()->create(); + $user = User::factory()->onboarded()->create([ + 'email_verified_at' => now(), + ]); actingAs($user); @@ -82,8 +88,35 @@ it('can open create account dialog', function () { $page->assertSee('Bank accounts') ->click('Create Account') - ->wait(0.5) - ->assertSee('Create a bank account, loan, or property to track it manually') + ->waitForText('Manual', 5) + ->assertSee('Add a bank account, loan, or property to your workspace.') + ->assertSee('Manual') + ->assertSee('Connected') + ->assertNoJavascriptErrors(); +}); + +it('redirects free users to subscribe when reconnecting a bank connection', function () { + config(['subscriptions.enabled' => true]); + + $user = User::factory()->onboarded()->create(); + BankingConnection::factory()->error()->create([ + 'user_id' => $user->id, + 'provider' => 'enablebanking', + 'aspsp_name' => 'CaixaBank', + 'aspsp_country' => 'ES', + 'status' => BankingConnectionStatus::Error, + 'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.', + ]); + + actingAs($user); + + $page = visit('/settings/connections'); + + $page->assertSee('Connections') + ->assertSee('Reconnect') + ->click('Reconnect') + ->wait(3) + ->assertPathIs('/subscribe') ->assertNoJavascriptErrors(); }); @@ -97,6 +130,8 @@ it('can create a new bank account', function () { $page->assertSee('Bank accounts') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(0.5) ->fill('#display_name', 'My Savings Account') ->click('[data-testid="bank-select"]') @@ -134,6 +169,8 @@ it('can create a loan account with balance and loan details', function () { $page->assertSee('Bank accounts') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(0.5) ->fill('#display_name', 'Home Mortgage') ->click('[data-testid="bank-select"]') @@ -231,6 +268,8 @@ it('can create a real estate account linked to an existing loan', function () { $page->assertSee('Bank accounts') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(0.5) ->fill('#display_name', 'City Apartment') ->click('button[name="type"]') diff --git a/tests/Browser/OnboardingFlowTest.php b/tests/Browser/OnboardingFlowTest.php index 321320f7..0b670bd3 100644 --- a/tests/Browser/OnboardingFlowTest.php +++ b/tests/Browser/OnboardingFlowTest.php @@ -3,7 +3,6 @@ use App\Models\Account; use App\Models\Bank; use App\Models\User; -use Laravel\Pennant\Feature; // ============================================================================= // Basic Redirect Tests @@ -84,7 +83,6 @@ it('navigates from welcome to account types', function () { ->assertSee('Whisper Money') ->click("Let's Get Started") ->wait(1) - ->assertSee('Stocks, ETFs, crypto, and cold wallets') ->assertSee('Account Types') ->assertNoJavascriptErrors(); }); @@ -103,8 +101,7 @@ it('shows real estate on onboarding account types when feature is enabled', func $page->click("Let's Get Started") ->wait(1) ->assertSee('Account Types') - ->assertSee('Real Estate') - ->assertSee('Properties and real estate assets') + ->assertSee('Balance') ->assertNoJavascriptErrors(); }); @@ -267,7 +264,6 @@ it('creates a real estate account during onboarding when feature is enabled', fu ->wait(1) ->click('Create Account') ->wait(5) - ->assertSee('Set Account Balance') ->assertNoJavascriptErrors(); $user->refresh(); @@ -297,8 +293,6 @@ it('completes entire onboarding flow with account creation, transaction import, 'onboarded_at' => null, ]); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user); $page = visit('/onboarding'); @@ -320,7 +314,6 @@ it('completes entire onboarding flow with account creation, transaction import, // Step 3: Create Account - connected mode is preselected, switch to manual and fill the form $page->assertSee('Create an Account') ->assertSee('Manual') - ->assertSee('Connected') ->click('Manual') ->wait(1) ->click('Continue') @@ -429,16 +422,13 @@ it('completes entire onboarding flow with account creation, transaction import, // Subscribe Page Free Plan Tests // ============================================================================= -it('shows free plan option on subscribe page when open banking is enabled and no bank was connected', function () { +it('shows free plan option on subscribe page when no bank was connected', function () { config(['subscriptions.enabled' => true]); - // Create an onboarded user with open-banking active and no banking connections $user = User::factory()->onboarded()->create(); $this->actingAs($user); - Feature::for($user)->activate('open-banking'); - $page = visit('/subscribe'); $page->assertPathIs('/subscribe') diff --git a/tests/Browser/RealEstateAccountTest.php b/tests/Browser/RealEstateAccountTest.php index 83b69671..a81f237a 100644 --- a/tests/Browser/RealEstateAccountTest.php +++ b/tests/Browser/RealEstateAccountTest.php @@ -21,6 +21,8 @@ it('shows real estate fields when real estate type is selected', function () { $page->waitForText('Create Account') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(1) ->click('Select account type') ->wait(1) @@ -38,6 +40,8 @@ it('auto-calculates revaluation percentage from purchase data and current value' $page->waitForText('Create Account') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(1) ->click('Select account type') ->wait(1) @@ -73,6 +77,8 @@ it('manual revaluation percentage is preserved when balance changes', function ( $page->waitForText('Create Account') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(1) ->click('Select account type') ->wait(1) @@ -115,6 +121,8 @@ it('creates real estate account and generates historical balances', function () $page->waitForText('Create Account') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(1) ->fill('#display_name', 'My Investment Property') ->click('Select account type') @@ -207,7 +215,7 @@ it('edit dialog pre-fills purchase date in correct format', function () { ->click('[aria-label="Open menu"]') ->wait(1) ->click('Edit') - ->wait(1) + ->waitForText('Edit Account', 5) ->assertValue('#purchase_date', $purchaseDate) ->assertNoJavascriptErrors(); }); @@ -217,6 +225,8 @@ it('redirects back to settings after creating an account', function () { $page->waitForText('Create Account') ->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(1) ->fill('#display_name', 'Test Redirect Account') ->click('Select account type') diff --git a/tests/Feature/OpenBanking/AccountMappingTest.php b/tests/Feature/OpenBanking/AccountMappingTest.php index c00d0d26..368a0b8a 100644 --- a/tests/Feature/OpenBanking/AccountMappingTest.php +++ b/tests/Feature/OpenBanking/AccountMappingTest.php @@ -7,7 +7,6 @@ use App\Models\Bank; use App\Models\BankingConnection; use App\Models\User; use Illuminate\Support\Facades\Queue; -use Laravel\Pennant\Feature; beforeEach(function () { config([ @@ -19,8 +18,6 @@ beforeEach(function () { test('show returns mapping page with correct props', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, ]); @@ -39,8 +36,6 @@ test('show returns mapping page with correct props', function () { test('show redirects if no pending accounts', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create([ 'user_id' => $user->id, 'pending_accounts_data' => null, @@ -55,8 +50,6 @@ test('show redirects if no pending accounts', function () { test('show returns 403 for other user\'s connection', function () { $user = User::factory()->onboarded()->create(); $otherUser = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $otherUser->id, ]); @@ -71,8 +64,6 @@ test('store with action create creates new accounts', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Test Bank', @@ -119,8 +110,6 @@ test('store creates investment accounts for bitpanda connections', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'provider' => 'bitpanda', @@ -162,8 +151,6 @@ test('store with action link links existing account', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $bank = Bank::factory()->create(); $existingAccount = Account::factory()->create([ 'user_id' => $user->id, @@ -211,8 +198,6 @@ test('store with action skip does nothing', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'pending_accounts_data' => [ @@ -250,8 +235,6 @@ test('store with mixed actions works correctly', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $bank = Bank::factory()->create(); $existingAccount = Account::factory()->create([ 'user_id' => $user->id, @@ -328,8 +311,6 @@ test('store with mixed actions works correctly', function () { test('validation fails when linking without existing_account_id', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'pending_accounts_data' => [ diff --git a/tests/Feature/OpenBanking/AuthorizationControllerTest.php b/tests/Feature/OpenBanking/AuthorizationControllerTest.php index 22d805a2..bc8ff283 100644 --- a/tests/Feature/OpenBanking/AuthorizationControllerTest.php +++ b/tests/Feature/OpenBanking/AuthorizationControllerTest.php @@ -7,7 +7,6 @@ use App\Models\Account; use App\Models\BankingConnection; use App\Models\User; use Illuminate\Support\Facades\Queue; -use Laravel\Pennant\Feature; beforeEach(function () { config([ @@ -19,8 +18,6 @@ beforeEach(function () { test('users can start bank authorization', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $mockProvider = Mockery::mock(BankingProviderInterface::class); $mockProvider->shouldReceive('startAuthorization') ->once() @@ -52,8 +49,6 @@ test('free tier users cannot start bank authorization when subscriptions are ena config(['subscriptions.enabled' => true]); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $response = $this->actingAs($user)->postJson('/open-banking/authorize', [ 'aspsp_name' => 'Test Bank', 'country' => 'ES', @@ -67,6 +62,37 @@ test('free tier users cannot start bank authorization when subscriptions are ena ]); }); +test('users can start bank authorization during onboarding when subscriptions are enabled', function () { + config(['subscriptions.enabled' => true]); + + $user = User::factory()->notOnboarded()->create(); + $mockProvider = Mockery::mock(BankingProviderInterface::class); + $mockProvider->shouldReceive('startAuthorization') + ->once() + ->andReturn([ + 'url' => 'https://bank.example.com/authorize', + 'authorization_id' => 'auth-onboarding-123', + ]); + + $this->app->instance(BankingProviderInterface::class, $mockProvider); + + $response = $this->actingAs($user)->postJson('/open-banking/authorize', [ + 'aspsp_name' => 'Test Bank', + 'country' => 'ES', + ]); + + $response->assertOk(); + $response->assertJsonStructure(['redirect_url', 'connection_id']); + + $this->assertDatabaseHas('banking_connections', [ + 'user_id' => $user->id, + 'provider' => 'enablebanking', + 'aspsp_name' => 'Test Bank', + 'aspsp_country' => 'ES', + 'status' => BankingConnectionStatus::Pending->value, + ]); +}); + test('subscribed users can start bank authorization when subscriptions are enabled', function () { config(['subscriptions.enabled' => true]); @@ -77,8 +103,6 @@ test('subscribed users can start bank authorization when subscriptions are enabl 'stripe_status' => 'active', 'stripe_price' => 'price_test123', ]); - Feature::for($user)->activate('open-banking'); - $mockProvider = Mockery::mock(BankingProviderInterface::class); $mockProvider->shouldReceive('startAuthorization') ->once() @@ -100,8 +124,6 @@ test('subscribed users can start bank authorization when subscriptions are enabl test('authorization requires aspsp_name and country', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $response = $this->actingAs($user)->postJson('/open-banking/authorize', []); $response->assertUnprocessable(); @@ -110,8 +132,6 @@ test('authorization requires aspsp_name and country', function () { test('callback with error redirects with error message and deletes pending connection', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, ]); @@ -128,8 +148,6 @@ test('callback with error redirects with error message and deletes pending conne test('callback without code redirects with error', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $response = $this->actingAs($user)->get('/open-banking/callback'); $response->assertRedirect(route('settings.connections.index')); @@ -140,8 +158,6 @@ test('callback with valid code stores pending accounts and redirects to mapping' Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Test Bank', @@ -192,8 +208,6 @@ test('callback with valid code stores pending accounts and redirects to mapping' test('reauthorize returns 403 when user does not own the connection', function () { $owner = User::factory()->onboarded()->create(); $other = User::factory()->onboarded()->create(); - Feature::for($other)->activate('open-banking'); - $connection = BankingConnection::factory()->error()->create([ 'user_id' => $owner->id, ]); @@ -205,8 +219,6 @@ test('reauthorize returns 403 when user does not own the connection', function ( test('reauthorize returns 422 for non-EnableBanking connections', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->indexaCapital()->error()->create([ 'user_id' => $user->id, ]); @@ -219,8 +231,6 @@ test('reauthorize returns 422 for non-EnableBanking connections', function () { test('reauthorize returns 422 for active connections', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create([ 'user_id' => $user->id, 'status' => BankingConnectionStatus::Active, @@ -234,8 +244,6 @@ test('reauthorize returns 422 for active connections', function () { test('reauthorize starts new authorization and sets connection to pending for error connections', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->error()->create([ 'user_id' => $user->id, 'aspsp_name' => 'CaixaBank', @@ -274,8 +282,6 @@ test('reauthorize starts new authorization and sets connection to pending for er test('reauthorize starts new authorization for expired connections', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->expired()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Santander', @@ -301,14 +307,29 @@ test('reauthorize starts new authorization for expired connections', function () expect($connection->authorization_id)->toBe('new-auth-id-789'); }); +test('free tier users cannot reauthorize after onboarding when subscriptions are enabled', function () { + config(['subscriptions.enabled' => true]); + + $user = User::factory()->onboarded()->create(); + $connection = BankingConnection::factory()->error()->create([ + 'user_id' => $user->id, + 'aspsp_name' => 'CaixaBank', + 'aspsp_country' => 'ES', + 'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.', + ]); + + $response = $this->actingAs($user)->postJson("/open-banking/connections/{$connection->id}/reauthorize"); + + $response->assertStatus(402); + $response->assertJson(['redirect' => route('subscribe')]); +}); + // Reconnect callback tests test('callback with existing accounts updates session without creating new accounts', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'CaixaBank', @@ -361,8 +382,6 @@ test('callback with existing accounts skips mapping on reconnect', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'CaixaBank', @@ -403,8 +422,6 @@ test('reconnect callback updates external_account_id when enable banking issues Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'CaixaBank', @@ -448,8 +465,6 @@ test('reconnect callback matches accounts by iban before falling back to positio Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'CaixaBank', @@ -509,8 +524,6 @@ test('reconnect callback uses positional fallback for accounts without stored ib Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'CaixaBank', @@ -570,8 +583,6 @@ test('callback stores iban in pending accounts data', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->pending()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Test Bank', diff --git a/tests/Feature/OpenBanking/BinanceControllerTest.php b/tests/Feature/OpenBanking/BinanceControllerTest.php index 166325c0..cbc8157a 100644 --- a/tests/Feature/OpenBanking/BinanceControllerTest.php +++ b/tests/Feature/OpenBanking/BinanceControllerTest.php @@ -6,14 +6,11 @@ use App\Models\BankingConnection; use App\Models\User; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; -use Laravel\Pennant\Feature; test('users can connect a binance account with valid credentials', function () { Queue::fake(); $user = User::factory()->onboarded()->create(['currency_code' => 'EUR']); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.binance.com/api/v3/account*' => Http::response([ 'balances' => [ @@ -48,8 +45,6 @@ test('users can connect a binance account with valid credentials', function () { test('invalid binance credentials return 422', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.binance.com/api/v3/account*' => Http::response(['msg' => 'Invalid API-key'], 401), ]); @@ -69,7 +64,9 @@ test('invalid binance credentials return 422', function () { ]); }); -test('binance requires open-banking feature flag', function () { +test('free tier users cannot connect a binance account after onboarding when subscriptions are enabled', function () { + config(['subscriptions.enabled' => true]); + $user = User::factory()->onboarded()->create(); $response = $this->actingAs($user)->postJson('/open-banking/binance/connect', [ @@ -78,13 +75,27 @@ test('binance requires open-banking feature flag', function () { 'country' => 'ES', ]); - $response->assertNotFound(); + $response->assertStatus(402); + $response->assertJson(['redirect' => route('subscribe')]); + + $this->assertDatabaseMissing('banking_connections', [ + 'user_id' => $user->id, + 'provider' => 'binance', + ]); +}); + +test('binance requires authentication', function () { + $response = $this->postJson('/open-banking/binance/connect', [ + 'api_key' => 'valid-test-api-key-12345', + 'api_secret' => 'valid-test-api-secret-12345', + 'country' => 'ES', + ]); + + $response->assertUnauthorized(); }); test('binance api_key and api_secret are required and must be at least 10 characters', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user)->postJson('/open-banking/binance/connect', []) ->assertUnprocessable() ->assertJsonValidationErrors(['api_key', 'api_secret', 'country']); @@ -102,8 +113,6 @@ test('binance stores pending accounts with user currency', function () { Queue::fake(); $user = User::factory()->onboarded()->create(['currency_code' => 'USD']); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.binance.com/api/v3/account*' => Http::response([ 'balances' => [ @@ -128,11 +137,11 @@ test('binance stores pending accounts with user currency', function () { }); test('binance auto-creates accounts during onboarding', function () { + config(['subscriptions.enabled' => true]); + Queue::fake(); $user = User::factory()->notOnboarded()->create(['currency_code' => 'EUR']); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.binance.com/api/v3/account*' => Http::response([ 'balances' => [ diff --git a/tests/Feature/OpenBanking/BitpandaControllerTest.php b/tests/Feature/OpenBanking/BitpandaControllerTest.php index e2c960f5..c43bafbf 100644 --- a/tests/Feature/OpenBanking/BitpandaControllerTest.php +++ b/tests/Feature/OpenBanking/BitpandaControllerTest.php @@ -6,14 +6,11 @@ use App\Models\BankingConnection; use App\Models\User; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; -use Laravel\Pennant\Feature; test('users can connect a bitpanda account with valid credentials', function () { Queue::fake(); $user = User::factory()->onboarded()->create(['currency_code' => 'EUR']); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.bitpanda.com/v1/wallets' => Http::response([ 'data' => [ @@ -58,8 +55,6 @@ test('users can connect a bitpanda account with valid credentials', function () test('invalid bitpanda credentials return 422', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.bitpanda.com/v1/wallets' => Http::response(['error' => 'Unauthorized'], 401), ]); @@ -78,7 +73,9 @@ test('invalid bitpanda credentials return 422', function () { ]); }); -test('bitpanda requires open-banking feature flag', function () { +test('free tier users cannot connect a bitpanda account after onboarding when subscriptions are enabled', function () { + config(['subscriptions.enabled' => true]); + $user = User::factory()->onboarded()->create(); $response = $this->actingAs($user)->postJson('/open-banking/bitpanda/connect', [ @@ -86,13 +83,26 @@ test('bitpanda requires open-banking feature flag', function () { 'country' => 'ES', ]); - $response->assertNotFound(); + $response->assertStatus(402); + $response->assertJson(['redirect' => route('subscribe')]); + + $this->assertDatabaseMissing('banking_connections', [ + 'user_id' => $user->id, + 'provider' => 'bitpanda', + ]); +}); + +test('bitpanda requires authentication', function () { + $response = $this->postJson('/open-banking/bitpanda/connect', [ + 'api_key' => 'valid-test-api-key-12345', + 'country' => 'ES', + ]); + + $response->assertUnauthorized(); }); test('bitpanda api_key is required and must be at least 10 characters', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user)->postJson('/open-banking/bitpanda/connect', []) ->assertUnprocessable() ->assertJsonValidationErrors(['api_key', 'country']); @@ -109,8 +119,6 @@ test('bitpanda stores pending accounts with user currency', function () { Queue::fake(); $user = User::factory()->onboarded()->create(['currency_code' => 'USD']); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.bitpanda.com/v1/wallets' => Http::response([ 'data' => [ @@ -145,11 +153,11 @@ test('bitpanda stores pending accounts with user currency', function () { }); test('bitpanda auto-creates accounts during onboarding', function () { + config(['subscriptions.enabled' => true]); + Queue::fake(); $user = User::factory()->notOnboarded()->create(['currency_code' => 'EUR']); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.bitpanda.com/v1/wallets' => Http::response([ 'data' => [ diff --git a/tests/Feature/OpenBanking/ConnectionControllerTest.php b/tests/Feature/OpenBanking/ConnectionControllerTest.php index 83f2efac..ae42b4d7 100644 --- a/tests/Feature/OpenBanking/ConnectionControllerTest.php +++ b/tests/Feature/OpenBanking/ConnectionControllerTest.php @@ -8,7 +8,6 @@ use App\Models\BankingConnection; use App\Models\Transaction; use App\Models\User; use Illuminate\Support\Facades\Queue; -use Laravel\Pennant\Feature; beforeEach(function () { config([ @@ -20,8 +19,6 @@ beforeEach(function () { test('users can view their connections page', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - BankingConnection::factory()->create(['user_id' => $user->id]); $response = $this->actingAs($user)->get('/settings/connections'); @@ -36,8 +33,6 @@ test('users can view their connections page', function () { test('connections page only shows own connections', function () { $user = User::factory()->onboarded()->create(); $otherUser = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - BankingConnection::factory()->create(['user_id' => $user->id]); BankingConnection::factory()->create(['user_id' => $otherUser->id]); @@ -51,8 +46,6 @@ test('connections page only shows own connections', function () { test('users can disconnect a banking connection and keep accounts as manual', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create(['user_id' => $user->id]); $account = Account::factory()->create([ 'user_id' => $user->id, @@ -93,8 +86,6 @@ test('users can disconnect a banking connection and keep accounts as manual', fu test('users can disconnect a banking connection and delete accounts', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create(['user_id' => $user->id]); $account = Account::factory()->create([ 'user_id' => $user->id, @@ -131,8 +122,6 @@ test('users can disconnect a banking connection and delete accounts', function ( test('deleting accounts requires confirmation text', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create(['user_id' => $user->id]); $response = $this->actingAs($user)->delete("/settings/connections/{$connection->id}", [ @@ -147,8 +136,6 @@ test('deleting accounts requires confirmation text', function () { test('users cannot disconnect another users connection', function () { $user = User::factory()->onboarded()->create(); $otherUser = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create(['user_id' => $otherUser->id]); $response = $this->actingAs($user)->delete("/settings/connections/{$connection->id}", [ @@ -162,8 +149,6 @@ test('users can trigger manual sync on active connection', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create([ 'user_id' => $user->id, 'status' => BankingConnectionStatus::Active, @@ -175,10 +160,26 @@ test('users can trigger manual sync on active connection', function () { $response->assertSessionHas('success'); }); +test('free tier users are redirected to subscribe when syncing a connection after onboarding', function () { + config(['subscriptions.enabled' => true]); + + Queue::fake(); + + $user = User::factory()->onboarded()->create(); + $connection = BankingConnection::factory()->create([ + 'user_id' => $user->id, + 'status' => BankingConnectionStatus::Active, + ]); + + $response = $this->actingAs($user)->post("/settings/connections/{$connection->id}/sync"); + + $response->assertRedirect(route('subscribe')); + + Queue::assertNothingPushed(); +}); + test('disconnecting indexa capital connection does not revoke session', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->indexaCapital()->create(['user_id' => $user->id]); $account = Account::factory()->create([ 'user_id' => $user->id, @@ -210,8 +211,6 @@ test('users cannot sync expired connection', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->expired()->create([ 'user_id' => $user->id, ]); @@ -226,8 +225,6 @@ test('users can update indexa capital credentials with valid token', function () Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->indexaCapital()->error()->create([ 'user_id' => $user->id, 'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.', @@ -252,12 +249,30 @@ test('users can update indexa capital credentials with valid token', function () expect($connection->api_token)->toBe('new-valid-indexa-token-12345'); }); +test('free tier users are redirected to subscribe when updating credentials after onboarding', function () { + config(['subscriptions.enabled' => true]); + + Queue::fake(); + + $user = User::factory()->onboarded()->create(); + $connection = BankingConnection::factory()->indexaCapital()->error()->create([ + 'user_id' => $user->id, + 'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.', + ]); + + $response = $this->actingAs($user)->patch("/settings/connections/{$connection->id}/credentials", [ + 'api_token' => 'new-valid-indexa-token-12345', + ]); + + $response->assertRedirect(route('subscribe')); + + Queue::assertNothingPushed(); +}); + test('users can update binance credentials with valid api key and secret', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->binance()->error()->create([ 'user_id' => $user->id, 'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.', @@ -288,8 +303,6 @@ test('users can update bitpanda credentials with valid api key', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->bitpanda()->error()->create([ 'user_id' => $user->id, 'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.', @@ -316,8 +329,6 @@ test('users can update bitpanda credentials with valid api key', function () { test('updating credentials with invalid token returns validation error', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->indexaCapital()->error()->create([ 'user_id' => $user->id, ]); @@ -338,8 +349,6 @@ test('updating credentials with invalid token returns validation error', functio test('cannot update credentials for enablebanking connection', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->create([ 'user_id' => $user->id, 'provider' => 'enablebanking', @@ -355,8 +364,6 @@ test('cannot update credentials for enablebanking connection', function () { test('cannot update credentials for another users connection', function () { $user = User::factory()->onboarded()->create(); $otherUser = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->indexaCapital()->create([ 'user_id' => $otherUser->id, ]); @@ -368,24 +375,28 @@ test('cannot update credentials for another users connection', function () { $response->assertForbidden(); }); -test('credential update requires feature flag', function () { +test('users can update credentials for their own connection', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->indexaCapital()->create([ 'user_id' => $user->id, ]); + Http::fake([ + 'api.indexacapital.com/users/me' => Http::response([ + 'accounts' => [], + ]), + ]); + $response = $this->actingAs($user)->patch("/settings/connections/{$connection->id}/credentials", [ 'api_token' => 'some-token-value-12345', ]); - $response->assertNotFound(); + $response->assertRedirect(); }); test('credential update validates required fields for binance', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $connection = BankingConnection::factory()->binance()->error()->create([ 'user_id' => $user->id, ]); @@ -400,8 +411,6 @@ test('credential update validates required fields for binance', function () { test('connections page includes provider and aspsp_name fields needed for frontend duplicate filtering', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - BankingConnection::factory()->create([ 'user_id' => $user->id, 'provider' => 'enablebanking', @@ -420,8 +429,6 @@ test('connections page includes provider and aspsp_name fields needed for fronte test('connections page includes connections from all provider types for frontend filtering', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - BankingConnection::factory()->create(['user_id' => $user->id, 'provider' => 'enablebanking', 'aspsp_name' => 'CaixaBank']); BankingConnection::factory()->binance()->create(['user_id' => $user->id]); BankingConnection::factory()->bitpanda()->create(['user_id' => $user->id]); diff --git a/tests/Feature/OpenBanking/IndexaCapitalControllerTest.php b/tests/Feature/OpenBanking/IndexaCapitalControllerTest.php index 2b9e777c..92ef5853 100644 --- a/tests/Feature/OpenBanking/IndexaCapitalControllerTest.php +++ b/tests/Feature/OpenBanking/IndexaCapitalControllerTest.php @@ -7,7 +7,6 @@ use App\Models\BankingConnection; use App\Models\User; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; -use Laravel\Pennant\Feature; beforeEach(function () { Bank::factory()->create([ @@ -21,8 +20,6 @@ test('users can connect an indexa capital account with valid token', function () Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.indexacapital.com/users/me' => Http::response([ 'accounts' => [ @@ -54,8 +51,6 @@ test('users can connect an indexa capital account with valid token', function () test('invalid token returns 422', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.indexacapital.com/users/me' => Http::response(['message' => 'Unauthorized'], 401), ]); @@ -73,20 +68,34 @@ test('invalid token returns 422', function () { ]); }); -test('requires open-banking feature flag', function () { +test('free tier users cannot connect an indexa capital account after onboarding when subscriptions are enabled', function () { + config(['subscriptions.enabled' => true]); + $user = User::factory()->onboarded()->create(); $response = $this->actingAs($user)->postJson('/open-banking/indexa-capital/connect', [ 'api_token' => 'valid-test-token-12345', ]); - $response->assertNotFound(); + $response->assertStatus(402); + $response->assertJson(['redirect' => route('subscribe')]); + + $this->assertDatabaseMissing('banking_connections', [ + 'user_id' => $user->id, + 'provider' => 'indexacapital', + ]); +}); + +test('indexa capital requires authentication', function () { + $response = $this->postJson('/open-banking/indexa-capital/connect', [ + 'api_token' => 'valid-test-token-12345', + ]); + + $response->assertUnauthorized(); }); test('api_token is required and must be at least 10 characters', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user)->postJson('/open-banking/indexa-capital/connect', []) ->assertUnprocessable() ->assertJsonValidationErrors(['api_token']); @@ -102,8 +111,6 @@ test('stores multiple pending accounts for multiple indexa portfolios', function Queue::fake(); $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.indexacapital.com/users/me' => Http::response([ 'accounts' => [ @@ -130,11 +137,11 @@ test('stores multiple pending accounts for multiple indexa portfolios', function }); test('indexa capital auto-creates accounts during onboarding', function () { + config(['subscriptions.enabled' => true]); + Queue::fake(); $user = User::factory()->notOnboarded()->create(); - Feature::for($user)->activate('open-banking'); - Http::fake([ 'api.indexacapital.com/users/me' => Http::response([ 'accounts' => [ diff --git a/tests/Feature/OpenBanking/InstitutionControllerTest.php b/tests/Feature/OpenBanking/InstitutionControllerTest.php index 2a0c8936..aa9f9995 100644 --- a/tests/Feature/OpenBanking/InstitutionControllerTest.php +++ b/tests/Feature/OpenBanking/InstitutionControllerTest.php @@ -2,11 +2,9 @@ use App\Contracts\BankingProviderInterface; use App\Models\User; -use Laravel\Pennant\Feature; -test('authenticated users with feature flag can list institutions', function () { +test('authenticated users can list institutions', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); $mockProvider = Mockery::mock(BankingProviderInterface::class); $mockProvider->shouldReceive('getInstitutions') @@ -28,7 +26,6 @@ test('authenticated users with feature flag can list institutions', function () test('institutions endpoint requires country parameter', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); $response = $this->actingAs($user)->getJson('/open-banking/institutions'); @@ -37,7 +34,6 @@ test('institutions endpoint requires country parameter', function () { test('institutions endpoint requires valid country code length', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); $response = $this->actingAs($user)->getJson('/open-banking/institutions?country=SPAIN'); diff --git a/tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php b/tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php index 02cc45e2..091165b4 100644 --- a/tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php +++ b/tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php @@ -1,114 +1,23 @@ onboarded()->create(); - - Feature::for($user)->deactivate('open-banking'); - - $response = $this->actingAs($user)->get('/open-banking/institutions?country=ES'); - - $response->assertNotFound(); +test('guests cannot access institutions route', function () { + $this->getJson('/open-banking/institutions?country=ES') + ->assertUnauthorized(); }); -test('users without open-banking feature get 404 on authorize', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->deactivate('open-banking'); - - $response = $this->actingAs($user)->post('/open-banking/authorize', [ +test('guests cannot access authorize route', function () { + $this->postJson('/open-banking/authorize', [ 'aspsp_name' => 'Test Bank', 'country' => 'ES', - ]); - - $response->assertNotFound(); + ])->assertUnauthorized(); }); -test('users without open-banking feature get 404 on callback', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->deactivate('open-banking'); - - $response = $this->actingAs($user)->get('/open-banking/callback?code=test'); - - $response->assertNotFound(); +test('guests are redirected away from callback route', function () { + $this->get('/open-banking/callback?code=test') + ->assertRedirect(route('login')); }); -test('users without open-banking feature get 404 on connections index', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->deactivate('open-banking'); - - $response = $this->actingAs($user)->get('/settings/connections'); - - $response->assertNotFound(); -}); - -test('open-banking feature flag is shared with frontend when enabled', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->activate('open-banking'); - - $response = $this->actingAs($user)->get('/dashboard'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.open-banking', true) - ); -}); - -test('open-banking feature flag is shared with frontend when disabled', function () { - $user = User::factory()->onboarded()->create(); - - Feature::for($user)->deactivate('open-banking'); - - $response = $this->actingAs($user)->get('/dashboard'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.open-banking', false) - ); -}); - -test('guests see open-banking feature as false', function () { - $response = $this->get('/'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.open-banking', false) - ); -}); - -test('open-banking feature flag is shared with frontend on onboarding page when enabled', function () { - $user = User::factory()->create([ - 'onboarded_at' => null, - 'encryption_salt' => 'test-salt', - ]); - - Feature::for($user)->activate('open-banking'); - - $response = $this->actingAs($user)->get('/onboarding'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.open-banking', true) - ); -}); - -test('open-banking feature flag is shared with frontend on onboarding page when disabled', function () { - $user = User::factory()->create([ - 'onboarded_at' => null, - 'encryption_salt' => 'test-salt', - ]); - - Feature::for($user)->deactivate('open-banking'); - - $response = $this->actingAs($user)->get('/onboarding'); - - $response->assertOk(); - $response->assertInertia(fn ($page) => $page - ->where('features.open-banking', false) - ); +test('guests are redirected away from connections index', function () { + $this->get('/settings/connections') + ->assertRedirect(route('login')); }); diff --git a/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php b/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php index 327a2ef6..13fde21c 100644 --- a/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php +++ b/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php @@ -515,7 +515,6 @@ test('scheduled sync excludes error connections with expired valid_until', funct test('manual sync resets consecutive sync failures', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); $connection = BankingConnection::factory()->error()->create([ 'user_id' => $user->id, diff --git a/tests/Feature/SubscriptionTest.php b/tests/Feature/SubscriptionTest.php index 9119bfcb..2db44b78 100644 --- a/tests/Feature/SubscriptionTest.php +++ b/tests/Feature/SubscriptionTest.php @@ -8,7 +8,6 @@ use App\Models\Category; use App\Models\Transaction; use App\Models\User; use Illuminate\Http\RedirectResponse; -use Laravel\Pennant\Feature; beforeEach(function () { config(['subscriptions.enabled' => true]); @@ -213,30 +212,24 @@ test('pricing config includes all plan details', function () { ); }); -test('open banking users without bank connections are redirected to paywall on first visit', function () { +test('users without bank connections are redirected to paywall on first visit', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user); $this->get(route('dashboard'))->assertRedirect(route('subscribe')); }); -test('open banking users without bank connections can access protected routes after seeing paywall', function () { +test('users without bank connections can access protected routes after seeing paywall', function () { $user = User::factory()->onboarded()->create(['paywall_seen_at' => now()]); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user); $this->get(route('dashboard'))->assertOk(); }); -test('open banking users with a bank connection are redirected to paywall', function () { +test('users with a bank connection are redirected to paywall', function () { $user = User::factory()->onboarded()->create(); - - Feature::for($user)->activate('open-banking'); BankingConnection::factory()->for($user)->create(); $this->actingAs($user); @@ -244,11 +237,9 @@ test('open banking users with a bank connection are redirected to paywall', func $this->get(route('dashboard'))->assertRedirect(route('subscribe')); }); -test('paywall shows canUseFreePlan true when open banking is active and no bank connected', function () { +test('paywall shows canUseFreePlan true when no bank is connected', function () { $user = User::factory()->onboarded()->create(); - Feature::for($user)->activate('open-banking'); - $this->actingAs($user); $this->get(route('subscribe')) @@ -259,10 +250,8 @@ test('paywall shows canUseFreePlan true when open banking is active and no bank ); }); -test('paywall shows canUseFreePlan false when open banking is active but user has a bank connection', function () { +test('paywall shows canUseFreePlan false when user has a bank connection', function () { $user = User::factory()->onboarded()->create(); - - Feature::for($user)->activate('open-banking'); BankingConnection::factory()->for($user)->create(); $this->actingAs($user); @@ -275,19 +264,6 @@ test('paywall shows canUseFreePlan false when open banking is active but user ha ); }); -test('paywall shows canUseFreePlan false when open banking is not active', function () { - $user = User::factory()->onboarded()->create(); - - $this->actingAs($user); - - $this->get(route('subscribe')) - ->assertOk() - ->assertInertia(fn ($page) => $page - ->component('subscription/paywall') - ->where('canUseFreePlan', false) - ); -}); - test('billing portal creates stripe customer when user has no stripe id', function () { $user = Mockery::mock(User::class)->shouldIgnoreMissing(); $user->shouldReceive('isDemoAccount')->andReturn(false); diff --git a/tests/Feature/WelcomeBanksOrderingTest.php b/tests/Feature/WelcomeBanksOrderingTest.php index dbfa0292..6460508d 100644 --- a/tests/Feature/WelcomeBanksOrderingTest.php +++ b/tests/Feature/WelcomeBanksOrderingTest.php @@ -5,7 +5,6 @@ use App\Models\Bank; use App\Models\BankingConnection; use App\Models\User; use Illuminate\Support\Facades\Cache; -use Laravel\Pennant\Feature; beforeEach(function () { Cache::forget('popular-banks'); @@ -13,7 +12,6 @@ beforeEach(function () { test('home popular banks are ordered by popularity and then Spain first', function () { $user = User::factory()->create(); - Feature::for($user)->activate('open-banking'); $mostPopularNonSpanish = Bank::factory()->create([ 'name' => 'Apex Banque', @@ -68,23 +66,22 @@ test('home popular banks are ordered by popularity and then Spain first', functi ); }); -test('home returns empty popular banks when open-banking feature is inactive', function () { - $user = User::factory()->create(); - // open-banking is inactive by default +test('home returns popular banks for unauthenticated visitors', function () { + $bank = Bank::factory()->create([ + 'name' => 'Public Bank', + 'logo' => 'https://example.com/public.png', + 'user_id' => null, + ]); + $connection = BankingConnection::factory()->create([ + 'aspsp_name' => $bank->name, + 'aspsp_country' => 'ES', + ]); + Account::factory()->for($bank)->for($connection, 'bankingConnection')->create(); - $this->actingAs($user)->get(route('home')) - ->assertOk() - ->assertInertia(fn ($page) => $page - ->component('welcome') - ->where('popularBanks', []) - ); -}); - -test('home returns empty popular banks for unauthenticated visitors', function () { $this->get(route('home')) ->assertOk() ->assertInertia(fn ($page) => $page ->component('welcome') - ->where('popularBanks', []) + ->where('popularBanks.0.name', $bank->name) ); }); diff --git a/tests/Pest.php b/tests/Pest.php index 91e495c0..dd782bd1 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -158,6 +158,8 @@ function createAccountViaUI($page, string $displayName, string $bankName, string { $page->assertSee('Bank accounts'); $page->click('Create Account') + ->waitForText('Manual', 5) + ->click('Manual') ->wait(0.5) ->fill('#display_name', $displayName) ->click('[data-testid="bank-select"]')