validated(); $user = auth()->user(); abort_unless(Feature::for($user)->active(InteractiveBrokers::class), 403); if ($this->shouldBlockOpenBankingAccess($user)) { return $this->subscribeJsonResponse(); } $client = new InteractiveBrokersClient($validated['token'], $validated['query_id']); try { $accounts = $client->fetchStatement(); } catch (\Throwable $e) { Log::warning('Interactive Brokers connection validation failed', ['error' => $e->getMessage()]); return response()->json([ 'message' => $this->connectErrorMessage($e), ], 422); } if (empty($accounts)) { return response()->json([ 'message' => 'No accounts found in the Flex statement. Check that your Flex Query includes the NAV section.', ], 422); } $bank = Bank::firstOrCreate( ['name' => 'Interactive Brokers', 'user_id' => null], ['name' => 'Interactive Brokers', 'logo' => '/images/banks/logos/interactive-brokers.png'], ); $connection = $user->bankingConnections()->create([ 'provider' => BankingProvider::InteractiveBrokers, ...BankingProvider::InteractiveBrokers->credentialColumns($validated), 'aspsp_name' => 'Interactive Brokers', 'aspsp_country' => 'US', 'aspsp_logo' => $bank->logo, 'status' => BankingConnectionStatus::Pending, ]); $connection->update([ 'status' => BankingConnectionStatus::AwaitingMapping, 'pending_accounts_data' => $this->buildPendingAccounts($accounts), ]); if (! $user->isOnboarded()) { $this->createAccountsFromPending($user, $connection, $accountUserCurrencyService); SyncBankingConnectionJob::dispatch($connection); return response()->json([ 'redirect_url' => route('onboarding', ['step' => 'create-account']), 'connection_id' => $connection->id, ]); } return response()->json([ 'redirect_url' => route('open-banking.map-accounts', $connection), 'connection_id' => $connection->id, ]); } /** * Turn a Flex failure into a message the user can act on: bad credentials, * a busy/rate-limited service, or a statement that is still generating. */ private function connectErrorMessage(\Throwable $e): string { if ($e instanceof RequestException && in_array($e->response->status(), [401, 403], true)) { return 'Invalid Flex token or query ID, or failed to connect to Interactive Brokers.'; } if ($e instanceof RequestException && $e->response->status() === 429) { return 'Interactive Brokers is rate limiting requests. Please wait a few minutes and try again.'; } if ($e instanceof TransientBankingProviderException) { return 'Interactive Brokers is still preparing your statement. Please try again in a moment.'; } return 'Invalid Flex token or query ID, or failed to connect to Interactive Brokers.'; } /** * Build pending accounts from the parsed Flex statement. * * @param array, investedAmount: float|null}> $accounts * @return array */ private function buildPendingAccounts(array $accounts): array { $pending = []; foreach ($accounts as $account) { $pending[] = [ 'uid' => $account['account_id'], 'currency' => $account['currency'] !== '' ? $account['currency'] : 'USD', 'name' => "Interactive Brokers ({$account['account_id']})", ]; } return $pending; } }