Skip to main content

Banking Integration

Thrust подключается к 3,400+ европейским банкам через два Open Banking провайдера: EnableBanking (primary) и Tink by Visa (secondary).
Переключение между провайдерами — одна строка кода в BankProviderManager.activeProviderType. Это позволяет мгновенно выпустить hotfix при падении одного из провайдеров.

Архитектура

Оба провайдера реализуют единый протокол BankDataProvider:
// Modules/Finance/BankDataProvider.swift
protocol BankDataProvider {
    var providerType: BankProviderType { get }

    func fetchInstitutions(countryCode: String) async throws -> [BankInstitution]
    func createAuthorizationLink(institutionId: String, institutionName: String?, country: String?) async throws -> BankAuthorizationResult
    func exchangeCallback(params: [String: String]) async throws -> BankSessionResult
    func fetchAccountDetails(accountId: String, sessionId: String) async throws -> ProviderAccount
    func fetchAccountBalances(accountId: String, sessionId: String) async throws -> [ProviderBalance]
    func fetchAccountTransactions(accountId: String, sessionId: String, dateFrom: Date?) async throws -> [ProviderTransaction]
}

Провайдеры

enum BankProviderType: String, Codable {
    case goCardless = "goCardless"      // deprecated, нет доступа к API
    case enableBanking = "enableBanking" // primary
    case tink = "tink"                   // secondary
}

BankProviderManager

Фабрика/роутер, управляющий активным провайдером:
// Modules/Finance/BankProviderManager.swift
@MainActor
final class BankProviderManager: ObservableObject {
    static let shared = BankProviderManager()

    // Переключение провайдера — одна строка:
    @Published var activeProviderType: BankProviderType = .enableBanking

    var activeProvider: any BankDataProvider {
        provider(for: activeProviderType)
    }
}

EnableBanking

Что такое EnableBanking?

EnableBanking — PSD2 Open Banking платформа с прямыми подключениями к банкам (без промежуточных агрегаторов). Характеристики:
  • 2,000+ банков в EU
  • JWT-аутентификация (PEM key)
  • Бесплатный тариф до определённого объёма
  • Транзакции возвращают только дату (YYYY-MM-DD), без времени

API

Base URL: https://api.enablebanking.com/

Endpoints:
- GET  /aspsps?country={code}           # Список банков
- POST /sessions                         # Создание сессии (авторизация)
- GET  /sessions/{id}                    # Статус сессии
- GET  /accounts/{id}                    # Информация о счёте
- GET  /accounts/{id}/balances           # Балансы
- GET  /accounts/{id}/transactions       # Транзакции

Auth Flow

1. JWT подписывается PEM ключом (RS256)
2. POST /sessions → redirect URL для банка
3. Пользователь авторизуется в банке через SFSafariViewController
4. Callback: thrust://oauth_callback?code=AUTH_CODE
5. GET /sessions/{code} → список счетов
6. GET /accounts/{id}/transactions → транзакции

Ключи

KeyОписание
ENABLEBANKING_APPLICATION_IDApplication ID из EnableBanking Console
ENABLEBANKING_PEM_KEYRSA Private Key (PEM) для JWT подписи

Tink (Visa Open Banking)

Что такое Tink?

Tink — платформа Open Banking от Visa. Лучшее качество данных среди всех PSD2 провайдеров. Характеристики:
  • 3,400+ банков в 18 EU+UK странах
  • OAuth2 аутентификация (client_credentials + authorization_code)
  • ML-enrichment: категоризация, merchant names, logos
  • Транзакции возвращают дату и время (ISO 8601 bookedDateTime)
  • ~0.50 EUR/user/month

API

Base URL: https://api.tink.com/

OAuth:
- POST /api/v1/oauth/token               # Client & User tokens
- POST /api/v1/user/create                # Создание пользователя
- POST /api/v1/oauth/authorization-grant  # Authorization grant

Data:
- GET  /api/v1/providers?market={code}    # Список банков
- GET  /data/v2/accounts                  # Счета
- GET  /data/v2/transactions              # Транзакции (с пагинацией)

Auth Flow

1. POST /api/v1/oauth/token (client_credentials) → client_access_token (30 мин)
2. POST /api/v1/user/create → user_id
3. POST /api/v1/oauth/authorization-grant → authorization_code
4. Open Tink Link URL → SFSafariViewController
   https://link.tink.com/1.0/transactions/connect-accounts
     ?client_id=...&redirect_uri=thrust://oauth_callback
     &authorization_code=...&market=PL
5. Callback: thrust://oauth_callback?code=USER_AUTH_CODE
6. POST /api/v1/oauth/token (authorization_code) → user_access_token + refresh_token
7. GET /data/v2/accounts → список счетов
8. GET /data/v2/transactions → транзакции с bookedDateTime

Amount Format

Tink использует специальный формат для сумм:
{
  "currencyCode": "EUR",
  "value": {
    "scale": "2",
    "unscaledValue": "-2500"
  }
}
Конверсия: Double(unscaledValue) / pow(10, Double(scale)) = -25.00

Token Management

