From 31eafbf578353bb442edbbfa80c95cf35a097983 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Wed, 21 May 2025 07:20:49 -0400 Subject: [PATCH] Checkpoint --- .../credit_liability_processor.rb | 16 +++++++ .../investment_balance_processor.rb | 2 + .../investment_holdings_processor.rb | 18 +++++++ .../investment_transactions_processor.rb | 17 +++++++ app/models/plaid_account/securitizable.rb | 39 +++++++++++++++ .../plaid_account/security_processor.rb | 22 +++++++++ app/models/security/resolver.rb | 48 +++++++++++++++++++ 7 files changed, 162 insertions(+) create mode 100644 app/models/plaid_account/credit_liability_processor.rb create mode 100644 app/models/plaid_account/investment_holdings_processor.rb create mode 100644 app/models/plaid_account/investment_transactions_processor.rb create mode 100644 app/models/plaid_account/securitizable.rb create mode 100644 app/models/plaid_account/security_processor.rb create mode 100644 app/models/security/resolver.rb diff --git a/app/models/plaid_account/credit_liability_processor.rb b/app/models/plaid_account/credit_liability_processor.rb new file mode 100644 index 00000000..6d1ce099 --- /dev/null +++ b/app/models/plaid_account/credit_liability_processor.rb @@ -0,0 +1,16 @@ +class PlaidAccount::CreditLiabilityProcessor + def initialize(plaid_account) + @plaid_account = plaid_account + end + + def process + puts "processing credit liability!" + end + + private + attr_reader :plaid_account + + def account + plaid_account.account + end +end diff --git a/app/models/plaid_account/investment_balance_processor.rb b/app/models/plaid_account/investment_balance_processor.rb index 748cb343..ed25a8a8 100644 --- a/app/models/plaid_account/investment_balance_processor.rb +++ b/app/models/plaid_account/investment_balance_processor.rb @@ -1,6 +1,8 @@ # Plaid Investment balances have a ton of edge cases. This processor is responsible # for deriving "brokerage cash" vs. "total value" based on Plaid's reported balances and holdings. class PlaidAccount::InvestmentBalanceProcessor + include PlaidAccount::Securitizable + attr_reader :plaid_account def initialize(plaid_account) diff --git a/app/models/plaid_account/investment_holdings_processor.rb b/app/models/plaid_account/investment_holdings_processor.rb new file mode 100644 index 00000000..7282a3e7 --- /dev/null +++ b/app/models/plaid_account/investment_holdings_processor.rb @@ -0,0 +1,18 @@ +class PlaidAccount::InvestmentHoldingsProcessor + include PlaidAccount::Securitizable + + def initialize(plaid_account) + @plaid_account = plaid_account + end + + def process + puts "processing investment holdings!" + end + + private + attr_reader :plaid_account + + def account + plaid_account.account + end +end diff --git a/app/models/plaid_account/investment_transactions_processor.rb b/app/models/plaid_account/investment_transactions_processor.rb new file mode 100644 index 00000000..c713924f --- /dev/null +++ b/app/models/plaid_account/investment_transactions_processor.rb @@ -0,0 +1,17 @@ +class PlaidAccount::InvestmentTransactionsProcessor + include PlaidAccount::Securitizable + + def initialize(plaid_account) + @plaid_account = plaid_account + end + + def process + end + + private + attr_reader :plaid_account + + def account + plaid_account.account + end +end diff --git a/app/models/plaid_account/securitizable.rb b/app/models/plaid_account/securitizable.rb new file mode 100644 index 00000000..757a1aaa --- /dev/null +++ b/app/models/plaid_account/securitizable.rb @@ -0,0 +1,39 @@ +module PlaidAccount::Securitizable + extend ActiveSupport::Concern + + # TODO + def get_security(plaid_security_id) + plaid_security = get_plaid_security(plaid_security_id) + + + + return [ nil, nil ] if plaid_security.nil? || plaid_security["ticker_symbol"].blank? + return [ nil, plaid_security ] if plaid_security["ticker_symbol"] == "CUR:USD" # internally, we do not consider cash a security and track it separately + + operating_mic = plaid_security["market_identifier_code"] + + # Find any matching security + security = Security.find_or_create_by!( + ticker: plaid_security["ticker_symbol"]&.upcase, + exchange_operating_mic: operating_mic&.upcase + ) + + [ security, plaid_security ] + end + + + + private + def securities + @securities ||= plaid_account.raw_investments_payload["securities"] || [] + end + + # These are the tickers that Plaid considers a "security", but we do not (mostly cash-like tickers) + # + # For example, "CUR:USD" is what Plaid uses for the "Cash Holding" and represents brokerage cash sitting + # in the brokerage account. Internally, we treat brokerage cash as a separate concept. It is NOT a holding + # in the Maybe app (although in the UI, we show it next to other holdings). + def ignored_plaid_security_tickers + [ "CUR:USD" ] + end +end diff --git a/app/models/plaid_account/security_processor.rb b/app/models/plaid_account/security_processor.rb new file mode 100644 index 00000000..428de1c1 --- /dev/null +++ b/app/models/plaid_account/security_processor.rb @@ -0,0 +1,22 @@ +# Resolves a Plaid security to an internal Security record, or nil +class PlaidAccount::SecurityProcessor + def initialize(plaid_security_id, plaid_securities) + @plaid_security_id = plaid_security_id + @plaid_securities = plaid_securities + end + + def process + end + + private + attr_reader :plaid_security_id, :plaid_securities + + # Tries to find security, or returns the "proxy security" (common with options contracts that have underlying securities) + def plaid_security + security = securities.find { |s| s["security_id"] == plaid_security_id && s["ticker_symbol"].present? } + + return security if security.present? + + securities.find { |s| s["proxy_security_id"] == plaid_security_id } + end +end diff --git a/app/models/security/resolver.rb b/app/models/security/resolver.rb new file mode 100644 index 00000000..fc6cea6d --- /dev/null +++ b/app/models/security/resolver.rb @@ -0,0 +1,48 @@ +class Security::Resolver + def initialize(symbol, exchange_operating_mic: nil, country_code: nil) + @symbol = symbol + @exchange_operating_mic = exchange_operating_mic + @country_code = country_code + end + + def resolve + return nil unless symbol + + exact_match = Security.find_by( + ticker: symbol, + exchange_operating_mic: exchange_operating_mic + ) + + exact_match if exact_match.present? + end + + private + attr_reader :symbol, :exchange_operating_mic, :country_code + + def fetch_from_provider + return nil unless Security.provider.present? + + result = Security.search_provider( + symbol, + exchange_operating_mic: exchange_operating_mic + ) + + return nil unless result.success? + + selection = if exchange_operating_mic.present? + result.data.find do |s| + s.ticker == symbol && s.exchange_operating_mic == exchange_operating_mic + end + else + result.data.sort_by + end + + unless selection.present? + + end + + selection + end + + def +end