1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Plaid balance calculator test cases

This commit is contained in:
Zach Gollwitzer 2025-05-23 11:29:23 -04:00
parent be5de57d5a
commit ef2ba0e54e
3 changed files with 107 additions and 11 deletions

View file

@ -1,13 +1,25 @@
# 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::Investments::BalanceCalculator
NegativeCashBalanceError = Class.new(StandardError)
NegativeTotalValueError = Class.new(StandardError)
def initialize(plaid_account, security_resolver:)
@plaid_account = plaid_account
@security_resolver = security_resolver
end
def balance
total_investment_account_value
total_value = total_investment_account_value
if total_value.negative?
Sentry.capture_exception(
NegativeTotalValueError.new("Total value is negative for plaid investment account"),
level: :warning
)
end
total_value
end
# Plaid considers "brokerage cash" and "cash equivalent holdings" to all be part of "cash balance"
@ -16,7 +28,16 @@ class PlaidAccount::Investments::BalanceCalculator
# For this reason, we must manually calculate the cash balance based on "total value" and "holdings value"
# See PlaidAccount::Investments::SecurityResolver for more details.
def cash_balance
calculate_investment_brokerage_cash
cash_balance = calculate_investment_brokerage_cash
if cash_balance.negative?
Sentry.capture_exception(
NegativeCashBalanceError.new("Cash balance is negative for plaid investment account"),
level: :warning
)
end
cash_balance
end
private

View file

@ -4,6 +4,7 @@ class PlaidAccount::Investments::SecurityResolver
def initialize(plaid_account)
@plaid_account = plaid_account
@security_cache = {}
end
# Resolves an internal Security record for a given Plaid security ID
@ -13,15 +14,6 @@ class PlaidAccount::Investments::SecurityResolver
plaid_security = get_plaid_security(plaid_security_id)
unless plaid_security
report_unresolvable_security(plaid_security_id)
return Response.new(security: nil, cash_equivalent?: false)
end
if brokerage_cash?(plaid_security)
return Response.new(security: nil, cash_equivalent?: true)
end
if plaid_security.nil?
report_unresolvable_security(plaid_security_id)
response = Response.new(security: nil, cash_equivalent?: false, brokerage_cash?: false)

View file

@ -0,0 +1,83 @@
require "test_helper"
class PlaidAccount::Investments::BalanceCalculatorTest < ActiveSupport::TestCase
setup do
@plaid_account = plaid_accounts(:one)
@plaid_account.update!(
plaid_type: "investment",
current_balance: 4000,
available_balance: 2000 # We ignore this since we have current_balance + holdings
)
end
test "calculates total balance from cash and positions" do
brokerage_cash_security_id = "plaid_brokerage_cash" # Plaid's brokerage cash security
cash_equivalent_security_id = "plaid_cash_equivalent" # Cash equivalent security (i.e. money market fund)
aapl_security_id = "plaid_aapl_security" # Regular stock security
test_investments = {
transactions: [], # Irrelevant for balance calcs, leave empty
holdings: [
# $1,000 in brokerage cash
{
security_id: brokerage_cash_security_id,
cost_basis: 1000,
institution_price: 1,
institution_value: 1000,
quantity: 1000
},
# $1,000 in money market funds
{
security_id: cash_equivalent_security_id,
cost_basis: 1000,
institution_price: 1,
institution_value: 1000,
quantity: 1000
},
# $2,000 worth of AAPL stock
{
security_id: aapl_security_id,
cost_basis: 2000,
institution_price: 200,
institution_value: 2000,
quantity: 10
}
],
securities: [
{
security_id: brokerage_cash_security_id,
ticker_symbol: "CUR:USD",
is_cash_equivalent: true,
type: "cash"
},
{
security_id: cash_equivalent_security_id,
ticker_symbol: "VMFXX", # Vanguard Money Market Reserves
is_cash_equivalent: true,
type: "mutual fund"
},
{
security_id: aapl_security_id,
ticker_symbol: "AAPL",
is_cash_equivalent: false,
type: "equity",
market_identifier_code: "XNAS"
}
]
}
@plaid_account.update!(raw_investments_payload: test_investments)
security_resolver = PlaidAccount::Investments::SecurityResolver.new(@plaid_account)
balance_calculator = PlaidAccount::Investments::BalanceCalculator.new(@plaid_account, security_resolver: security_resolver)
# We set this equal to `current_balance`
assert_equal 4000, balance_calculator.balance
# This is the sum of "non-brokerage-cash-holdings". In the above test case, this means
# we're summing up $2,000 of AAPL + $1,000 Vanguard MM for $3,000 in holdings value.
# We back this $3,000 from the $4,000 total to get $1,000 in cash balance.
assert_equal 1000, balance_calculator.cash_balance
end
end