Make parent budgets auto-aggregate from subcategory edits (#1312)

* Initial plan

* Auto-sum parent budgets from subcategory edits

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/f1c1b9ef-0e5d-4300-8f1b-e40876abfdcd

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Finalize subcategory budget parent aggregation

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/f1c1b9ef-0e5d-4300-8f1b-e40876abfdcd

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Address follow-up review on budget aggregation

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/b773decd-69a2-4da9-81ed-3be7d24cbb52

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>
This commit is contained in:
Copilot
2026-04-07 16:41:45 +02:00
committed by GitHub
parent d3469a91f2
commit ec1562782b
6 changed files with 176 additions and 66 deletions

View File

@@ -0,0 +1,97 @@
require "test_helper"
class BudgetCategoriesControllerTest < ActionDispatch::IntegrationTest
include ActionView::RecordIdentifier
setup do
sign_in users(:family_admin)
@budget = budgets(:one)
@family = @budget.family
@parent_category = Category.create!(
name: "Bills controller test",
family: @family,
color: "#4da568",
lucide_icon: "house"
)
@electric_category = Category.create!(
name: "Electric controller test",
parent: @parent_category,
family: @family
)
@water_category = Category.create!(
name: "Water controller test",
parent: @parent_category,
family: @family
)
@parent_budget_category = BudgetCategory.create!(
budget: @budget,
category: @parent_category,
budgeted_spending: 500,
currency: "USD"
)
@electric_budget_category = BudgetCategory.create!(
budget: @budget,
category: @electric_category,
budgeted_spending: 100,
currency: "USD"
)
@water_budget_category = BudgetCategory.create!(
budget: @budget,
category: @water_category,
budgeted_spending: 50,
currency: "USD"
)
end
test "updating a subcategory adjusts the parent budget by the same delta" do
assert_changes -> { @parent_budget_category.reload.budgeted_spending.to_f }, from: 500.0, to: 550.0 do
patch budget_budget_category_path(@budget, @electric_budget_category),
params: { budget_category: { budgeted_spending: 150 } },
as: :turbo_stream
end
assert_response :success
assert_includes @response.body, dom_id(@parent_budget_category, :form)
end
test "manual parent budget remains on top of subcategory changes" do
@parent_budget_category.update!(budgeted_spending: 900)
assert_changes -> { @parent_budget_category.reload.budgeted_spending.to_f }, from: 900.0, to: 975.0 do
patch budget_budget_category_path(@budget, @water_budget_category),
params: { budget_category: { budgeted_spending: 125 } },
as: :turbo_stream
end
end
test "sibling subcategory budget form rerenders without a max allocation cap" do
patch budget_budget_category_path(@budget, @electric_budget_category),
params: { budget_category: { budgeted_spending: 125 } },
as: :turbo_stream
assert_response :success
fragment = Nokogiri::HTML.fragment(@response.body)
input = fragment.at_css("input##{dom_id(@water_budget_category, :budgeted_spending)}")
assert_not_nil input
assert_nil input["max"]
end
test "clearing a subcategory budget switches it back to shared and lowers the parent" do
assert_changes -> { @parent_budget_category.reload.budgeted_spending.to_f }, from: 500.0, to: 400.0 do
patch budget_budget_category_path(@budget, @electric_budget_category),
params: { budget_category: { budgeted_spending: "" } },
as: :turbo_stream
end
assert_equal 0.0, @electric_budget_category.reload.budgeted_spending.to_f
end
end

View File

@@ -87,43 +87,6 @@ class BudgetCategoryTest < ActiveSupport::TestCase
assert_equal 200, @subcategory_with_limit_bc.available_to_spend
end
test "max_allocation excludes budgets of inheriting siblings" do
# Create another inheriting subcategory
another_inheriting = Category.create!(
name: "Test Coffee #{Time.now.to_f}",
parent: @parent_category,
family: @family
)
another_inheriting_bc = BudgetCategory.create!(
budget: @budget,
category: another_inheriting,
budgeted_spending: 0, # Inherits
currency: "USD"
)
# Max allocation for new subcategory should only account for the one with explicit limit (300)
# 1000 (parent) - 300 (subcategory_with_limit) = 700
assert_equal 700, another_inheriting_bc.max_allocation
# If we add a new subcategory with a limit
new_subcategory_cat = Category.create!(
name: "Test Fast Food #{Time.now.to_f}",
parent: @parent_category,
family: @family
)
new_subcategory_bc = BudgetCategory.create!(
budget: @budget,
category: new_subcategory_cat,
budgeted_spending: 0,
currency: "USD"
)
# Max should still be 700 because both inheriting subcategories don't count
assert_equal 700, new_subcategory_bc.max_allocation
end
test "percent_of_budget_spent for inheriting subcategory uses parent budget" do
# Mock spending
@budget.stubs(:budget_category_actual_spending).with(@subcategory_inheriting_bc).returns(100)
@@ -179,4 +142,31 @@ class BudgetCategoryTest < ActiveSupport::TestCase
assert_equal 800, @subcategory_with_limit_bc.available_to_spend
assert_equal 800, @subcategory_inheriting_bc.available_to_spend
end
test "update_budgeted_spending! preserves positive parent reserve when subcategory becomes individual" do
@subcategory_inheriting_bc.update_budgeted_spending!(200)
assert_equal 1200, @parent_budget_category.reload.budgeted_spending
assert_equal 200, @subcategory_inheriting_bc.reload.budgeted_spending
refute @subcategory_inheriting_bc.reload.inherits_parent_budget?
end
test "update_budgeted_spending! lowers parent when subcategory returns to shared" do
@subcategory_with_limit_bc.update_budgeted_spending!(0)
assert_equal 700, @parent_budget_category.reload.budgeted_spending
assert @subcategory_with_limit_bc.reload.inherits_parent_budget?
end
test "update_budgeted_spending! does not preserve a negative parent reserve" do
# Create an artificial inconsistent parent total to verify recovery behavior.
@parent_budget_category.update!(budgeted_spending: 50)
@subcategory_inheriting_bc.update!(budgeted_spending: 50)
@subcategory_with_limit_bc.update_budgeted_spending!(20)
assert_equal 70, @parent_budget_category.reload.budgeted_spending
assert_equal 20, @subcategory_with_limit_bc.reload.budgeted_spending
assert_equal 50, @subcategory_inheriting_bc.reload.budgeted_spending
end
end