mirror of
https://github.com/we-promise/sure.git
synced 2026-07-27 20:22:16 +00:00
* fix(sync): don't persist zero balances when a provider balance fetch fails A nil current_balance means the sync's balance fetch did not succeed (the snapshot upsert clears it; only a successful fetch repopulates it). The Lunchflow and Enable Banking processors coerced nil to 0 and write it (plus a currency fallback) onto the linked account, so any transient provider failure persists wrong data with no user-visible signal. Instead, treat nil as no-data: skip the account update. Also stop the Lunchflow snapshot upsert resetting an established account's currency to USD when the accounts endpoint omits currency. * fix(lunchflow): normalize the preserved currency in snapshot upsert Addresses the review comment on #2617: the preserved fallback reused the record's raw in-memory currency, so a blank value would fail the presence validation and break import, and an invalid code would persist instead of falling back to USD. Run it through parse_currency like the payload value. Regression test added. * fix(lunchflow,eb): review round 2 — failure visibility and currency parity Addresses the three review findings on #2617: 1. Capture the Lunch Flow balance-fetch failure via DebugLogEntry (the sync otherwise reports success with no mention of the skip). This is Lunch Flow only: Enable Banking's importer already surfaces the failure through transactions_failed/@sync_error. 2. Apply the currency-preservation fix to Enable Banking's snapshot upsert (same shape as the Lunch Flow fix: parity/safety). Regression test added. 3. Label the Enable Banking processor currency assertion as a parity check — it also passes on main, since the reset defect was Lunch Flow-specific.
120 lines
5.2 KiB
Ruby
120 lines
5.2 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
|
|
|
|
# A nil current_balance means this sync's balance fetch did not succeed:
|
|
# upsert_enable_banking_snapshot! clears it and only a successful
|
|
# balance fetch repopulates it. Coercing nil to 0 here would persist a
|
|
# zero balance (and a zero current_anchor valuation) onto a healthy
|
|
# account whenever the provider has a transient failure, so leave the
|
|
# account untouched instead.
|
|
if balance.nil?
|
|
Rails.logger.warn("EnableBankingAccount::Processor - No balance available for enable_banking_account #{enable_banking_account.id} (balance fetch failed or not yet run), skipping account update")
|
|
return
|
|
end
|
|
|
|
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
|