mirror of
https://github.com/we-promise/sure.git
synced 2026-07-18 07:45:24 +00:00
* fix(sync): store EnableBanking credit card debt as balance instead of available credit Previously, the EnableBanking processor forcibly overrode the primary account balance for credit cards to be the available credit (credit_limit - debt) rather than the actual outstanding debt. Since credit cards are modeled as Liability accounts, this caused the balance sheet (net worth) to treat available credit mathematically as a debt. This PR aligns the EnableBanking processor with the Plaid and SimpleFIN processors by storing the absolute debt as the account balance, while tracking the available credit via accountable metadata. Fixes #2458 * docs(sync): update EnableBanking credit card processor documentation Updates the inline processor comments to reflect the new behavior introduced by the previous commit, clarifying that outstanding debt is stored sequentially as the primary balance rather than the UX available credit overriding it. * refactor(sync): clarify balance and available credit calculations in EnableBanking processor Refactors the debt polarity assignments to clarify why liability balances are strictly parsed as absolute positive numbers. Replaces the implicit ordering dependency between the '.abs' conversion and the 'available_credit' math with an explicit 'outstanding_debt' variable to prevent regression by future maintainers. * style: remove trailing whitespace in EnableBanking processor
108 lines
4.6 KiB
Ruby
108 lines
4.6 KiB
Ruby
class EnableBankingAccount::Processor
|
|
class ProcessingError < StandardError; end
|
|
include CurrencyNormalizable
|
|
|
|
attr_reader :enable_banking_account
|
|
|
|
def initialize(enable_banking_account)
|
|
@enable_banking_account = enable_banking_account
|
|
end
|
|
|
|
def process
|
|
unless enable_banking_account.current_account.present?
|
|
Rails.logger.info "EnableBankingAccount::Processor - No linked account for enable_banking_account #{enable_banking_account.id}, skipping processing"
|
|
return
|
|
end
|
|
|
|
Rails.logger.info "EnableBankingAccount::Processor - Processing enable_banking_account #{enable_banking_account.id} (uid #{enable_banking_account.uid})"
|
|
|
|
begin
|
|
process_account!
|
|
rescue StandardError => e
|
|
Rails.logger.error "EnableBankingAccount::Processor - Failed to process account #{enable_banking_account.id}: #{e.message}"
|
|
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
|
report_exception(e, "account")
|
|
raise
|
|
end
|
|
|
|
process_transactions
|
|
end
|
|
|
|
private
|
|
|
|
def process_account!
|
|
if enable_banking_account.current_account.blank?
|
|
Rails.logger.error("Enable Banking account #{enable_banking_account.id} has no associated Account")
|
|
return
|
|
end
|
|
|
|
account = enable_banking_account.current_account
|
|
balance = enable_banking_account.current_balance || 0
|
|
available_credit = nil
|
|
|
|
# For liability accounts, ensure balance sign is correct.
|
|
# For CreditCards, we expect the main balance to reflect the absolute outstanding debt
|
|
# rather than available credit, to ensure net worth calculations handle the liability accurately.
|
|
# Any available credit metrics (from limits) are instead stored safely as metadata on the Accountable.
|
|
# Loans and CreditCards must always represent their outstanding balance as an absolute
|
|
# positive debt amount, regardless of the API's reported sign, to ensure the BalanceSheet
|
|
# calculates net worth accurately.
|
|
if account.accountable_type == "Loan" || account.accountable_type == "CreditCard"
|
|
# Standardize the raw balance to an absolute positive debt
|
|
outstanding_debt = balance.abs
|
|
|
|
# Override the top-level balance variable intended for the account
|
|
balance = outstanding_debt
|
|
|
|
if account.accountable_type == "CreditCard"
|
|
if enable_banking_account.credit_limit.present?
|
|
# Compute available credit based on the strictly positive outstanding debt
|
|
available = enable_banking_account.credit_limit - outstanding_debt
|
|
available_credit = [ available, 0 ].max
|
|
unless account.accountable.present?
|
|
Rails.logger.warn "EnableBankingAccount::Processor - CreditCard accountable missing for account #{account.id}"
|
|
end
|
|
elsif account.accountable&.available_credit.present?
|
|
# Fallback: no credit_limit from API — compute it using available_credit defined at account level
|
|
Rails.logger.info "Using stored available_credit fallback for account #{account.id}"
|
|
available_credit = account.accountable.available_credit
|
|
end
|
|
end
|
|
end
|
|
|
|
currency = parse_currency(enable_banking_account.currency) || account.currency || "EUR"
|
|
|
|
# Wrap both writes in a transaction so a failure on either rolls back both.
|
|
ActiveRecord::Base.transaction do
|
|
if account.accountable.present? && account.accountable.respond_to?(:available_credit=)
|
|
account.accountable.update!(available_credit: available_credit)
|
|
end
|
|
account.update!(currency: currency, cash_balance: balance)
|
|
|
|
# Use set_current_balance to create a current_anchor valuation entry.
|
|
# This enables Balance::ReverseCalculator, which works backward from the
|
|
# bank-reported balance — eliminating spurious cash adjustment spikes.
|
|
result = account.set_current_balance(balance)
|
|
raise ProcessingError, "Failed to set current balance: #{result.error}" unless result.success?
|
|
end
|
|
|
|
# TODO: pass explicit window_start_date to sync_later to avoid full history recalculation on every sync
|
|
# Currently relies on set_current_balance's implicit sync trigger; window params would require refactor
|
|
end
|
|
|
|
def process_transactions
|
|
EnableBankingAccount::Transactions::Processor.new(enable_banking_account).process
|
|
rescue => e
|
|
report_exception(e, "transactions")
|
|
end
|
|
|
|
def report_exception(error, context)
|
|
Sentry.capture_exception(error) do |scope|
|
|
scope.set_tags(
|
|
enable_banking_account_id: enable_banking_account.id,
|
|
context: context
|
|
)
|
|
end
|
|
end
|
|
end
|