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
* Handle missing git binary in commit_sha initializer
Rescues Errno::ENOENT when git is not installed, falling back to
BUILD_COMMIT_SHA env var or "unknown". Fixes crash in Docker
development containers that lack git.
* Revert "Handle missing git binary in commit_sha initializer"
This reverts commit 7e58458faa.
* Subtract uncategorized refunds from overall budget spending
Uncategorized refunds were not being netted against actual_spending
because the synthetic uncategorized category has no persisted ID and
wasn't matched by the budget_categories ID set. Now checks for
category.uncategorized? in addition to the ID lookup.
* perf: optimize budget category actual spending calculation
213 lines
6.2 KiB
Ruby
213 lines
6.2 KiB
Ruby
require "test_helper"
|
|
|
|
class BudgetTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:empty)
|
|
end
|
|
|
|
test "budget_date_valid? allows going back 2 years even without entries" do
|
|
two_years_ago = 2.years.ago.beginning_of_month
|
|
assert Budget.budget_date_valid?(two_years_ago, family: @family)
|
|
end
|
|
|
|
test "budget_date_valid? allows going back to earliest entry date if more than 2 years ago" do
|
|
# Create an entry 3 years ago
|
|
old_account = Account.create!(
|
|
family: @family,
|
|
accountable: Depository.new,
|
|
name: "Old Account",
|
|
status: "active",
|
|
currency: "USD",
|
|
balance: 1000
|
|
)
|
|
|
|
old_entry = Entry.create!(
|
|
account: old_account,
|
|
entryable: Transaction.new(category: categories(:income)),
|
|
date: 3.years.ago,
|
|
name: "Old Transaction",
|
|
amount: 100,
|
|
currency: "USD"
|
|
)
|
|
|
|
# Should allow going back to the old entry date
|
|
assert Budget.budget_date_valid?(3.years.ago.beginning_of_month, family: @family)
|
|
end
|
|
|
|
test "budget_date_valid? does not allow dates before earliest entry or 2 years ago" do
|
|
# Create an entry 1 year ago
|
|
account = Account.create!(
|
|
family: @family,
|
|
accountable: Depository.new,
|
|
name: "Test Account",
|
|
status: "active",
|
|
currency: "USD",
|
|
balance: 500
|
|
)
|
|
|
|
Entry.create!(
|
|
account: account,
|
|
entryable: Transaction.new(category: categories(:income)),
|
|
date: 1.year.ago,
|
|
name: "Recent Transaction",
|
|
amount: 100,
|
|
currency: "USD"
|
|
)
|
|
|
|
# Should not allow going back more than 2 years
|
|
refute Budget.budget_date_valid?(3.years.ago.beginning_of_month, family: @family)
|
|
end
|
|
|
|
test "budget_date_valid? does not allow future dates beyond current month" do
|
|
refute Budget.budget_date_valid?(2.months.from_now, family: @family)
|
|
end
|
|
|
|
test "previous_budget_param returns nil when date is too old" do
|
|
# Create a budget at the oldest allowed date
|
|
two_years_ago = 2.years.ago.beginning_of_month
|
|
budget = Budget.create!(
|
|
family: @family,
|
|
start_date: two_years_ago,
|
|
end_date: two_years_ago.end_of_month,
|
|
currency: "USD"
|
|
)
|
|
|
|
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 "actual_spending subtracts uncategorized refunds" 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 purchase",
|
|
amount: 400,
|
|
currency: "USD"
|
|
)
|
|
|
|
# Create an uncategorized refund
|
|
Entry.create!(
|
|
account: account,
|
|
entryable: Transaction.create!(category: nil),
|
|
date: Date.current,
|
|
name: "Uncategorized refund",
|
|
amount: -150,
|
|
currency: "USD"
|
|
)
|
|
|
|
budget = Budget.find(budget.id)
|
|
budget.sync_budget_categories
|
|
|
|
# The uncategorized refund should reduce overall actual_spending
|
|
# Other fixtures may contribute spending, so check that the net
|
|
# uncategorized amount (400 - 150 = 250) is reflected by comparing
|
|
# with and without the refund rather than asserting an exact total.
|
|
spending_with_refund = budget.actual_spending
|
|
|
|
# Remove the refund and check spending increases
|
|
Entry.find_by(name: "Uncategorized refund").destroy!
|
|
budget = Budget.find(budget.id)
|
|
spending_without_refund = budget.actual_spending
|
|
|
|
assert_equal 150, spending_without_refund - spending_with_refund
|
|
end
|
|
|
|
test "previous_budget_param returns param when date is valid" do
|
|
budget = Budget.create!(
|
|
family: @family,
|
|
start_date: Date.current.beginning_of_month,
|
|
end_date: Date.current.end_of_month,
|
|
currency: "USD"
|
|
)
|
|
|
|
assert_not_nil budget.previous_budget_param
|
|
end
|
|
end
|