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:
parent
be5de57d5a
commit
ef2ba0e54e
3 changed files with 107 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue