mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +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
|
# 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.
|
# for deriving "brokerage cash" vs. "total value" based on Plaid's reported balances and holdings.
|
||||||
class PlaidAccount::Investments::BalanceCalculator
|
class PlaidAccount::Investments::BalanceCalculator
|
||||||
|
NegativeCashBalanceError = Class.new(StandardError)
|
||||||
|
NegativeTotalValueError = Class.new(StandardError)
|
||||||
|
|
||||||
def initialize(plaid_account, security_resolver:)
|
def initialize(plaid_account, security_resolver:)
|
||||||
@plaid_account = plaid_account
|
@plaid_account = plaid_account
|
||||||
@security_resolver = security_resolver
|
@security_resolver = security_resolver
|
||||||
end
|
end
|
||||||
|
|
||||||
def balance
|
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
|
end
|
||||||
|
|
||||||
# Plaid considers "brokerage cash" and "cash equivalent holdings" to all be part of "cash balance"
|
# 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"
|
# For this reason, we must manually calculate the cash balance based on "total value" and "holdings value"
|
||||||
# See PlaidAccount::Investments::SecurityResolver for more details.
|
# See PlaidAccount::Investments::SecurityResolver for more details.
|
||||||
def cash_balance
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -4,6 +4,7 @@ class PlaidAccount::Investments::SecurityResolver
|
||||||
|
|
||||||
def initialize(plaid_account)
|
def initialize(plaid_account)
|
||||||
@plaid_account = plaid_account
|
@plaid_account = plaid_account
|
||||||
|
@security_cache = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Resolves an internal Security record for a given Plaid security ID
|
# 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)
|
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?
|
if plaid_security.nil?
|
||||||
report_unresolvable_security(plaid_security_id)
|
report_unresolvable_security(plaid_security_id)
|
||||||
response = Response.new(security: nil, cash_equivalent?: false, brokerage_cash?: false)
|
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