mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
* Add tests and logic for Simplefin account balance normalization - Introduced `SimplefinAccountProcessorTest` to verify balance normalization logic. - Updated `SimplefinAccount::Processor` to invert negative balances for liability accounts (credit cards and loans) while keeping asset balances unchanged. - Added comments to clarify balance conventions and sign normalization rules. * Refactor balances-only sync logic and improve tests for edge cases - Updated `SimplefinItem::Importer` and `SimplefinItem::Syncer` to ensure `last_synced_at` remains nil during balances-only runs, preserving chunked-history behavior for full syncs. - Introduced additional comments to clarify balances-only implications and syncing logic. - Added test case in `SimplefinAccountProcessorTest` to verify correct handling of overpayment for credit card liabilities. - Refined balance normalization in `SimplefinAccount::Processor` to always invert liability balances for consistency. --------- Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
102 lines
3.6 KiB
Ruby
102 lines
3.6 KiB
Ruby
class SimplefinAccount::Processor
|
||
attr_reader :simplefin_account
|
||
|
||
def initialize(simplefin_account)
|
||
@simplefin_account = simplefin_account
|
||
end
|
||
|
||
# Each step represents different SimpleFin data processing
|
||
# Processing the account is the first step and if it fails, we halt
|
||
# Each subsequent step can fail independently, but we continue processing
|
||
def process
|
||
# If the account is missing (e.g., user deleted the connection and re‑linked later),
|
||
# do not auto‑link. Relinking is now a manual, user‑confirmed flow via the Relink modal.
|
||
unless simplefin_account.current_account.present?
|
||
return
|
||
end
|
||
|
||
process_account!
|
||
# Ensure provider link exists after processing the account/balance
|
||
begin
|
||
simplefin_account.ensure_account_provider!
|
||
rescue => e
|
||
Rails.logger.warn("SimpleFin provider link ensure failed for #{simplefin_account.id}: #{e.class} - #{e.message}")
|
||
end
|
||
process_transactions
|
||
process_investments
|
||
process_liabilities
|
||
end
|
||
|
||
private
|
||
|
||
def process_account!
|
||
# This should not happen in normal flow since accounts are created manually
|
||
# during setup, but keeping as safety check
|
||
if simplefin_account.current_account.blank?
|
||
Rails.logger.error("SimpleFin account #{simplefin_account.id} has no associated Account - this should not happen after manual setup")
|
||
return
|
||
end
|
||
|
||
# Update account balance and cash balance from latest SimpleFin data
|
||
account = simplefin_account.current_account
|
||
balance = simplefin_account.current_balance || simplefin_account.available_balance || 0
|
||
|
||
# Normalize balances for liabilities (SimpleFIN typically uses opposite sign)
|
||
# App convention:
|
||
# - Liabilities: positive => you owe; negative => provider owes you (overpayment/credit)
|
||
# Since providers often send the opposite sign, ALWAYS invert for liabilities so
|
||
# that both debt and overpayment cases are represented correctly.
|
||
if [ "CreditCard", "Loan" ].include?(account.accountable_type)
|
||
balance = -balance
|
||
end
|
||
|
||
# Calculate cash balance correctly for investment accounts
|
||
cash_balance = if account.accountable_type == "Investment"
|
||
calculator = SimplefinAccount::Investments::BalanceCalculator.new(simplefin_account)
|
||
calculator.cash_balance
|
||
else
|
||
balance
|
||
end
|
||
|
||
account.update!(
|
||
balance: balance,
|
||
cash_balance: cash_balance,
|
||
currency: simplefin_account.currency
|
||
)
|
||
end
|
||
|
||
def process_transactions
|
||
SimplefinAccount::Transactions::Processor.new(simplefin_account).process
|
||
rescue => e
|
||
report_exception(e, "transactions")
|
||
end
|
||
|
||
def process_investments
|
||
return unless simplefin_account.current_account&.accountable_type == "Investment"
|
||
SimplefinAccount::Investments::TransactionsProcessor.new(simplefin_account).process
|
||
SimplefinAccount::Investments::HoldingsProcessor.new(simplefin_account).process
|
||
rescue => e
|
||
report_exception(e, "investments")
|
||
end
|
||
|
||
def process_liabilities
|
||
case simplefin_account.current_account&.accountable_type
|
||
when "CreditCard"
|
||
SimplefinAccount::Liabilities::CreditProcessor.new(simplefin_account).process
|
||
when "Loan"
|
||
SimplefinAccount::Liabilities::LoanProcessor.new(simplefin_account).process
|
||
end
|
||
rescue => e
|
||
report_exception(e, "liabilities")
|
||
end
|
||
|
||
def report_exception(error, context)
|
||
Sentry.capture_exception(error) do |scope|
|
||
scope.set_tags(
|
||
simplefin_account_id: simplefin_account.id,
|
||
context: context
|
||
)
|
||
end
|
||
end
|
||
end
|