mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 08:32:15 +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.
101 lines
3.6 KiB
Ruby
101 lines
3.6 KiB
Ruby
class LunchflowAccount::Processor
|
|
include CurrencyNormalizable
|
|
|
|
attr_reader :lunchflow_account
|
|
|
|
def initialize(lunchflow_account)
|
|
@lunchflow_account = lunchflow_account
|
|
end
|
|
|
|
def process
|
|
unless lunchflow_account.current_account.present?
|
|
Rails.logger.info "LunchflowAccount::Processor - No linked account for lunchflow_account #{lunchflow_account.id}, skipping processing"
|
|
return
|
|
end
|
|
|
|
Rails.logger.info "LunchflowAccount::Processor - Processing lunchflow_account #{lunchflow_account.id} (account #{lunchflow_account.account_id})"
|
|
|
|
begin
|
|
process_account!
|
|
rescue StandardError => e
|
|
Rails.logger.error "LunchflowAccount::Processor - Failed to process account #{lunchflow_account.id}: #{e.message}"
|
|
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
|
report_exception(e, "account")
|
|
raise
|
|
end
|
|
|
|
process_transactions
|
|
process_investments
|
|
end
|
|
|
|
private
|
|
|
|
def process_account!
|
|
if lunchflow_account.current_account.blank?
|
|
Rails.logger.error("Lunchflow account #{lunchflow_account.id} has no associated Account")
|
|
return
|
|
end
|
|
|
|
# Update account balance from latest Lunchflow data
|
|
account = lunchflow_account.current_account
|
|
balance = lunchflow_account.current_balance
|
|
|
|
# A nil current_balance means this sync's balance fetch did not succeed:
|
|
# upsert_lunchflow_snapshot! clears it (the accounts endpoint carries no
|
|
# balance) and only a successful balance fetch repopulates it. Coercing
|
|
# nil to 0 here would persist a zero balance (and the USD currency
|
|
# fallback) onto a healthy account whenever the provider has a
|
|
# transient failure, so leave the account untouched instead.
|
|
if balance.nil?
|
|
Rails.logger.warn("LunchflowAccount::Processor - No balance available for lunchflow_account #{lunchflow_account.id} (balance fetch failed or not yet run), skipping account update")
|
|
return
|
|
end
|
|
|
|
# LunchFlow balance convention matches our app convention:
|
|
# - Positive balance = debt (you owe money)
|
|
# - Negative balance = credit balance (bank owes you, e.g., overpayment)
|
|
# No sign conversion needed - pass through as-is (same as Plaid)
|
|
#
|
|
# Exception: CreditCard and Loan accounts return inverted signs
|
|
# Provider returns negative for positive balance, so we negate it
|
|
if account.accountable_type == "CreditCard" || account.accountable_type == "Loan"
|
|
balance = -balance
|
|
end
|
|
|
|
# Normalize currency with fallback chain: parsed lunchflow currency -> existing account currency -> USD
|
|
currency = parse_currency(lunchflow_account.currency) || account.currency || "USD"
|
|
|
|
# Update account balance
|
|
account.update!(
|
|
balance: balance,
|
|
cash_balance: balance,
|
|
currency: currency
|
|
)
|
|
end
|
|
|
|
def process_transactions
|
|
LunchflowAccount::Transactions::Processor.new(lunchflow_account).process
|
|
rescue => e
|
|
report_exception(e, "transactions")
|
|
end
|
|
|
|
def process_investments
|
|
# Only process holdings for investment/crypto accounts with holdings support
|
|
return unless lunchflow_account.holdings_supported?
|
|
return unless [ "Investment", "Crypto" ].include?(lunchflow_account.current_account&.accountable_type)
|
|
|
|
LunchflowAccount::Investments::HoldingsProcessor.new(lunchflow_account).process
|
|
rescue => e
|
|
report_exception(e, "holdings")
|
|
end
|
|
|
|
def report_exception(error, context)
|
|
Sentry.capture_exception(error) do |scope|
|
|
scope.set_tags(
|
|
lunchflow_account_id: lunchflow_account.id,
|
|
context: context
|
|
)
|
|
end
|
|
end
|
|
end
|