From ed9d9d63359d3c20ab868b52825040ccb631e5ee Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Mon, 19 May 2025 14:35:23 -0400 Subject: [PATCH] Test raw payloads on plaid accounts --- app/models/plaid_item/accounts_snapshot.rb | 10 ++-- test/models/plaid_account/importer_test.rb | 13 +++++ test/models/plaid_item/importer_test.rb | 5 +- test/support/plaid_mock.rb | 56 ++++++++++++---------- 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/app/models/plaid_item/accounts_snapshot.rb b/app/models/plaid_item/accounts_snapshot.rb index aee60a1c..a6d2c355 100644 --- a/app/models/plaid_item/accounts_snapshot.rb +++ b/app/models/plaid_item/accounts_snapshot.rb @@ -41,10 +41,14 @@ class PlaidItem::AccountsSnapshot def account_scoped_investments_data(account_id) return nil unless investments_data + transactions = investments_data.transactions.select { |t| t.account_id == account_id } + holdings = investments_data.holdings.select { |h| h.account_id == account_id } + securities = transactions.count > 0 && holdings.count > 0 ? investments_data.securities : [] + InvestmentsData.new( - transactions: investments_data.transactions.select { |t| t.account_id == account_id }, - holdings: investments_data.holdings.select { |h| h.account_id == account_id }, - securities: investments_data.securities + transactions: transactions, + holdings: holdings, + securities: securities ) end diff --git a/test/models/plaid_account/importer_test.rb b/test/models/plaid_account/importer_test.rb index 3575dc2d..68f61041 100644 --- a/test/models/plaid_account/importer_test.rb +++ b/test/models/plaid_account/importer_test.rb @@ -18,5 +18,18 @@ class PlaidAccount::ImporterTest < ActiveSupport::TestCase assert_equal @account_snapshot.account_data.mask, @plaid_account.mask assert_equal @account_snapshot.account_data.type, @plaid_account.plaid_type assert_equal @account_snapshot.account_data.subtype, @plaid_account.plaid_subtype + + # This account has transactions data + assert_equal PlaidMock::TRANSACTIONS.count, @plaid_account.raw_transactions_payload["added"].count + + # This account does not have investment data + assert_equal 0, @plaid_account.raw_investments_payload["holdings"].count + assert_equal 0, @plaid_account.raw_investments_payload["securities"].count + assert_equal 0, @plaid_account.raw_investments_payload["transactions"].count + + # This account is a credit card, so it should have liability data + assert_equal @plaid_account.plaid_id, @plaid_account.raw_liabilities_payload["credit"]["account_id"] + assert_nil @plaid_account.raw_liabilities_payload["mortgage"] + assert_nil @plaid_account.raw_liabilities_payload["student"] end end diff --git a/test/models/plaid_item/importer_test.rb b/test/models/plaid_item/importer_test.rb index fc795b19..085517c9 100644 --- a/test/models/plaid_item/importer_test.rb +++ b/test/models/plaid_item/importer_test.rb @@ -16,7 +16,8 @@ class PlaidItem::ImporterTest < ActiveSupport::TestCase assert_equal PlaidMock::ITEM.institution_id, @plaid_item.institution_id assert_equal PlaidMock::ITEM.available_products, @plaid_item.available_products assert_equal PlaidMock::ITEM.billed_products, @plaid_item.billed_products - assert_not_nil @plaid_item.raw_payload - assert_not_nil @plaid_item.raw_institution_payload + + assert_equal PlaidMock::ITEM.item_id, @plaid_item.raw_payload["item_id"] + assert_equal PlaidMock::INSTITUTION.institution_id, @plaid_item.raw_institution_payload["institution_id"] end end diff --git a/test/support/plaid_mock.rb b/test/support/plaid_mock.rb index f0ab7073..eddc54b8 100644 --- a/test/support/plaid_mock.rb +++ b/test/support/plaid_mock.rb @@ -1,11 +1,19 @@ require "ostruct" +# Lightweight wrapper that allows Ostruct objects to properly serialize to JSON +# for storage on PlaidItem / PlaidAccount JSONB columns +class MockData < OpenStruct + def as_json(options = {}) + @table.as_json(options) + end +end + # A basic Plaid provider mock that returns static payloads for testing class PlaidMock TransactionSyncResponse = Struct.new(:added, :modified, :removed, :cursor, keyword_init: true) InvestmentsResponse = Struct.new(:holdings, :transactions, :securities, keyword_init: true) - ITEM = OpenStruct.new( + ITEM = MockData.new( item_id: "item_mock_1", institution_id: "ins_mock", institution_name: "Mock Institution", @@ -13,31 +21,31 @@ class PlaidMock billed_products: %w[transactions investments liabilities] ) - INSTITUTION = OpenStruct.new( + INSTITUTION = MockData.new( institution_id: "ins_mock", institution_name: "Mock Institution" ) ACCOUNTS = [ - OpenStruct.new( + MockData.new( account_id: "acc_mock_1", name: "Mock Checking", mask: "1111", type: "depository", subtype: "checking", - balances: OpenStruct.new( + balances: MockData.new( current: 1_000.00, available: 800.00, iso_currency_code: "USD" ) ), - OpenStruct.new( + MockData.new( account_id: "acc_mock_2", name: "Mock Brokerage", mask: "2222", type: "investment", subtype: "brokerage", - balances: OpenStruct.new( + balances: MockData.new( current: 15_000.00, available: 15_000.00, iso_currency_code: "USD" @@ -46,7 +54,7 @@ class PlaidMock ] SECURITIES = [ - OpenStruct.new( + MockData.new( security_id: "sec_mock_1", ticker_symbol: "AAPL", proxy_security_id: nil, @@ -55,7 +63,7 @@ class PlaidMock is_cash_equivalent: false ), # Cash security representation – used to exclude cash-equivalent holdings - OpenStruct.new( + MockData.new( security_id: "sec_mock_cash", ticker_symbol: "CUR:USD", proxy_security_id: nil, @@ -66,7 +74,7 @@ class PlaidMock ] TRANSACTIONS = [ - OpenStruct.new( + MockData.new( transaction_id: "txn_mock_1", account_id: "acc_mock_1", merchant_name: "Mock Coffee", @@ -82,7 +90,7 @@ class PlaidMock ] INVESTMENT_TRANSACTIONS = [ - OpenStruct.new( + MockData.new( investment_transaction_id: "inv_txn_mock_1", account_id: "acc_mock_2", security_id: "sec_mock_1", @@ -94,7 +102,7 @@ class PlaidMock iso_currency_code: "USD", date: Date.current.to_s ), - OpenStruct.new( + MockData.new( investment_transaction_id: "inv_txn_mock_cash", account_id: "acc_mock_2", security_id: "sec_mock_cash", @@ -109,14 +117,14 @@ class PlaidMock ] HOLDINGS = [ - OpenStruct.new( + MockData.new( account_id: "acc_mock_2", security_id: "sec_mock_1", quantity: 10, institution_price: 150.00, iso_currency_code: "USD" ), - OpenStruct.new( + MockData.new( account_id: "acc_mock_2", security_id: "sec_mock_cash", quantity: 200.0, @@ -127,22 +135,22 @@ class PlaidMock LIABILITIES = { credit: [ - OpenStruct.new( + MockData.new( account_id: "acc_mock_1", minimum_payment_amount: 25.00, - aprs: [ OpenStruct.new(apr_percentage: 19.99) ] + aprs: [ MockData.new(apr_percentage: 19.99) ] ) ], mortgage: [ - OpenStruct.new( + MockData.new( account_id: "acc_mock_3", origination_principal_amount: 250_000, origination_date: 10.years.ago.to_date.to_s, - interest_rate: OpenStruct.new(type: "fixed", percentage: 3.5) + interest_rate: MockData.new(type: "fixed", percentage: 3.5) ) ], student: [ - OpenStruct.new( + MockData.new( account_id: "acc_mock_4", origination_principal_amount: 50_000, origination_date: 6.years.ago.to_date.to_s, @@ -152,7 +160,7 @@ class PlaidMock } def get_link_token(*, **) - OpenStruct.new(link_token: "link-mock-123") + MockData.new(link_token: "link-mock-123") end def create_public_token(username: nil) @@ -160,23 +168,23 @@ class PlaidMock end def exchange_public_token(_token) - OpenStruct.new(access_token: "access-mock-123") + MockData.new(access_token: "access-mock-123") end def get_item(_access_token) - OpenStruct.new( + MockData.new( item: ITEM ) end def get_institution(institution_id) - OpenStruct.new( + MockData.new( institution: INSTITUTION ) end def get_item_accounts(_item_or_token) - OpenStruct.new(accounts: ACCOUNTS) + MockData.new(accounts: ACCOUNTS) end def get_transactions(access_token, next_cursor: nil) @@ -197,7 +205,7 @@ class PlaidMock end def get_item_liabilities(_item_or_token) - OpenStruct.new( + MockData.new( credit: LIABILITIES[:credit], mortgage: LIABILITIES[:mortgage], student: LIABILITIES[:student]