bankingConnection->id; } public function handle(TransactionSyncService $transactionSync, BalanceSyncService $balanceSync): void { $connection = $this->bankingConnection; if ($connection->isEnableBanking() && $connection->isExpired()) { $connection->update(['status' => BankingConnectionStatus::Expired]); Log::info('Banking connection expired, skipping sync', ['connection_id' => $connection->id]); return; } if (! $connection->isActive()) { return; } try { $isFirstSync = ! $connection->last_synced_at || $this->fullSync; if ($connection->isIndexaCapital()) { $this->syncIndexaCapital($connection, $isFirstSync); } elseif ($connection->isBinance()) { $this->syncBinance($connection, $isFirstSync); } elseif ($connection->isBitpanda()) { $this->syncBitpanda($connection); } else { $this->syncEnableBanking($connection, $transactionSync, $balanceSync, $isFirstSync); } $connection->update([ 'last_synced_at' => now(), 'error_message' => null, ]); } catch (\Throwable $e) { Log::error('Banking sync failed', [ 'connection_id' => $connection->id, 'error' => $e->getMessage(), ]); $connection->update([ 'status' => BankingConnectionStatus::Error, 'error_message' => $this->friendlyErrorMessage($e), ]); if ($this->isAuthError($e) && $this->isApiKeyProvider($connection) && $this->attempts() >= $this->tries) { Mail::to($connection->user)->send(new BankingConnectionAuthFailedEmail( $connection->user, $connection, )); } throw $e; } } private function syncIndexaCapital(BankingConnection $connection, bool $isFirstSync): void { $client = new IndexaCapitalClient($connection->api_token); $syncService = new IndexaCapitalBalanceSyncService; $connection->load('accounts'); foreach ($connection->accounts as $account) { $syncService->sync($account, $client, $isFirstSync); } } private function syncBinance(BankingConnection $connection, bool $isFirstSync): void { $client = new BinanceClient($connection->api_token, $connection->api_secret); $syncService = app(BinanceBalanceSyncService::class); $connection->load('accounts'); foreach ($connection->accounts as $account) { if ($isFirstSync) { $syncService->syncCurrentBalance($account, $client); SyncBinanceHistoricalBalancesJob::dispatch($account)->delay(now()->addSeconds(30)); } else { $syncService->sync($account, $client, isFirstSync: false); } } } private function syncBitpanda(BankingConnection $connection): void { $client = new BitpandaClient($connection->api_token); $syncService = app(BitpandaBalanceSyncService::class); $connection->load('accounts'); foreach ($connection->accounts as $account) { $syncService->sync($account, $client); } } private function syncEnableBanking(BankingConnection $connection, TransactionSyncService $transactionSync, BalanceSyncService $balanceSync, bool $isFirstSync): void { $dateFrom = $isFirstSync ? now()->subYear()->toDateString() : ($connection->last_synced_at?->toDateString() ?? now()->subMonth()->toDateString()); $dateTo = now()->toDateString(); $strategy = $isFirstSync ? 'longest' : null; $transactionsPerBank = []; $connection->load('accounts.bank'); foreach ($connection->accounts as $account) { if ($account->isLinked()) { $lastTransaction = $account->transactions() ->latest('transaction_date') ->first(); $linkedDateFrom = $lastTransaction ? $lastTransaction->transaction_date->toDateString() : $dateFrom; $created = $transactionSync->sync($account, $linkedDateFrom, $dateTo, $strategy, saveDailyBalances: false); $balanceSync->sync($account); } else { $created = $transactionSync->sync($account, $dateFrom, $dateTo, $strategy); $balanceSync->sync($account); if ($isFirstSync) { $balanceSync->calculateHistoricalBalances($account); } } if ($created > 0) { $bankName = $account->bank->name ?? __('Unknown Bank'); $transactionsPerBank[$bankName] = ($transactionsPerBank[$bankName] ?? 0) + $created; } } $totalTransactions = array_sum($transactionsPerBank); if (! $isFirstSync && $totalTransactions > 0) { Mail::to($connection->user)->send(new BankTransactionsSyncedEmail( $connection->user, $totalTransactions, $transactionsPerBank, )); } } private function friendlyErrorMessage(\Throwable $e): string { if ($e instanceof RequestException) { $status = $e->response->status(); return match (true) { $status === 429 => __('Rate limit exceeded. Please wait a few minutes and try again.'), $status === 401 || $status === 403 => __('Authentication failed. Your credentials may have expired or been revoked.'), $status >= 500 => __('The provider is experiencing issues. Please try again later.'), default => __('Failed to sync with the provider. Please try again later.'), }; } return __('An unexpected error occurred during sync. Please try again later.'); } private function isAuthError(\Throwable $e): bool { return $e instanceof RequestException && in_array($e->response->status(), [401, 403]); } private function isApiKeyProvider(BankingConnection $connection): bool { return $connection->isIndexaCapital() || $connection->isBinance() || $connection->isBitpanda(); } }