class PlaidAccount::Processor include PlaidAccount::TypeMappable attr_reader :plaid_account def initialize(plaid_account) @plaid_account = plaid_account end # Each step represents a different Plaid API endpoint / "product" # # Processing the account is the first step and if it fails, we halt the entire processor # Each subsequent step can fail independently, but we continue processing the rest of the steps def process process_account! process_transactions process_investments process_liabilities end private def family plaid_account.plaid_item.family end # Shared securities reader and resolver def security_resolver @security_resolver ||= PlaidAccount::Investments::SecurityResolver.new(plaid_account) end def process_account! PlaidAccount.transaction do # Find existing account through account_provider or legacy plaid_account_id account_provider = AccountProvider.find_by(provider: plaid_account) account = if account_provider account_provider.account else # Legacy fallback: find by plaid_account_id if it still exists family.accounts.find_by(plaid_account_id: plaid_account.id) end # Initialize new account if not found if account.nil? account = family.accounts.new account.accountable = map_accountable(plaid_account.plaid_type) end # Create or assign the accountable if needed if account.accountable.nil? accountable = map_accountable(plaid_account.plaid_type) account.accountable = accountable end # Name and subtype are the attributes a user can override for Plaid accounts # Use enrichable pattern to respect locked attributes account.enrich_attributes( { name: plaid_account.name }, source: "plaid" ) # Enrich subtype on the accountable, respecting locks account.accountable.enrich_attributes( { subtype: map_subtype(plaid_account.plaid_type, plaid_account.plaid_subtype) }, source: "plaid" ) account.assign_attributes( balance: balance_calculator.balance, currency: plaid_account.currency, cash_balance: balance_calculator.cash_balance ) account.save! # Create account provider link if it doesn't exist unless account_provider AccountProvider.find_or_create_by!( account: account, provider: plaid_account, provider_type: "PlaidAccount" ) end # Create or update the current balance anchor valuation for event-sourced ledger # Note: This is a partial implementation. In the future, we'll introduce HoldingValuation # to properly track the holdings vs. cash breakdown, but for now we're only tracking # the total balance in the current anchor. The cash_balance field on the account model # is still being used for the breakdown. account.set_current_balance(balance_calculator.balance) end end def process_transactions PlaidAccount::Transactions::Processor.new(plaid_account).process rescue => e report_exception(e) end def process_investments PlaidAccount::Investments::TransactionsProcessor.new(plaid_account, security_resolver: security_resolver).process PlaidAccount::Investments::HoldingsProcessor.new(plaid_account, security_resolver: security_resolver).process rescue => e report_exception(e) end def process_liabilities case [ plaid_account.plaid_type, plaid_account.plaid_subtype ] when [ "credit", "credit card" ] PlaidAccount::Liabilities::CreditProcessor.new(plaid_account).process when [ "loan", "mortgage" ] PlaidAccount::Liabilities::MortgageProcessor.new(plaid_account).process when [ "loan", "student" ] PlaidAccount::Liabilities::StudentLoanProcessor.new(plaid_account).process end rescue => e report_exception(e) end def balance_calculator if plaid_account.plaid_type == "investment" @balance_calculator ||= PlaidAccount::Investments::BalanceCalculator.new(plaid_account, security_resolver: security_resolver) else balance = plaid_account.current_balance || plaid_account.available_balance || 0 # We don't currently distinguish "cash" vs. "non-cash" balances for non-investment accounts. OpenStruct.new( balance: balance, cash_balance: balance ) end end def report_exception(error) Sentry.capture_exception(error) do |scope| scope.set_tags(plaid_account_id: plaid_account.id) end end end