mirror of
https://github.com/we-promise/sure
synced 2026-04-25 17:15:07 +02:00
* Replace Maybe for Sure in select code areas * Make sure passwords are consistent * Remove (admin|member) from demo data first name * Database and schema names finally to `sure` * Fix broken test * Another (benchmarking) database name to `sure_*` * More rebranding to Sure * Missed this Maybe mention in the same page * Random nitpicks and more Maybes * Demo data accounts and more Maybes * Test data account updates * Impersonation test accounts * Consistency with `compose.example.yml`
72 lines
2.5 KiB
Ruby
72 lines
2.5 KiB
Ruby
# 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_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"
|
|
#
|
|
# Internally, we DO NOT. Sure clearly distinguishes between "brokerage cash" vs. "holdings (i.e. invested cash)"
|
|
# 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
|
|
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
|
|
attr_reader :plaid_account, :security_resolver
|
|
|
|
def holdings
|
|
plaid_account.raw_investments_payload["holdings"] || []
|
|
end
|
|
|
|
def calculate_investment_brokerage_cash
|
|
total_investment_account_value - true_holdings_value
|
|
end
|
|
|
|
# This is our source of truth. We assume Plaid's `current_balance` reporting is 100% accurate
|
|
# Plaid guarantees `current_balance` AND/OR `available_balance` is always present, and based on the docs,
|
|
# `current_balance` should represent "total account value".
|
|
def total_investment_account_value
|
|
plaid_account.current_balance || plaid_account.available_balance
|
|
end
|
|
|
|
# Plaid holdings summed up, LESS "brokerage cash" holdings (that we've manually identified)
|
|
def true_holdings_value
|
|
# True holdings are holdings *less* Plaid's "pseudo-securities" (e.g. `CUR:USD` brokerage cash "holding")
|
|
true_holdings = holdings.reject do |h|
|
|
security = security_resolver.resolve(plaid_security_id: h["security_id"])
|
|
security.brokerage_cash?
|
|
end
|
|
|
|
true_holdings.sum { |h| h["quantity"] * h["institution_price"] }
|
|
end
|
|
end
|