mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-24 23:59:40 +02:00
Plaid sync tests and multi-currency investment support (#1531)
* Plaid sync tests and multi-currency investment support * Fix system test * Cleanup * Remove data migration
This commit is contained in:
parent
b2a56aefc1
commit
800eb4c146
21 changed files with 406 additions and 165 deletions
|
@ -4,7 +4,7 @@ class Account::HoldingsControllerTest < ActionDispatch::IntegrationTest
|
|||
setup do
|
||||
sign_in users(:family_admin)
|
||||
@account = accounts(:investment)
|
||||
@holding = @account.holdings.current.first
|
||||
@holding = @account.holdings.first
|
||||
end
|
||||
|
||||
test "gets holdings" do
|
||||
|
|
3
test/fixtures/securities.yml
vendored
3
test/fixtures/securities.yml
vendored
|
@ -2,8 +2,11 @@ aapl:
|
|||
ticker: AAPL
|
||||
name: Apple
|
||||
exchange_mic: XNAS
|
||||
country_code: US
|
||||
|
||||
msft:
|
||||
ticker: MSFT
|
||||
name: Microsoft
|
||||
exchange_mic: XNAS
|
||||
country_code: US
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class Account::SyncerTest < ActiveSupport::TestCase
|
|||
)
|
||||
end
|
||||
|
||||
test "converts foreign account balances to family currency" do
|
||||
test "converts foreign account balances and holdings to family currency" do
|
||||
@account.family.update! currency: "USD"
|
||||
@account.update! currency: "EUR"
|
||||
|
||||
|
@ -27,10 +27,19 @@ class Account::SyncerTest < ActiveSupport::TestCase
|
|||
]
|
||||
)
|
||||
|
||||
Account::HoldingCalculator.any_instance.expects(:calculate).returns(
|
||||
[
|
||||
Account::Holding.new(security: securities(:aapl), date: 1.day.ago.to_date, amount: 500, currency: "EUR"),
|
||||
Account::Holding.new(security: securities(:aapl), date: Date.current, amount: 500, currency: "EUR")
|
||||
]
|
||||
)
|
||||
|
||||
Account::Syncer.new(@account).run
|
||||
|
||||
assert_equal [ 1000, 1000 ], @account.balances.where(currency: "EUR").chronological.map(&:balance)
|
||||
assert_equal [ 1200, 2000 ], @account.balances.where(currency: "USD").chronological.map(&:balance)
|
||||
assert_equal [ 500, 500 ], @account.holdings.where(currency: "EUR").chronological.map(&:amount)
|
||||
assert_equal [ 600, 1000 ], @account.holdings.where(currency: "USD").chronological.map(&:amount)
|
||||
end
|
||||
|
||||
test "purges stale balances and holdings" do
|
||||
|
|
|
@ -59,13 +59,4 @@ class AccountTest < ActiveSupport::TestCase
|
|||
assert_equal 0, @account.series(currency: "NZD").values.count
|
||||
end
|
||||
end
|
||||
|
||||
test "calculates shares owned of holding for date" do
|
||||
account = accounts(:investment)
|
||||
security = securities(:aapl)
|
||||
|
||||
assert_equal 10, account.holding_qty(security, date: Date.current)
|
||||
assert_equal 10, account.holding_qty(security, date: 1.day.ago.to_date)
|
||||
assert_equal 0, account.holding_qty(security, date: 2.days.ago.to_date)
|
||||
end
|
||||
end
|
||||
|
|
82
test/models/plaid_investment_sync_test.rb
Normal file
82
test/models/plaid_investment_sync_test.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
require "test_helper"
|
||||
|
||||
class PlaidInvestmentSyncTest < ActiveSupport::TestCase
|
||||
include PlaidTestHelper
|
||||
|
||||
setup do
|
||||
@plaid_account = plaid_accounts(:one)
|
||||
end
|
||||
|
||||
test "syncs basic investments and handles cash holding" do
|
||||
assert_equal 0, @plaid_account.account.entries.count
|
||||
assert_equal 0, @plaid_account.account.holdings.count
|
||||
|
||||
plaid_aapl_id = "aapl_id"
|
||||
|
||||
transactions = [
|
||||
create_plaid_investment_transaction({
|
||||
investment_transaction_id: "inv_txn_1",
|
||||
security_id: plaid_aapl_id,
|
||||
quantity: 10,
|
||||
price: 200,
|
||||
date: 5.days.ago.to_date,
|
||||
type: "buy"
|
||||
})
|
||||
]
|
||||
|
||||
holdings = [
|
||||
create_plaid_cash_holding,
|
||||
create_plaid_holding({
|
||||
security_id: plaid_aapl_id,
|
||||
quantity: 10,
|
||||
institution_price: 200,
|
||||
cost_basis: 2000
|
||||
})
|
||||
]
|
||||
|
||||
securities = [
|
||||
create_plaid_security({
|
||||
security_id: plaid_aapl_id,
|
||||
close_price: 200,
|
||||
ticker_symbol: "AAPL"
|
||||
})
|
||||
]
|
||||
|
||||
# Cash holding should be ignored, resulting in 1, NOT 2 total holdings after sync
|
||||
assert_difference -> { Account::Trade.count } => 1,
|
||||
-> { Account::Transaction.count } => 0,
|
||||
-> { Account::Holding.count } => 1,
|
||||
-> { Security.count } => 0 do
|
||||
PlaidInvestmentSync.new(@plaid_account).sync!(
|
||||
transactions: transactions,
|
||||
holdings: holdings,
|
||||
securities: securities
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Some cash transactions from Plaid are labeled as type: "cash" while others are linked to a "cash" security
|
||||
# In both cases, we should treat them as cash-only transactions (not trades)
|
||||
test "handles cash investment transactions" do
|
||||
transactions = [
|
||||
create_plaid_investment_transaction({
|
||||
price: 1,
|
||||
quantity: 5,
|
||||
amount: 5,
|
||||
type: "fee",
|
||||
subtype: "miscellaneous fee",
|
||||
security_id: PLAID_TEST_CASH_SECURITY_ID
|
||||
})
|
||||
]
|
||||
|
||||
assert_difference -> { Account::Trade.count } => 0,
|
||||
-> { Account::Transaction.count } => 1,
|
||||
-> { Security.count } => 0 do
|
||||
PlaidInvestmentSync.new(@plaid_account).sync!(
|
||||
transactions: transactions,
|
||||
holdings: [ create_plaid_cash_holding ],
|
||||
securities: [ create_plaid_cash_security ]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
128
test/support/plaid_test_helper.rb
Normal file
128
test/support/plaid_test_helper.rb
Normal file
|
@ -0,0 +1,128 @@
|
|||
require "ostruct"
|
||||
|
||||
module PlaidTestHelper
|
||||
PLAID_TEST_ACCOUNT_ID = "plaid_test_account_id"
|
||||
PLAID_TEST_CASH_SECURITY_ID = "plaid_test_cash_security_id"
|
||||
|
||||
# Special case
|
||||
def create_plaid_cash_security(attributes = {})
|
||||
default_attributes = {
|
||||
close_price: nil,
|
||||
close_price_as_of: nil,
|
||||
cusip: nil,
|
||||
fixed_income: nil,
|
||||
industry: nil,
|
||||
institution_id: nil,
|
||||
institution_security_id: nil,
|
||||
is_cash_equivalent: false, # Plaid sometimes returns false here (bad data), so we should not rely on it
|
||||
isin: nil,
|
||||
iso_currency_code: "USD",
|
||||
market_identifier_code: nil,
|
||||
name: "US Dollar",
|
||||
option_contract: nil,
|
||||
proxy_security_id: nil,
|
||||
sector: nil,
|
||||
security_id: PLAID_TEST_CASH_SECURITY_ID,
|
||||
sedol: nil,
|
||||
ticker_symbol: "CUR:USD",
|
||||
type: "cash",
|
||||
unofficial_currency_code: nil,
|
||||
update_datetime: nil
|
||||
}
|
||||
|
||||
OpenStruct.new(
|
||||
default_attributes.merge(attributes)
|
||||
)
|
||||
end
|
||||
|
||||
def create_plaid_security(attributes = {})
|
||||
default_attributes = {
|
||||
close_price: 606.71,
|
||||
close_price_as_of: Date.current,
|
||||
cusip: nil,
|
||||
fixed_income: nil,
|
||||
industry: "Mutual Funds",
|
||||
institution_id: nil,
|
||||
institution_security_id: nil,
|
||||
is_cash_equivalent: false,
|
||||
isin: nil,
|
||||
iso_currency_code: "USD",
|
||||
market_identifier_code: "XNAS",
|
||||
name: "iShares S&P 500 Index",
|
||||
option_contract: nil,
|
||||
proxy_security_id: nil,
|
||||
sector: "Financial",
|
||||
security_id: "plaid_test_security_id",
|
||||
sedol: "2593025",
|
||||
ticker_symbol: "IVV",
|
||||
type: "etf",
|
||||
unofficial_currency_code: nil,
|
||||
update_datetime: nil
|
||||
}
|
||||
|
||||
OpenStruct.new(
|
||||
default_attributes.merge(attributes)
|
||||
)
|
||||
end
|
||||
|
||||
def create_plaid_cash_holding(attributes = {})
|
||||
default_attributes = {
|
||||
account_id: PLAID_TEST_ACCOUNT_ID,
|
||||
cost_basis: 1000,
|
||||
institution_price: 1,
|
||||
institution_price_as_of: Date.current,
|
||||
iso_currency_code: "USD",
|
||||
quantity: 1000,
|
||||
security_id: PLAID_TEST_CASH_SECURITY_ID,
|
||||
unofficial_currency_code: nil,
|
||||
vested_quantity: nil,
|
||||
vested_value: nil
|
||||
}
|
||||
|
||||
OpenStruct.new(
|
||||
default_attributes.merge(attributes)
|
||||
)
|
||||
end
|
||||
|
||||
def create_plaid_holding(attributes = {})
|
||||
default_attributes = {
|
||||
account_id: PLAID_TEST_ACCOUNT_ID,
|
||||
cost_basis: 2000,
|
||||
institution_price: 200,
|
||||
institution_price_as_of: Date.current,
|
||||
iso_currency_code: "USD",
|
||||
quantity: 10,
|
||||
security_id: "plaid_test_security_id",
|
||||
unofficial_currency_code: nil,
|
||||
vested_quantity: nil,
|
||||
vested_value: nil
|
||||
}
|
||||
|
||||
OpenStruct.new(
|
||||
default_attributes.merge(attributes)
|
||||
)
|
||||
end
|
||||
|
||||
def create_plaid_investment_transaction(attributes = {})
|
||||
default_attributes = {
|
||||
account_id: PLAID_TEST_ACCOUNT_ID,
|
||||
amount: 500,
|
||||
cancel_transaction_id: nil,
|
||||
date: 5.days.ago.to_date,
|
||||
fees: 0,
|
||||
investment_transaction_id: "plaid_test_investment_transaction_id",
|
||||
iso_currency_code: "USD",
|
||||
name: "Buy 100 shares of IVV",
|
||||
price: 606.71,
|
||||
quantity: 100,
|
||||
security_id: "plaid_test_security_id",
|
||||
type: "buy",
|
||||
subtype: "buy",
|
||||
unofficial_currency_code: nil
|
||||
}
|
||||
|
||||
OpenStruct.new(
|
||||
default_attributes.merge(attributes)
|
||||
)
|
||||
end
|
||||
end
|
|
@ -16,7 +16,8 @@ class TradesTest < ApplicationSystemTestCase
|
|||
name: "Apple Inc.",
|
||||
logo_url: "https://logo.synthfinance.com/ticker/AAPL",
|
||||
exchange_acronym: "NASDAQ",
|
||||
exchange_mic: "XNAS"
|
||||
exchange_mic: "XNAS",
|
||||
country_code: "US"
|
||||
)
|
||||
])
|
||||
end
|
||||
|
@ -43,7 +44,7 @@ class TradesTest < ApplicationSystemTestCase
|
|||
end
|
||||
|
||||
test "can create sell transaction" do
|
||||
aapl = @account.holdings.current.find { |h| h.security.ticker == "AAPL" }
|
||||
aapl = @account.holdings.find { |h| h.security.ticker == "AAPL" }
|
||||
|
||||
open_new_trade_modal
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue