2025-05-23 18:58:22 -04:00
|
|
|
require "test_helper"
|
|
|
|
|
|
|
|
class PlaidAccount::ProcessorTest < ActiveSupport::TestCase
|
|
|
|
setup do
|
|
|
|
@plaid_account = plaid_accounts(:one)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "processes new account and assigns attributes" do
|
|
|
|
Account.destroy_all # Clear out internal accounts so we start fresh
|
|
|
|
|
|
|
|
expect_default_subprocessor_calls
|
|
|
|
|
|
|
|
@plaid_account.update!(
|
|
|
|
plaid_id: "test_plaid_id",
|
|
|
|
plaid_type: "depository",
|
|
|
|
plaid_subtype: "checking",
|
|
|
|
current_balance: 1000,
|
|
|
|
available_balance: 1000,
|
|
|
|
currency: "USD",
|
|
|
|
name: "Test Plaid Account",
|
|
|
|
mask: "1234"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert_difference "Account.count" do
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
|
|
|
|
@plaid_account.reload
|
|
|
|
|
|
|
|
account = Account.order(created_at: :desc).first
|
|
|
|
assert_equal "Test Plaid Account", account.name
|
|
|
|
assert_equal @plaid_account.id, account.plaid_account_id
|
|
|
|
assert_equal "checking", account.subtype
|
|
|
|
assert_equal 1000, account.balance
|
|
|
|
assert_equal 1000, account.cash_balance
|
|
|
|
assert_equal "USD", account.currency
|
|
|
|
assert_equal "Depository", account.accountable_type
|
|
|
|
assert_equal "checking", account.subtype
|
|
|
|
end
|
|
|
|
|
|
|
|
test "processing is idempotent with updates and enrichments" do
|
|
|
|
expect_default_subprocessor_calls
|
|
|
|
|
|
|
|
assert_equal "Plaid Depository Account", @plaid_account.account.name
|
|
|
|
assert_equal "checking", @plaid_account.account.subtype
|
|
|
|
|
|
|
|
@plaid_account.account.update!(
|
|
|
|
name: "User updated name",
|
|
|
|
subtype: "savings",
|
|
|
|
balance: 2000 # User cannot override balance. This will be overridden by the processor on next processing
|
|
|
|
)
|
|
|
|
|
|
|
|
@plaid_account.account.lock_attr!(:name)
|
|
|
|
@plaid_account.account.lock_attr!(:subtype)
|
|
|
|
@plaid_account.account.lock_attr!(:balance) # Even if balance somehow becomes locked, Plaid ignores it and overrides it
|
|
|
|
|
|
|
|
assert_no_difference "Account.count" do
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
|
|
|
|
@plaid_account.reload
|
|
|
|
|
|
|
|
assert_equal "User updated name", @plaid_account.account.name
|
|
|
|
assert_equal "savings", @plaid_account.account.subtype
|
|
|
|
assert_equal @plaid_account.current_balance, @plaid_account.account.balance # Overriden by processor
|
|
|
|
end
|
|
|
|
|
|
|
|
test "account processing failure halts further processing" do
|
|
|
|
Account.any_instance.stubs(:save!).raises(StandardError.new("Test error"))
|
|
|
|
|
|
|
|
PlaidAccount::Transactions::Processor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Investments::TransactionsProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Investments::HoldingsProcessor.any_instance.expects(:process).never
|
|
|
|
|
|
|
|
expect_no_investment_balance_calculator_calls
|
|
|
|
expect_no_liability_processor_calls
|
|
|
|
|
|
|
|
assert_raises(StandardError) do
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "product processing failure reports exception and continues processing" do
|
|
|
|
PlaidAccount::Transactions::Processor.any_instance.stubs(:process).raises(StandardError.new("Test error"))
|
|
|
|
|
|
|
|
# Subsequent product processors still run
|
|
|
|
expect_investment_product_processor_calls
|
|
|
|
|
|
|
|
assert_nothing_raised do
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "calculates balance using BalanceCalculator for investment accounts" do
|
|
|
|
@plaid_account.update!(plaid_type: "investment")
|
|
|
|
|
2025-07-15 11:42:41 -04:00
|
|
|
# Balance is called twice: once for account.balance and once for set_current_balance
|
|
|
|
PlaidAccount::Investments::BalanceCalculator.any_instance.expects(:balance).returns(1000).twice
|
2025-05-23 18:58:22 -04:00
|
|
|
PlaidAccount::Investments::BalanceCalculator.any_instance.expects(:cash_balance).returns(1000).once
|
|
|
|
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
2025-07-15 11:42:41 -04:00
|
|
|
|
|
|
|
# Verify that the balance was set correctly
|
|
|
|
account = @plaid_account.account
|
|
|
|
assert_equal 1000, account.balance
|
|
|
|
assert_equal 1000, account.cash_balance
|
|
|
|
|
|
|
|
# Verify current balance anchor was created with correct value
|
|
|
|
current_anchor = account.valuations.current_anchor.first
|
|
|
|
assert_not_nil current_anchor
|
|
|
|
assert_equal 1000, current_anchor.entry.amount
|
2025-05-23 18:58:22 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
test "processes credit liability data" do
|
|
|
|
expect_investment_product_processor_calls
|
|
|
|
expect_no_investment_balance_calculator_calls
|
|
|
|
expect_depository_product_processor_calls
|
|
|
|
|
|
|
|
@plaid_account.update!(plaid_type: "credit", plaid_subtype: "credit card")
|
|
|
|
|
|
|
|
PlaidAccount::Liabilities::CreditProcessor.any_instance.expects(:process).once
|
|
|
|
PlaidAccount::Liabilities::MortgageProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Liabilities::StudentLoanProcessor.any_instance.expects(:process).never
|
|
|
|
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
|
|
|
|
test "processes mortgage liability data" do
|
|
|
|
expect_investment_product_processor_calls
|
|
|
|
expect_no_investment_balance_calculator_calls
|
|
|
|
expect_depository_product_processor_calls
|
|
|
|
|
|
|
|
@plaid_account.update!(plaid_type: "loan", plaid_subtype: "mortgage")
|
|
|
|
|
|
|
|
PlaidAccount::Liabilities::CreditProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Liabilities::MortgageProcessor.any_instance.expects(:process).once
|
|
|
|
PlaidAccount::Liabilities::StudentLoanProcessor.any_instance.expects(:process).never
|
|
|
|
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
|
|
|
|
test "processes student loan liability data" do
|
|
|
|
expect_investment_product_processor_calls
|
|
|
|
expect_no_investment_balance_calculator_calls
|
|
|
|
expect_depository_product_processor_calls
|
|
|
|
|
|
|
|
@plaid_account.update!(plaid_type: "loan", plaid_subtype: "student")
|
|
|
|
|
|
|
|
PlaidAccount::Liabilities::CreditProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Liabilities::MortgageProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Liabilities::StudentLoanProcessor.any_instance.expects(:process).once
|
|
|
|
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
|
2025-07-15 11:42:41 -04:00
|
|
|
test "creates current balance anchor when processing account" do
|
|
|
|
expect_default_subprocessor_calls
|
|
|
|
|
|
|
|
# Clear out accounts to start fresh
|
|
|
|
Account.destroy_all
|
|
|
|
|
|
|
|
@plaid_account.update!(
|
|
|
|
plaid_id: "test_plaid_id",
|
|
|
|
plaid_type: "depository",
|
|
|
|
plaid_subtype: "checking",
|
|
|
|
current_balance: 1500,
|
|
|
|
available_balance: 1500,
|
|
|
|
currency: "USD",
|
|
|
|
name: "Test Account with Anchor",
|
|
|
|
mask: "1234"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert_difference "Account.count", 1 do
|
|
|
|
assert_difference "Entry.count", 1 do
|
|
|
|
assert_difference "Valuation.count", 1 do
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
account = Account.order(created_at: :desc).first
|
|
|
|
assert_equal 1500, account.balance
|
|
|
|
|
|
|
|
# Verify current balance anchor was created
|
|
|
|
current_anchor = account.valuations.current_anchor.first
|
|
|
|
assert_not_nil current_anchor
|
|
|
|
assert_equal "current_anchor", current_anchor.kind
|
|
|
|
assert_equal 1500, current_anchor.entry.amount
|
|
|
|
assert_equal Date.current, current_anchor.entry.date
|
|
|
|
assert_equal "Current balance", current_anchor.entry.name
|
|
|
|
end
|
|
|
|
|
|
|
|
test "updates existing current balance anchor when reprocessing" do
|
|
|
|
# First process creates the account and anchor
|
|
|
|
expect_default_subprocessor_calls
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
|
|
|
|
account = @plaid_account.account
|
|
|
|
original_anchor = account.valuations.current_anchor.first
|
|
|
|
assert_not_nil original_anchor
|
|
|
|
original_anchor_id = original_anchor.id
|
|
|
|
original_entry_id = original_anchor.entry.id
|
|
|
|
original_balance = original_anchor.entry.amount
|
|
|
|
|
|
|
|
# Update the plaid account balance
|
|
|
|
@plaid_account.update!(current_balance: 2500)
|
|
|
|
|
|
|
|
# Expect subprocessor calls again for the second processing
|
|
|
|
expect_default_subprocessor_calls
|
|
|
|
|
|
|
|
# Reprocess should update the existing anchor
|
|
|
|
assert_no_difference "Valuation.count" do
|
|
|
|
assert_no_difference "Entry.count" do
|
|
|
|
PlaidAccount::Processor.new(@plaid_account).process
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Verify the anchor was updated
|
|
|
|
original_anchor.reload
|
|
|
|
assert_equal original_anchor_id, original_anchor.id
|
|
|
|
assert_equal original_entry_id, original_anchor.entry.id
|
|
|
|
assert_equal 2500, original_anchor.entry.amount
|
|
|
|
assert_not_equal original_balance, original_anchor.entry.amount
|
|
|
|
end
|
|
|
|
|
2025-05-23 18:58:22 -04:00
|
|
|
private
|
|
|
|
def expect_investment_product_processor_calls
|
|
|
|
PlaidAccount::Investments::TransactionsProcessor.any_instance.expects(:process).once
|
|
|
|
PlaidAccount::Investments::HoldingsProcessor.any_instance.expects(:process).once
|
|
|
|
end
|
|
|
|
|
|
|
|
def expect_depository_product_processor_calls
|
|
|
|
PlaidAccount::Transactions::Processor.any_instance.expects(:process).once
|
|
|
|
end
|
|
|
|
|
|
|
|
def expect_no_investment_balance_calculator_calls
|
|
|
|
PlaidAccount::Investments::BalanceCalculator.any_instance.expects(:balance).never
|
|
|
|
PlaidAccount::Investments::BalanceCalculator.any_instance.expects(:cash_balance).never
|
|
|
|
end
|
|
|
|
|
|
|
|
def expect_no_liability_processor_calls
|
|
|
|
PlaidAccount::Liabilities::CreditProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Liabilities::MortgageProcessor.any_instance.expects(:process).never
|
|
|
|
PlaidAccount::Liabilities::StudentLoanProcessor.any_instance.expects(:process).never
|
|
|
|
end
|
|
|
|
|
|
|
|
def expect_default_subprocessor_calls
|
|
|
|
expect_depository_product_processor_calls
|
|
|
|
expect_investment_product_processor_calls
|
|
|
|
expect_no_investment_balance_calculator_calls
|
|
|
|
expect_no_liability_processor_calls
|
|
|
|
end
|
|
|
|
end
|