Auto-categorize investment contributions across all transfer paths (#924)

* Ensure investment contributions are auto-categorized with proper kind and category creation.

* Retrigger CI
This commit is contained in:
LPW
2026-02-07 10:41:31 -05:00
committed by GitHub
parent fc3504abe0
commit 36661bdc9b
10 changed files with 193 additions and 5 deletions

View File

@@ -39,4 +39,24 @@ class TransferMatchesControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to transactions_url
assert_equal "Transfer created", flash[:notice]
end
test "assigns investment_contribution kind and category for investment destination" do
# Outflow from depository (positive amount), target is investment
outflow_entry = create_transaction(amount: 100, account: accounts(:depository))
post transaction_transfer_match_path(outflow_entry), params: {
transfer_match: {
method: "new",
target_account_id: accounts(:investment).id
}
}
outflow_entry.reload
outflow_txn = outflow_entry.entryable
assert_equal "investment_contribution", outflow_txn.kind
category = @user.family.investment_contributions_category
assert_equal category, outflow_txn.category
end
end

View File

@@ -109,6 +109,19 @@ class Family::AutoTransferMatchableTest < ActiveSupport::TestCase
end
end
test "auto-matched cash to investment assigns investment contribution category" do
investment = accounts(:investment)
outflow_entry = create_transaction(date: Date.current, account: @depository, amount: 500)
inflow_entry = create_transaction(date: Date.current, account: investment, amount: -500)
@family.auto_match_transfers!
outflow_entry.reload
category = @family.investment_contributions_category
assert_equal category, outflow_entry.entryable.category
end
test "does not match multi-currency transfer with missing exchange rate" do
create_transaction(date: Date.current, account: @depository, amount: 500)
create_transaction(date: Date.current, account: @credit_card, amount: -700, currency: "GBP")

View File

@@ -7,6 +7,35 @@ class FamilyTest < ActiveSupport::TestCase
@syncable = families(:dylan_family)
end
test "investment_contributions_category creates category when missing" do
family = families(:dylan_family)
family.categories.where(name: Category.investment_contributions_name).destroy_all
assert_nil family.categories.find_by(name: Category.investment_contributions_name)
category = family.investment_contributions_category
assert category.persisted?
assert_equal Category.investment_contributions_name, category.name
assert_equal "#0d9488", category.color
assert_equal "expense", category.classification
assert_equal "trending-up", category.lucide_icon
end
test "investment_contributions_category returns existing category" do
family = families(:dylan_family)
existing = family.categories.find_or_create_by!(name: Category.investment_contributions_name) do |c|
c.color = "#0d9488"
c.classification = "expense"
c.lucide_icon = "trending-up"
end
assert_no_difference "Category.count" do
result = family.investment_contributions_category
assert_equal existing, result
end
end
test "available_merchants includes family merchants without transactions" do
family = families(:dylan_family)

View File

@@ -149,6 +149,70 @@ class PlaidAccount::Investments::TransactionsProcessorTest < ActiveSupport::Test
assert_equal -1, entry.trade.qty
end
test "creates contribution transactions as cash transactions" do
test_investments_payload = {
transactions: [
{
"investment_transaction_id" => "contrib_123",
"type" => "contribution",
"amount" => -500.0,
"iso_currency_code" => "USD",
"date" => Date.current,
"name" => "401k Contribution"
}
]
}
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve).never
processor = PlaidAccount::Investments::TransactionsProcessor.new(@plaid_account, security_resolver: @security_resolver)
assert_difference [ "Entry.count", "Transaction.count" ], 1 do
processor.process
end
entry = Entry.order(created_at: :desc).first
assert_equal(-500.0, entry.amount)
assert_equal "USD", entry.currency
assert_equal "401k Contribution", entry.name
assert_instance_of Transaction, entry.entryable
end
test "creates withdrawal transactions as cash transactions" do
test_investments_payload = {
transactions: [
{
"investment_transaction_id" => "withdraw_123",
"type" => "withdrawal",
"amount" => 1000.0,
"iso_currency_code" => "USD",
"date" => Date.current,
"name" => "IRA Withdrawal"
}
]
}
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve).never
processor = PlaidAccount::Investments::TransactionsProcessor.new(@plaid_account, security_resolver: @security_resolver)
assert_difference [ "Entry.count", "Transaction.count" ], 1 do
processor.process
end
entry = Entry.order(created_at: :desc).first
assert_equal 1000.0, entry.amount
assert_equal "USD", entry.currency
assert_equal "IRA Withdrawal", entry.name
assert_instance_of Transaction, entry.entryable
end
test "creates transfer transactions as cash transactions" do
test_investments_payload = {
transactions: [

View File

@@ -165,6 +165,30 @@ class Rule::ActionTest < ActiveSupport::TestCase
end
end
test "set_as_transfer_or_payment assigns investment_contribution kind and category for investment destination" do
investment = accounts(:investment)
action = Rule::Action.new(
rule: @transaction_rule,
action_type: "set_as_transfer_or_payment",
value: investment.id
)
# Only apply to txn1 (positive amount = outflow)
action.apply(Transaction.where(id: @txn1.id))
@txn1.reload
transfer = Transfer.find_by(outflow_transaction_id: @txn1.id) || Transfer.find_by(inflow_transaction_id: @txn1.id)
assert transfer.present?, "Transfer should be created"
assert_equal "investment_contribution", transfer.outflow_transaction.kind
assert_equal "funds_movement", transfer.inflow_transaction.kind
category = @family.investment_contributions_category
assert_equal category, transfer.outflow_transaction.category
end
test "set_investment_activity_label ignores invalid values" do
action = Rule::Action.new(
rule: @transaction_rule,