mirror of
https://github.com/we-promise/sure.git
synced 2026-04-12 16:47:22 +00:00
* feat(enable-banking): enhance transaction import, metadata handling, and UI * fix(enable-banking): address security, sync edge cases and PR feedback * fix(enable-banking): resolve silent failures, auth overrides, and sync logic bugs * fix(enable-banking): resolve sync logic bugs, trailing whitespaces, and apply safe_psu_headers * test(enable-banking): mock set_current_balance to return success result * fix(budget): properly filter pending transactions and classify synced loan payments * style: fix trailing whitespace detected by rubocop * refactor: address code review feedback for Enable Banking sync and reporting --------- Signed-off-by: Louis <contact@boul2gom.com> Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
223 lines
7.9 KiB
Ruby
223 lines
7.9 KiB
Ruby
require "digest/md5"
|
|
|
|
class EnableBankingEntry::Processor
|
|
include CurrencyNormalizable
|
|
|
|
# enable_banking_transaction is the raw hash fetched from Enable Banking API
|
|
# Transaction structure from Enable Banking:
|
|
# {
|
|
# transaction_id, entry_reference, booking_date, value_date, transaction_date,
|
|
# transaction_amount: { amount, currency },
|
|
# creditor_name, debtor_name, remittance_information, ...
|
|
# }
|
|
def initialize(enable_banking_transaction, enable_banking_account:, import_adapter: nil)
|
|
@enable_banking_transaction = enable_banking_transaction
|
|
@enable_banking_account = enable_banking_account
|
|
@import_adapter = import_adapter
|
|
end
|
|
|
|
def process
|
|
unless account.present?
|
|
Rails.logger.warn "EnableBankingEntry::Processor - No linked account for enable_banking_account #{enable_banking_account.id}, skipping transaction #{external_id}"
|
|
return nil
|
|
end
|
|
|
|
begin
|
|
import_adapter.import_transaction(
|
|
external_id: external_id,
|
|
amount: amount,
|
|
currency: currency,
|
|
date: date,
|
|
name: name,
|
|
source: "enable_banking",
|
|
merchant: merchant,
|
|
notes: notes,
|
|
extra: extra
|
|
)
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "EnableBankingEntry::Processor - Validation error for transaction #{external_id}: #{e.message}"
|
|
raise
|
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
Rails.logger.error "EnableBankingEntry::Processor - Failed to save transaction #{external_id}: #{e.message}"
|
|
raise StandardError.new("Failed to import transaction: #{e.message}")
|
|
rescue => e
|
|
Rails.logger.error "EnableBankingEntry::Processor - Unexpected error processing transaction #{external_id}: #{e.class} - #{e.message}"
|
|
Rails.logger.error e.backtrace.join("\n")
|
|
raise StandardError.new("Unexpected error importing transaction: #{e.message}")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :enable_banking_transaction, :enable_banking_account
|
|
|
|
def import_adapter
|
|
@import_adapter ||= Account::ProviderImportAdapter.new(account)
|
|
end
|
|
|
|
def account
|
|
@account ||= enable_banking_account.current_account
|
|
end
|
|
|
|
def data
|
|
@data ||= enable_banking_transaction.with_indifferent_access
|
|
end
|
|
|
|
def external_id
|
|
id = data[:transaction_id].presence || data[:entry_reference].presence
|
|
raise ArgumentError, "Enable Banking transaction missing required field 'transaction_id'" unless id
|
|
"enable_banking_#{id}"
|
|
end
|
|
|
|
def name
|
|
# Build name from available Enable Banking transaction fields
|
|
# Priority: counterparty name > bank_transaction_code description > remittance_information
|
|
|
|
# Determine counterparty based on transaction direction
|
|
# For outgoing payments (DBIT), counterparty is the creditor (who we paid)
|
|
# For incoming payments (CRDT), counterparty is the debtor (who paid us)
|
|
counterparty = if credit_debit_indicator == "CRDT"
|
|
data.dig(:debtor, :name) || data[:debtor_name]
|
|
else
|
|
data.dig(:creditor, :name) || data[:creditor_name]
|
|
end
|
|
|
|
return counterparty if counterparty.present?
|
|
|
|
# Fall back to bank_transaction_code description
|
|
bank_tx_description = data.dig(:bank_transaction_code, :description)
|
|
return bank_tx_description if bank_tx_description.present?
|
|
|
|
# Fall back to remittance_information
|
|
remittance = data[:remittance_information]
|
|
return remittance.first.truncate(100) if remittance.is_a?(Array) && remittance.first.present?
|
|
|
|
# Final fallback: use transaction type indicator
|
|
credit_debit_indicator == "CRDT" ? "Incoming Transfer" : "Outgoing Transfer"
|
|
end
|
|
|
|
def merchant
|
|
# For outgoing payments (DBIT), merchant is the creditor (who we paid)
|
|
# For incoming payments (CRDT), merchant is the debtor (who paid us)
|
|
merchant_name = if credit_debit_indicator == "CRDT"
|
|
data.dig(:debtor, :name) || data[:debtor_name]
|
|
else
|
|
data.dig(:creditor, :name) || data[:creditor_name]
|
|
end
|
|
|
|
return nil unless merchant_name.present?
|
|
|
|
merchant_name = merchant_name.to_s.strip
|
|
return nil if merchant_name.blank?
|
|
|
|
merchant_id = Digest::MD5.hexdigest(merchant_name.downcase)
|
|
|
|
@merchant ||= begin
|
|
import_adapter.find_or_create_merchant(
|
|
provider_merchant_id: "enable_banking_merchant_#{merchant_id}",
|
|
name: merchant_name,
|
|
source: "enable_banking"
|
|
)
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
Rails.logger.error "EnableBankingEntry::Processor - Failed to create merchant '#{merchant_name}': #{e.message}"
|
|
nil
|
|
end
|
|
end
|
|
|
|
def notes
|
|
parts = []
|
|
|
|
remittance = data[:remittance_information]
|
|
if remittance.is_a?(Array) && remittance.any?
|
|
parts << remittance.join("\n")
|
|
elsif remittance.is_a?(String) && remittance.present?
|
|
parts << remittance
|
|
end
|
|
|
|
parts << data[:note] if data[:note].present?
|
|
|
|
parts.join("\n\n").presence
|
|
end
|
|
|
|
def extra
|
|
eb = {}
|
|
|
|
if data[:exchange_rate].present?
|
|
eb[:fx_rate] = data.dig(:exchange_rate, :exchange_rate)
|
|
eb[:fx_unit_currency] = data.dig(:exchange_rate, :unit_currency)
|
|
eb[:fx_instructed_amount] = data.dig(:exchange_rate, :instructed_amount, :amount)
|
|
end
|
|
|
|
eb[:merchant_category_code] = data[:merchant_category_code] if data[:merchant_category_code].present?
|
|
eb[:pending] = true if data[:_pending] == true
|
|
|
|
eb.compact!
|
|
eb.empty? ? nil : { enable_banking: eb }
|
|
end
|
|
|
|
def amount_value
|
|
@amount_value ||= begin
|
|
tx_amount = data[:transaction_amount] || {}
|
|
raw_amount = tx_amount[:amount] || data[:amount] || "0"
|
|
|
|
absolute_amount = case raw_amount
|
|
when String
|
|
BigDecimal(raw_amount).abs
|
|
when Numeric
|
|
BigDecimal(raw_amount.to_s).abs
|
|
else
|
|
BigDecimal("0")
|
|
end
|
|
|
|
# Sure convention: positive = outflow (expense/debit from account), negative = inflow (income/credit)
|
|
# Enable Banking: DBIT = debit from account (outflow), CRDT = credit to account (inflow)
|
|
# Therefore: DBIT → +absolute_amount, CRDT → -absolute_amount
|
|
credit_debit_indicator == "CRDT" ? -absolute_amount : absolute_amount
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "Failed to parse Enable Banking transaction amount: #{raw_amount.inspect} - #{e.message}"
|
|
raise
|
|
end
|
|
end
|
|
|
|
def credit_debit_indicator
|
|
data[:credit_debit_indicator]
|
|
end
|
|
|
|
def amount
|
|
# Sure convention: positive = outflow (debit/expense), negative = inflow (credit/income)
|
|
# amount_value already applies this: DBIT → +absolute, CRDT → -absolute
|
|
amount_value
|
|
end
|
|
|
|
def currency
|
|
tx_amount = data[:transaction_amount] || {}
|
|
parse_currency(tx_amount[:currency]) || parse_currency(data[:currency]) || account&.currency || "EUR"
|
|
end
|
|
|
|
def log_invalid_currency(currency_value)
|
|
Rails.logger.warn("Invalid currency code '#{currency_value}' in Enable Banking transaction #{external_id}, falling back to account currency")
|
|
end
|
|
|
|
def date
|
|
# Prefer booking_date, fall back to value_date, then transaction_date
|
|
date_value = data[:booking_date] || data[:value_date] || data[:transaction_date]
|
|
|
|
case date_value
|
|
when String
|
|
Date.parse(date_value)
|
|
when Integer, Float
|
|
Time.at(date_value).to_date
|
|
when Time, DateTime
|
|
date_value.to_date
|
|
when Date
|
|
date_value
|
|
else
|
|
Rails.logger.error("Enable Banking transaction has invalid date value: #{date_value.inspect}")
|
|
raise ArgumentError, "Invalid date format: #{date_value.inspect}"
|
|
end
|
|
rescue ArgumentError, TypeError => e
|
|
Rails.logger.error("Failed to parse Enable Banking transaction date '#{date_value}': #{e.message}")
|
|
raise ArgumentError, "Unable to parse transaction date: #{date_value.inspect}"
|
|
end
|
|
end
|