Fix nil-key collision in budget category hash lookups (#1136)

Both Uncategorized and Other Investments are synthetic categories with
id=nil. When expense_totals_by_category indexes by category.id, Other
Investments overwrites Uncategorized at the nil key, causing uncategorized
actual spending to always return 0.

Use category.name as fallback key (id || name) to differentiate the two
synthetic categories in all hash builders and lookup sites.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Juan Manuel Reyes
2026-03-06 14:24:33 -08:00
committed by GitHub
parent f8d3678a40
commit 388f249e4e
2 changed files with 31 additions and 5 deletions

View File

@@ -218,9 +218,9 @@ class Budget < ApplicationRecord
end
def budget_category_actual_spending(budget_category)
cat_id = budget_category.category_id
expense = expense_totals_by_category[cat_id]&.total || 0
refund = income_totals_by_category[cat_id]&.total || 0
key = budget_category.category_id || budget_category.category.name
expense = expense_totals_by_category[key]&.total || 0
refund = income_totals_by_category[key]&.total || 0
[ expense - refund, 0 ].max
end
@@ -318,10 +318,10 @@ class Budget < ApplicationRecord
end
def expense_totals_by_category
@expense_totals_by_category ||= expense_totals.category_totals.index_by { |ct| ct.category.id }
@expense_totals_by_category ||= expense_totals.category_totals.index_by { |ct| ct.category.id || ct.category.name }
end
def income_totals_by_category
@income_totals_by_category ||= income_totals.category_totals.index_by { |ct| ct.category.id }
@income_totals_by_category ||= income_totals.category_totals.index_by { |ct| ct.category.id || ct.category.name }
end
end

View File

@@ -304,4 +304,30 @@ class BudgetTest < ActiveSupport::TestCase
assert_not_nil budget.previous_budget_param
end
test "uncategorized budget category actual spending reflects uncategorized transactions" do
family = families(:dylan_family)
budget = Budget.find_or_bootstrap(family, start_date: Date.current.beginning_of_month)
account = accounts(:depository)
# Create an uncategorized expense
Entry.create!(
account: account,
entryable: Transaction.create!(category: nil),
date: Date.current,
name: "Uncategorized lunch",
amount: 75,
currency: "USD"
)
budget = Budget.find(budget.id)
budget.sync_budget_categories
uncategorized_bc = budget.uncategorized_budget_category
spending = budget.budget_category_actual_spending(uncategorized_bc)
# Must be > 0 — the nil-key collision between Uncategorized and
# Other Investments synthetic categories previously caused this to return 0
assert spending >= 75, "Uncategorized actual spending should include the $75 transaction, got #{spending}"
end
end