feat(banking): enable Interactive Brokers for all users (#593)
## What Removes the `InteractiveBrokers` Pennant feature flag. The integration is now always available to every user. ## Changes - Delete the `App\Features\InteractiveBrokers` flag class. - Drop the per-user `abort_unless(...->active(...))` gate from `InteractiveBrokersController`. - Stop sharing the `interactiveBrokers` flag in Inertia props (`HandleInertiaRequests`) and remove it from the `Features` type. - Remove the now-unused `feature` gating mechanism from the connect-provider registry and `useConnectFlow` — Interactive Brokers was its only consumer. - Update tests: drop the "blocked when flag off" case and all flag activations; the connect dialog now asserts IB is always offered. The Interactive Brokers integration itself (provider enum, client, syncer) is untouched. ## Testing - `php artisan test InteractiveBrokersControllerTest InertiaSharedDataTest` — 17 passed - `vitest connect-account-dialog` — 7 passed - `bun run lint` / `bun run format` — clean
This commit is contained in:
parent
f72e2a64ca
commit
094ff4d5ac
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Features;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
class InteractiveBrokers
|
||||
{
|
||||
/**
|
||||
* Resolve the feature's initial value.
|
||||
*
|
||||
* Off by default; enable per beta tester with
|
||||
* `php artisan feature:enable InteractiveBrokers user@example.com`
|
||||
* until the Flex integration is validated against a live account.
|
||||
*/
|
||||
public function resolve(?User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ namespace App\Http\Controllers\OpenBanking;
|
|||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Enums\BankingProvider;
|
||||
use App\Exceptions\Banking\TransientBankingProviderException;
|
||||
use App\Features\InteractiveBrokers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending;
|
||||
use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate;
|
||||
|
|
@ -17,7 +16,6 @@ use App\Services\Banking\InteractiveBrokersClient;
|
|||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class InteractiveBrokersController extends Controller
|
||||
{
|
||||
|
|
@ -32,8 +30,6 @@ class InteractiveBrokersController extends Controller
|
|||
$validated = $request->validated();
|
||||
$user = auth()->user();
|
||||
|
||||
abort_unless(Feature::for($user)->active(InteractiveBrokers::class), 403);
|
||||
|
||||
if ($this->shouldBlockOpenBankingAccess($user)) {
|
||||
return $this->subscribeJsonResponse();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Enums\BankingConnectionStatus;
|
|||
use App\Enums\BankingProvider;
|
||||
use App\Features\AiConsentSettings;
|
||||
use App\Features\CalculateBalancesOnImport;
|
||||
use App\Features\InteractiveBrokers;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Services\CurrencyOptions;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
|
|
@ -180,21 +179,18 @@ class HandleInertiaRequests extends Middleware
|
|||
return [
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => false,
|
||||
'interactiveBrokers' => false,
|
||||
'aiConsentSettings' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$features = Feature::for($user)->values([
|
||||
CalculateBalancesOnImport::class,
|
||||
InteractiveBrokers::class,
|
||||
AiConsentSettings::class,
|
||||
]);
|
||||
|
||||
return [
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => $features[CalculateBalancesOnImport::class] !== false,
|
||||
'interactiveBrokers' => $features[InteractiveBrokers::class] !== false,
|
||||
'aiConsentSettings' => $features[AiConsentSettings::class] !== false,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ globalThis.ResizeObserver ??= class {
|
|||
disconnect() {}
|
||||
};
|
||||
|
||||
const mockFeatures = { interactiveBrokers: false };
|
||||
|
||||
vi.mock('@inertiajs/react', () => ({
|
||||
usePage: () => ({ props: { features: mockFeatures } }),
|
||||
usePage: () => ({ props: { features: {} } }),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/i18n', () => ({
|
||||
|
|
@ -116,11 +114,9 @@ async function reachBankStep(
|
|||
describe('ConnectAccountDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockFeatures.interactiveBrokers = false;
|
||||
});
|
||||
|
||||
it('shows Interactive Brokers only when the feature flag is enabled', async () => {
|
||||
mockFeatures.interactiveBrokers = true;
|
||||
it('shows Interactive Brokers as a connectable provider', async () => {
|
||||
await reachBankStep([]);
|
||||
|
||||
expect(
|
||||
|
|
@ -128,14 +124,6 @@ describe('ConnectAccountDialog', () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides Interactive Brokers when the feature flag is disabled', async () => {
|
||||
await reachBankStep([]);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /Interactive Brokers/ }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps an already-connected bank selectable and badges it', async () => {
|
||||
await reachBankStep([liveBbvaConnection()]);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ import {
|
|||
isProviderComplete,
|
||||
} from '@/lib/connect-providers';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import type { SharedData } from '@/types';
|
||||
import type {
|
||||
BankingConnection,
|
||||
EnableBankingInstitution,
|
||||
} from '@/types/banking';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const CONNECT_COUNTRIES = [
|
||||
|
|
@ -48,7 +46,6 @@ export type ConnectStep = 'country' | 'bank' | 'confirm';
|
|||
* the components.
|
||||
*/
|
||||
export function useConnectFlow(connections: BankingConnection[]) {
|
||||
const { features } = usePage<SharedData>().props;
|
||||
const [step, setStep] = useState<ConnectStep>('country');
|
||||
const [country, setCountry] = useState('');
|
||||
const [institutions, setInstitutions] = useState<
|
||||
|
|
@ -144,7 +141,6 @@ export function useConnectFlow(connections: BankingConnection[]) {
|
|||
|
||||
const extraInstitutions = CONNECT_PROVIDERS.filter(
|
||||
(p) =>
|
||||
(!p.feature || features[p.feature]) &&
|
||||
(!p.onlyCountry || p.onlyCountry === countryCode) &&
|
||||
!hasLiveConnectionForProvider(
|
||||
connections,
|
||||
|
|
@ -175,7 +171,7 @@ export function useConnectFlow(connections: BankingConnection[]) {
|
|||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[connections, features],
|
||||
[connections],
|
||||
);
|
||||
|
||||
const handleAuthorize = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Input } from '@/components/ui/input';
|
|||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Features } from '@/types';
|
||||
import type { EnableBankingInstitution } from '@/types/banking';
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
||||
|
|
@ -41,8 +40,6 @@ export type ConnectProvider = {
|
|||
sendsCountry?: boolean;
|
||||
/** Only offered when connecting from this country (e.g. Indexa: ES). */
|
||||
onlyCountry?: string;
|
||||
/** Hidden unless this Pennant feature is active. */
|
||||
feature?: keyof Features;
|
||||
/** Confirm-step header copy (i18n key). */
|
||||
headerDescription: string;
|
||||
/** Confirm-card copy (i18n key). */
|
||||
|
|
@ -215,7 +212,6 @@ export const CONNECT_PROVIDERS: ConnectProvider[] = [
|
|||
maximum_consent_validity: null,
|
||||
},
|
||||
endpoint: '/open-banking/interactive-brokers/connect',
|
||||
feature: 'interactiveBrokers',
|
||||
headerDescription:
|
||||
'Enter your Flex Web Service token and Query ID to connect your Interactive Brokers account.',
|
||||
cardDescription:
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export interface NavDivider {
|
|||
export interface Features {
|
||||
cashflow: boolean;
|
||||
calculateBalancesOnImport: boolean;
|
||||
interactiveBrokers: boolean;
|
||||
aiConsentSettings: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ test('shared feature flags do not include coinbase flag', function () {
|
|||
expect($props['features'])->toBe([
|
||||
'cashflow' => true,
|
||||
'calculateBalancesOnImport' => false,
|
||||
'interactiveBrokers' => false,
|
||||
'aiConsentSettings' => false,
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Features\InteractiveBrokers;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
use App\Models\BankingConnection;
|
||||
|
|
@ -9,7 +8,6 @@ use App\Models\User;
|
|||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
beforeEach(function () {
|
||||
Bank::factory()->create([
|
||||
|
|
@ -41,23 +39,10 @@ function ibConnect(): array
|
|||
return ['token' => 'flex-token-1234567890', 'query_id' => '123456'];
|
||||
}
|
||||
|
||||
test('is blocked when the feature flag is off', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
$this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect())
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertDatabaseMissing('banking_connections', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'interactivebrokers',
|
||||
]);
|
||||
});
|
||||
|
||||
test('users can connect with valid flex credentials', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
ibFakeFlex();
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect());
|
||||
|
|
@ -79,8 +64,6 @@ test('users can connect with valid flex credentials', function () {
|
|||
|
||||
test('invalid flex credentials return 422', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
|
||||
Http::fake([
|
||||
'*SendRequest*' => Http::response('<FlexStatementResponse><Status>Fail</Status><ErrorCode>1015</ErrorCode><ErrorMessage>Invalid token.</ErrorMessage></FlexStatementResponse>'),
|
||||
]);
|
||||
|
|
@ -99,8 +82,6 @@ test('free tier users cannot connect after onboarding when subscriptions are ena
|
|||
config(['subscriptions.enabled' => true]);
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect());
|
||||
|
||||
$response->assertStatus(402);
|
||||
|
|
@ -109,8 +90,6 @@ test('free tier users cannot connect after onboarding when subscriptions are ena
|
|||
|
||||
test('token and query_id are required', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
|
||||
$this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', [])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['token', 'query_id']);
|
||||
|
|
@ -121,7 +100,6 @@ test('auto-creates accounts during onboarding', function () {
|
|||
Queue::fake();
|
||||
|
||||
$user = User::factory()->notOnboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
ibFakeFlex(['U1111111', 'U2222222']);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect());
|
||||
|
|
@ -161,8 +139,6 @@ test('reports a friendly message while the statement is still generating', funct
|
|||
Sleep::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
|
||||
Http::fake([
|
||||
'*SendRequest*' => Http::response('<FlexStatementResponse><Status>Success</Status><ReferenceCode>999</ReferenceCode></FlexStatementResponse>'),
|
||||
'*GetStatement*' => Http::response('<FlexStatementResponse><Status>Warn</Status><ErrorCode>1019</ErrorCode><ErrorMessage>Statement generation in progress. Please try again shortly.</ErrorMessage></FlexStatementResponse>'),
|
||||
|
|
@ -180,8 +156,6 @@ test('reports a friendly message while the statement is still generating', funct
|
|||
|
||||
test('reports a rate-limit message when IB throttles the request', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate(InteractiveBrokers::class);
|
||||
|
||||
Http::fake([
|
||||
'*SendRequest*' => Http::response('<FlexStatementResponse><Status>Fail</Status><ErrorCode>1018</ErrorCode><ErrorMessage>Too many requests have been made from this token.</ErrorMessage></FlexStatementResponse>'),
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue