mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
Fix budget category totals to net refunds against expenses
Budget spending calculations now subtract refunds (negative transactions classified as income) from expense totals in the same category. Previously, refunds were excluded entirely, causing budgets to show gross spending instead of net spending. Fixes #314
This commit is contained in:
@@ -155,11 +155,13 @@ class Budget < ApplicationRecord
|
||||
end
|
||||
|
||||
def actual_spending
|
||||
expense_totals.total
|
||||
[ expense_totals.total - refunds_in_expense_categories, 0 ].max
|
||||
end
|
||||
|
||||
def budget_category_actual_spending(budget_category)
|
||||
expense_totals.category_totals.find { |ct| ct.category.id == budget_category.category.id }&.total || 0
|
||||
expense = expense_totals.category_totals.find { |ct| ct.category.id == budget_category.category.id }&.total || 0
|
||||
refund = income_totals.category_totals.find { |ct| ct.category.id == budget_category.category.id }&.total || 0
|
||||
[ expense - refund, 0 ].max
|
||||
end
|
||||
|
||||
def category_median_monthly_expense(category)
|
||||
@@ -235,6 +237,14 @@ class Budget < ApplicationRecord
|
||||
end
|
||||
|
||||
private
|
||||
def refunds_in_expense_categories
|
||||
expense_category_ids = budget_categories.map(&:category_id).to_set
|
||||
income_totals.category_totals
|
||||
.reject { |ct| ct.category.subcategory? }
|
||||
.select { |ct| expense_category_ids.include?(ct.category.id) }
|
||||
.sum(&:total)
|
||||
end
|
||||
|
||||
def income_statement
|
||||
@income_statement ||= family.income_statement
|
||||
end
|
||||
|
||||
@@ -75,6 +75,88 @@ class BudgetTest < ActiveSupport::TestCase
|
||||
assert_nil budget.previous_budget_param
|
||||
end
|
||||
|
||||
test "actual_spending nets refunds against expenses in same category" do
|
||||
family = families(:dylan_family)
|
||||
budget = Budget.find_or_bootstrap(family, start_date: Date.current.beginning_of_month)
|
||||
|
||||
healthcare = Category.create!(
|
||||
name: "Healthcare #{Time.now.to_f}",
|
||||
family: family,
|
||||
color: "#e74c3c",
|
||||
classification: "expense"
|
||||
)
|
||||
|
||||
budget.sync_budget_categories
|
||||
budget_category = budget.budget_categories.find_by(category: healthcare)
|
||||
budget_category.update!(budgeted_spending: 200)
|
||||
|
||||
account = accounts(:depository)
|
||||
|
||||
# Create a $500 expense
|
||||
Entry.create!(
|
||||
account: account,
|
||||
entryable: Transaction.create!(category: healthcare),
|
||||
date: Date.current,
|
||||
name: "Doctor visit",
|
||||
amount: 500,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
# Create a $200 refund (negative amount = income classification in the SQL)
|
||||
Entry.create!(
|
||||
account: account,
|
||||
entryable: Transaction.create!(category: healthcare),
|
||||
date: Date.current,
|
||||
name: "Insurance reimbursement",
|
||||
amount: -200,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
# Clear memoized values
|
||||
budget = Budget.find(budget.id)
|
||||
budget.sync_budget_categories
|
||||
|
||||
# Budget category should show net spending: $500 - $200 = $300
|
||||
assert_equal 300, budget.budget_category_actual_spending(
|
||||
budget.budget_categories.find_by(category: healthcare)
|
||||
)
|
||||
end
|
||||
|
||||
test "budget_category_actual_spending does not go below zero" do
|
||||
family = families(:dylan_family)
|
||||
budget = Budget.find_or_bootstrap(family, start_date: Date.current.beginning_of_month)
|
||||
|
||||
category = Category.create!(
|
||||
name: "Returns Only #{Time.now.to_f}",
|
||||
family: family,
|
||||
color: "#3498db",
|
||||
classification: "expense"
|
||||
)
|
||||
|
||||
budget.sync_budget_categories
|
||||
budget_category = budget.budget_categories.find_by(category: category)
|
||||
budget_category.update!(budgeted_spending: 100)
|
||||
|
||||
account = accounts(:depository)
|
||||
|
||||
# Only a refund, no expense
|
||||
Entry.create!(
|
||||
account: account,
|
||||
entryable: Transaction.create!(category: category),
|
||||
date: Date.current,
|
||||
name: "Full refund",
|
||||
amount: -50,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
budget = Budget.find(budget.id)
|
||||
budget.sync_budget_categories
|
||||
|
||||
assert_equal 0, budget.budget_category_actual_spending(
|
||||
budget.budget_categories.find_by(category: category)
|
||||
)
|
||||
end
|
||||
|
||||
test "previous_budget_param returns param when date is valid" do
|
||||
budget = Budget.create!(
|
||||
family: @family,
|
||||
|
||||
Reference in New Issue
Block a user