mirror of
https://github.com/we-promise/sure.git
synced 2026-07-20 00:35:22 +00:00
Decoupled/MFA banks (e.g. VR Bank in Holstein) were hard-blocked because the authorize flow aborted whenever auth_methods[0] was DECOUPLED. Enable Banking's hosted /auth page actually coordinates decoupled SCA and redirects back with a code, so route these banks through it instead: - Provider#start_authorization accepts and forwards an auth_method param - EnableBankingItem#select_auth_method picks the best method (REDIRECT > DECOUPLED > EMBEDDED), filtering by psu_type and skipping hidden methods - Shared begin_authorization! re-fetches ASPSP metadata on each authorize and reauthorize, so the method is always re-derived (no persistence required) - Remove the DECOUPLED block in the controller Also stop the integration from constantly reporting "session expired": - Only a session-level GET /sessions 401/404 flips the connection to requires_update; per-account 401/404 are retried and no longer kill the whole connection - Reconcile session_expires_at from the API's access.valid_until on every sync - Treat an expired session as a graceful requires_update state instead of raising a bare error No schema changes. Adds covering tests.
92 lines
3.9 KiB
Ruby
92 lines
3.9 KiB
Ruby
class EnableBankingItem::Syncer
|
|
include SyncStats::Collector
|
|
|
|
attr_reader :enable_banking_item
|
|
|
|
def initialize(enable_banking_item)
|
|
@enable_banking_item = enable_banking_item
|
|
end
|
|
|
|
def perform_sync(sync)
|
|
# An expired/missing session is an expected state that needs user action, not a
|
|
# hard failure. Mark the connection requires_update and finish the sync
|
|
# gracefully so the UI surfaces the "Reconnect" CTA instead of a red sync error.
|
|
unless enable_banking_item.session_valid?
|
|
sync.update!(status_text: "Session expired - re-authorization required") if sync.respond_to?(:status_text)
|
|
enable_banking_item.update!(status: :requires_update)
|
|
collect_health_stats(sync, errors: nil)
|
|
return
|
|
end
|
|
|
|
# Phase 1: Import data from Enable Banking API
|
|
sync.update!(status_text: "Importing accounts from Enable Banking...") if sync.respond_to?(:status_text)
|
|
import_result = enable_banking_item.import_latest_enable_banking_data
|
|
|
|
unless import_result[:success]
|
|
# A session-level auth failure detected mid-import flips the item to
|
|
# requires_update — surface that as a graceful reconnect state, not a red
|
|
# error. Transient/per-account failures leave status good and fall through
|
|
# to a normal sync error that retries next time.
|
|
if enable_banking_item.requires_update?
|
|
sync.update!(status_text: "Re-authorization required") if sync.respond_to?(:status_text)
|
|
collect_health_stats(sync, errors: nil)
|
|
return
|
|
end
|
|
|
|
error_msg = import_result[:error]
|
|
if error_msg.blank? && (import_result[:accounts_failed].to_i > 0 || import_result[:transactions_failed].to_i > 0)
|
|
parts = []
|
|
parts << "#{import_result[:accounts_failed]} #{'account'.pluralize(import_result[:accounts_failed])} failed" if import_result[:accounts_failed].to_i > 0
|
|
parts << "#{import_result[:transactions_failed]} #{'transaction'.pluralize(import_result[:transactions_failed])} failed" if import_result[:transactions_failed].to_i > 0
|
|
error_msg = parts.join(", ")
|
|
end
|
|
raise StandardError.new(error_msg.presence || "Import failed")
|
|
end
|
|
|
|
# Phase 2: Check account setup status and collect sync statistics
|
|
sync.update!(status_text: "Checking account configuration...") if sync.respond_to?(:status_text)
|
|
collect_setup_stats(sync, provider_accounts: enable_banking_item.enable_banking_accounts.includes(:account_provider, :account))
|
|
|
|
unlinked_accounts = enable_banking_item.enable_banking_accounts.left_joins(:account_provider).where(account_providers: { id: nil })
|
|
|
|
if unlinked_accounts.any?
|
|
enable_banking_item.update!(pending_account_setup: true)
|
|
sync.update!(status_text: "#{unlinked_accounts.count} accounts need setup...") if sync.respond_to?(:status_text)
|
|
else
|
|
enable_banking_item.update!(pending_account_setup: false)
|
|
end
|
|
|
|
# Phase 3: Process transactions for linked and visible accounts only
|
|
linked_account_ids = enable_banking_item.enable_banking_accounts
|
|
.joins(:account_provider)
|
|
.joins(:account)
|
|
.merge(Account.visible)
|
|
.pluck("accounts.id")
|
|
|
|
if linked_account_ids.any?
|
|
sync.update!(status_text: "Processing transactions...") if sync.respond_to?(:status_text)
|
|
enable_banking_item.process_accounts
|
|
|
|
# Collect transaction statistics
|
|
collect_transaction_stats(sync, account_ids: linked_account_ids, source: "enable_banking")
|
|
|
|
# Phase 4: Schedule balance calculations for linked accounts
|
|
sync.update!(status_text: "Calculating balances...") if sync.respond_to?(:status_text)
|
|
enable_banking_item.schedule_account_syncs(
|
|
parent_sync: sync,
|
|
window_start_date: sync.window_start_date,
|
|
window_end_date: sync.window_end_date
|
|
)
|
|
end
|
|
|
|
collect_health_stats(sync, errors: nil)
|
|
rescue => e
|
|
collect_health_stats(sync, errors: [ { message: e.message, category: "sync_error" } ])
|
|
raise
|
|
end
|
|
|
|
def perform_post_sync
|
|
# no-op
|
|
end
|
|
end
|