create([ 'name' => 'Interactive Brokers', 'user_id' => null, 'logo' => '/images/banks/logos/interactive-brokers.png', ]); }); function ibFakeFlex(array $accountIds = ['U1234567']): void { $statements = ''; foreach ($accountIds as $accountId) { $statements .= '' .'' .'' .''; } Http::fake([ '*SendRequest*' => Http::response('Success999'), '*GetStatement*' => Http::response(''.$statements.''), ]); } 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()); $response->assertOk(); $response->assertJsonStructure(['redirect_url', 'connection_id']); $connection = BankingConnection::where('user_id', $user->id)->where('provider', 'interactivebrokers')->first(); expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping); expect($connection->api_secret)->toBe('123456'); expect($connection->pending_accounts_data)->toHaveCount(1); expect($connection->pending_accounts_data[0]['uid'])->toBe('U1234567'); expect($connection->pending_accounts_data[0]['name'])->toBe('Interactive Brokers (U1234567)'); expect($connection->pending_accounts_data[0]['currency'])->toBe('USD'); Queue::assertNothingPushed(); }); test('invalid flex credentials return 422', function () { $user = User::factory()->onboarded()->create(); Feature::for($user)->activate(InteractiveBrokers::class); Http::fake([ '*SendRequest*' => Http::response('Fail1015Invalid token.'), ]); $response = $this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect()); $response->assertUnprocessable(); $this->assertDatabaseMissing('banking_connections', [ 'user_id' => $user->id, 'provider' => 'interactivebrokers', ]); }); test('free tier users cannot connect after onboarding when subscriptions are enabled', function () { 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); $response->assertJson(['redirect' => route('subscribe')]); }); 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']); }); test('auto-creates accounts during onboarding', function () { config(['subscriptions.enabled' => true]); 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()); $response->assertOk(); $response->assertJsonPath('redirect_url', route('onboarding', ['step' => 'create-account'])); $connection = BankingConnection::where('user_id', $user->id)->where('provider', 'interactivebrokers')->first(); expect($connection->status)->toBe(BankingConnectionStatus::Active); expect($connection->pending_accounts_data)->toBeNull(); $this->assertDatabaseHas('accounts', [ 'user_id' => $user->id, 'banking_connection_id' => $connection->id, 'external_account_id' => 'U1111111', 'type' => 'investment', ]); $this->assertDatabaseHas('accounts', [ 'user_id' => $user->id, 'banking_connection_id' => $connection->id, 'external_account_id' => 'U2222222', 'type' => 'investment', ]); Queue::assertPushed(SyncBankingConnectionJob::class); }); test('requires authentication', function () { $this->postJson('/open-banking/interactive-brokers/connect', [ 'token' => 'flex-token-1234567890', 'query_id' => '123456', ])->assertUnauthorized(); }); test('reports a friendly message while the statement is still generating', function () { Sleep::fake(); $user = User::factory()->onboarded()->create(); Feature::for($user)->activate(InteractiveBrokers::class); Http::fake([ '*SendRequest*' => Http::response('Success999'), '*GetStatement*' => Http::response('Warn1019Statement generation in progress. Please try again shortly.'), ]); $this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect()) ->assertUnprocessable() ->assertJsonFragment(['message' => 'Interactive Brokers is still preparing your statement. Please try again in a moment.']); $this->assertDatabaseMissing('banking_connections', [ 'user_id' => $user->id, 'provider' => 'interactivebrokers', ]); }); 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('Fail1018Too many requests have been made from this token.'), ]); $this->actingAs($user)->postJson('/open-banking/interactive-brokers/connect', ibConnect()) ->assertUnprocessable() ->assertJsonFragment(['message' => 'Interactive Brokers is rate limiting requests. Please wait a few minutes and try again.']); });