Files
sure/test/models/plaid_account/investments/security_resolver_test.rb
Zach Gollwitzer 03a146222d Plaid sync domain improvements (#2267)
Breaks our Plaid sync process out into more manageable classes. Notably, this moves the sync process to a distinct, 2-step flow:

1. Import stage - we first make API calls and import Plaid data to "mirror" tables
2. Processing stage - read the raw data, apply business rules, build internal domain models and sync balances

This provides several benefits:

- Plaid syncs can now be "replayed" without fetching API data again
- Mirror tables provide better audit and debugging capabilities
- Eliminates the "all or nothing" sync behavior that is currently in place, which is brittle
2025-05-23 18:58:22 -04:00

116 lines
3.4 KiB
Ruby

require "test_helper"
class PlaidAccount::Investments::SecurityResolverTest < ActiveSupport::TestCase
setup do
@upstream_resolver = mock("Security::Resolver")
@plaid_account = plaid_accounts(:one)
@resolver = PlaidAccount::Investments::SecurityResolver.new(@plaid_account)
end
test "handles missing plaid security" do
missing_id = "missing_security_id"
# Ensure there are *no* securities that reference the missing ID
@plaid_account.update!(raw_investments_payload: {
securities: [
{
"security_id" => "some_other_id",
"ticker_symbol" => "FOO",
"type" => "equity",
"market_identifier_code" => "XNAS"
}
]
})
Security::Resolver.expects(:new).never
Sentry.stubs(:capture_exception)
response = @resolver.resolve(plaid_security_id: missing_id)
assert_nil response.security
refute response.cash_equivalent?
refute response.brokerage_cash?
end
test "identifies brokerage cash plaid securities" do
brokerage_cash_id = "brokerage_cash_security_id"
@plaid_account.update!(raw_investments_payload: {
securities: [
{
"security_id" => brokerage_cash_id,
"ticker_symbol" => "CUR:USD", # Plaid brokerage cash ticker
"type" => "cash",
"is_cash_equivalent" => true
}
]
})
Security::Resolver.expects(:new).never
response = @resolver.resolve(plaid_security_id: brokerage_cash_id)
assert_nil response.security
assert response.cash_equivalent?
assert response.brokerage_cash?
end
test "identifies cash equivalent plaid securities" do
mmf_security_id = "money_market_security_id"
@plaid_account.update!(raw_investments_payload: {
securities: [
{
"security_id" => mmf_security_id,
"ticker_symbol" => "VMFXX", # Vanguard Federal Money Market Fund
"type" => "mutual fund",
"is_cash_equivalent" => true,
"market_identifier_code" => "XNAS"
}
]
})
resolved_security = Security.create!(ticker: "VMFXX", exchange_operating_mic: "XNAS")
Security::Resolver.expects(:new)
.with("VMFXX", exchange_operating_mic: "XNAS")
.returns(@upstream_resolver)
@upstream_resolver.expects(:resolve).returns(resolved_security)
response = @resolver.resolve(plaid_security_id: mmf_security_id)
assert_equal resolved_security, response.security
assert response.cash_equivalent?
refute response.brokerage_cash?
end
test "resolves normal plaid securities" do
security_id = "regular_security_id"
@plaid_account.update!(raw_investments_payload: {
securities: [
{
"security_id" => security_id,
"ticker_symbol" => "IVV",
"type" => "etf",
"is_cash_equivalent" => false,
"market_identifier_code" => "XNAS"
}
]
})
resolved_security = Security.create!(ticker: "IVV", exchange_operating_mic: "XNAS")
Security::Resolver.expects(:new)
.with("IVV", exchange_operating_mic: "XNAS")
.returns(@upstream_resolver)
@upstream_resolver.expects(:resolve).returns(resolved_security)
response = @resolver.resolve(plaid_security_id: security_id)
assert_equal resolved_security, response.security
refute response.cash_equivalent? # Normal securities are not cash equivalent
refute response.brokerage_cash?
end
end