ТокенХранениеTTL
Client tokenMemory (TinkAPIClient)30 мин
User access tokenKeychain (SecureTokenStore)~1 час
User refresh tokenKeychain (SecureTokenStore)Длительный

Ключи

KeyОписание
TINK_CLIENT_IDClient ID из console.tink.com
TINK_CLIENT_SECRETClient Secret из console.tink.com

OAuth Callback Routing

Оба провайдера используют один redirect URI: thrust://oauth_callback. Различение по параметру pendingProvider:
// BankProviderManager.swift
func detectProvider(from url: URL) -> BankProviderType? {
    let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?
        .queryItems?.reduce(into: [:]) { $0[$1.name] = $1.value } ?? [:]

    // GoCardless — уникальные параметры
    if params["ref"] != nil || params["reference"] != nil {
        return .goCardless
    }

    // code= → проверяем pendingProvider (EnableBanking или Tink)
    if params["code"] != nil {
        if let pending = UserDefaults.standard.string(forKey: "bank_pending_provider"),
           let type = BankProviderType(rawValue: pending) {
            return type
        }
    }

    return activeProviderType // fallback
}
Каждый провайдер вызывает setPendingProvider() перед открытием авторизации, чтобы callback вернулся к правильному обработчику.

Сервисы

Файловая структура

Modules/Finance/
├── BankDataProvider.swift          # Протокол
├── BankProviderManager.swift       # Фабрика/роутер
├── GoCardlessProvider.swift        # GoCardless (deprecated)
├── EnableBanking/
│   ├── EnableBankingAPIClient.swift # HTTP клиент
│   ├── EnableBankingModels.swift    # Codable модели
│   └── EnableBankingProvider.swift  # BankDataProvider adapter
└── Tink/
    ├── TinkAPIClient.swift         # HTTP клиент (OAuth2)
    ├── TinkModels.swift            # Codable модели
    └── TinkProvider.swift          # BankDataProvider adapter

Процесс подключения банка

1. Выбор банка

Пользователь выбирает страну → запрос списка банков через активный провайдер:
let institutions = try await BankProviderManager.shared
    .activeProvider
    .fetchInstitutions(countryCode: "PL")

2. Авторизация

Создание ссылки для авторизации в банке:
let result = try await BankProviderManager.shared
    .activeProvider
    .createAuthorizationLink(
        institutionId: selectedBank.id,
        institutionName: selectedBank.name,
        country: "PL"
    )

// result.authURL — URL для SFSafariViewController

3. Callback

После авторизации банк редиректит на thrust://oauth_callback:
// AppDelegate / onOpenURL
func handleBankCallback(_ url: URL) {
    let provider = BankProviderManager.shared.detectProvider(from: url)
    let params = extractParams(from: url)

    let session = try await BankProviderManager.shared
        .provider(for: provider)
        .exchangeCallback(params: params)
}

4. Синхронизация

for accountId in session.accountIds {
    let transactions = try await provider
        .fetchAccountTransactions(
            accountId: accountId,
            sessionId: session.sessionId,
            dateFrom: Calendar.current.date(byAdding: .day, value: -90, to: Date())
        )

    await saveTransactions(transactions, for: accountId)
}

Детекция дубликатов

// Services/TransactionDeduplicator.swift
class TransactionDeduplicator {
    func findDuplicates(for transaction: Transaction) async -> [Transaction] {
        let hash = generateHash(transaction)
        let candidates = try? modelContext.fetch(
            FetchDescriptor<Transaction>(
                predicate: #Predicate { $0.contentHash == hash }
            )
        )
        return candidates?.filter {
            calculateConfidence($0, transaction) > 0.8
        } ?? []
    }
}

Безопасность

Хранение секретов

  • API ключи: Secrets.plistSecretsProviderAPIKeys
  • OAuth токены: Keychain через SecureTokenStore
  • PEM ключи: Secrets.plist (не коммитится в git)

Certificate Pinning

// Security/CertificatePinningManager.swift
// Все HTTP запросы проходят через pinned URLSession
let session = CertificatePinningManager.shared.createPinnedSession()

Ghost Mode

При активном Ghost Mode все сетевые запросы блокируются:
guard !PrivacyManager.shared.isGhostModeActive else {
    throw APIError.custom("Network access is disabled in Ghost Mode")
}

Сравнение провайдеров

EnableBankingTink
Банки~2,000 EU~3,400 EU+UK
АутентификацияJWT (PEM key)OAuth2
Дата транзакцийТолько дата (YYYY-MM-DD)Дата + время (ISO 8601)
EnrichmentНетML категоризация, merchant names
СтоимостьБесплатный тариф~0.50 EUR/user/month
ПровайдерНезависимыйVisa

Переключение провайдера

Для переключения (например, при hotfix):
// BankProviderManager.swift:13
@Published var activeProviderType: BankProviderType = .enableBanking
// Меняем на:
@Published var activeProviderType: BankProviderType = .tink
Существующие подключения продолжат работать через свой провайдер (хранится в bankProviderRaw каждого счёта). Переключение влияет только на новые подключения.

Следующие шаги

Transactions

Управление транзакциями

Accounts

Управление счетами

Security

Certificate Pinning

Architecture

Архитектура приложения