class EnableBankingAccount < ApplicationRecord include CurrencyNormalizable, Encryptable # Encrypt raw payloads if ActiveRecord encryption is configured if encryption_ready? encrypts :raw_payload encrypts :raw_transactions_payload end belongs_to :enable_banking_item # New association through account_providers has_one :account_provider, as: :provider, dependent: :destroy has_one :account, through: :account_provider, source: :account has_one :linked_account, through: :account_provider, source: :account validates :name, :currency, presence: true validates :uid, presence: true, uniqueness: { scope: :enable_banking_item_id } # account_id is not uniquely scoped: uid already enforces one-account-per-identifier per item # Helper to get account using account_providers system def current_account account end # Returns the API account ID (UUID) for Enable Banking API calls # The Enable Banking API requires a valid UUID for balance/transaction endpoints # Falls back to raw_payload["uid"] for existing accounts that have the wrong account_id stored def api_account_id # Check if account_id looks like a valid UUID (not an identification_hash) if account_id.present? && account_id.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i) account_id else # Fall back to raw_payload for existing accounts with incorrect account_id raw_payload&.dig("uid") || account_id || uid end end # Map PSD2 cash_account_type codes to user-friendly names # Based on ISO 20022 External Cash Account Type codes def account_type_display return nil unless account_type.present? type_mappings = { "CACC" => "Current/Checking Account", "SVGS" => "Savings Account", "CARD" => "Card Account", "CRCD" => "Credit Card", "LOAN" => "Loan Account", "MORT" => "Mortgage Account", "ODFT" => "Overdraft Account", "CASH" => "Cash Account", "TRAN" => "Transacting Account", "SALA" => "Salary Account", "MOMA" => "Money Market Account", "NREX" => "Non-Resident External Account", "TAXE" => "Tax Account", "TRAS" => "Cash Trading Account", "ONDP" => "Overnight Deposit" } type_mappings[account_type.upcase] || account_type.titleize end CASH_ACCOUNT_TYPE_MAP = { "CACC" => { type: "Depository", subtype: "checking" }, "SVGS" => { type: "Depository", subtype: "savings" }, "CARD" => { type: "CreditCard", subtype: "credit_card" }, "CRCD" => { type: "CreditCard", subtype: "credit_card" }, "LOAN" => { type: "Loan", subtype: nil }, "MORT" => { type: "Loan", subtype: "mortgage" }, "ODFT" => { type: "Depository", subtype: "checking" }, "TRAN" => { type: "Depository", subtype: "checking" }, "SALA" => { type: "Depository", subtype: "checking" }, "MOMA" => { type: "Depository", subtype: "savings" }, "NREX" => { type: "Depository", subtype: "checking" }, "TAXE" => { type: "Depository", subtype: "checking" }, "TRAS" => { type: "Depository", subtype: "checking" }, "ONDP" => { type: "Depository", subtype: "savings" }, "CASH" => { type: "Depository", subtype: "checking" }, "OTHR" => nil }.freeze def suggested_account_type CASH_ACCOUNT_TYPE_MAP[account_type&.upcase]&.dig(:type) end def suggested_subtype CASH_ACCOUNT_TYPE_MAP[account_type&.upcase]&.dig(:subtype) end def upsert_enable_banking_snapshot!(account_snapshot) snapshot = account_snapshot.with_indifferent_access raw_account_id = snapshot[:account_id] account_id_data = if raw_account_id.is_a?(Hash) raw_account_id elsif raw_account_id.is_a?(Array) && raw_account_id.first.is_a?(Hash) raw_account_id.find { |item| item[:iban].present? } || {} else {} end credit_limit_amount = snapshot.dig(:credit_limit, :amount) update!( current_balance: nil, currency: parse_currency(snapshot[:currency]) || "EUR", name: build_account_name(snapshot), account_id: snapshot[:uid], uid: snapshot[:identification_hash] || snapshot[:uid], iban: account_id_data[:iban] || snapshot[:iban], account_type: snapshot[:cash_account_type] || snapshot[:account_type], account_status: "active", provider: "enable_banking", product: snapshot[:product], credit_limit: parse_decimal_safe(credit_limit_amount), identification_hashes: snapshot[:identification_hashes] || [], institution_metadata: { name: enable_banking_item&.aspsp_name, aspsp_name: enable_banking_item&.aspsp_name, bic: snapshot.dig(:account_servicer, :bic_fi), servicer_name: snapshot.dig(:account_servicer, :name) }.compact, raw_payload: account_snapshot ) end def upsert_enable_banking_transactions_snapshot!(transactions_snapshot) assign_attributes( raw_transactions_payload: transactions_snapshot ) save! end private def build_account_name(snapshot) # Try to build a meaningful name from the account data raw_account_id = snapshot[:account_id] account_id_data = if raw_account_id.is_a?(Hash) raw_account_id elsif raw_account_id.is_a?(Array) && raw_account_id.first.is_a?(Hash) raw_account_id.find { |item| item[:iban].present? } || {} else {} end iban = account_id_data[:iban] || snapshot[:iban] if snapshot[:name].present? snapshot[:name] elsif iban.present? # Use last 4 digits of IBAN for privacy "Account ...#{iban[-4..]}" else "Enable Banking Account" end end def log_invalid_currency(currency_value) Rails.logger.warn("Invalid currency code '#{currency_value}' for EnableBanking account #{id}, defaulting to EUR") end def parse_decimal_safe(value) return nil if value.blank? BigDecimal(value.to_s) rescue ArgumentError, TypeError nil end end