From e05869a4f4a45f93e9207accbc296f4cd5e17971 Mon Sep 17 00:00:00 2001 From: Garrett Date: Sat, 7 Feb 2026 01:47:39 +0000 Subject: [PATCH] feat: Allow creating a budget up to 2 years ahead --- app/models/budget.rb | 21 +++++++---- test/models/budget_test.rb | 74 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/app/models/budget.rb b/app/models/budget.rb index c345801fc..eee6a3254 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -29,13 +29,14 @@ class Budget < ApplicationRecord end def budget_date_valid?(date, family:) - if family.uses_custom_month_start? - budget_start = family.custom_month_start_for(date) - budget_start >= oldest_valid_budget_date(family) && budget_start <= family.custom_month_end_for(Date.current) + budget_start = if family.uses_custom_month_start? + family.custom_month_start_for(date) else - beginning_of_month = date.beginning_of_month - beginning_of_month >= oldest_valid_budget_date(family) && beginning_of_month <= Date.current.end_of_month + date.beginning_of_month end + + budget_start >= oldest_valid_budget_date(family) && + budget_start <= latest_valid_budget_start_date(family) end def find_or_bootstrap(family, start_date:) @@ -70,6 +71,14 @@ class Budget < ApplicationRecord oldest_entry_date = family.oldest_entry_date.beginning_of_month [ two_years_ago, oldest_entry_date ].min end + + def latest_valid_budget_start_date(family) + if family.uses_custom_month_start? + family.current_custom_month_period.start_date + 2.years + else + Date.current.beginning_of_month + 2.years + end + end end def period @@ -151,8 +160,6 @@ class Budget < ApplicationRecord end def next_budget_param - return nil if current? - next_date = start_date + 1.month return nil unless self.class.budget_date_valid?(next_date, family: family) diff --git a/test/models/budget_test.rb b/test/models/budget_test.rb index cd3e95307..b8cc8bdf2 100644 --- a/test/models/budget_test.rb +++ b/test/models/budget_test.rb @@ -58,8 +58,35 @@ class BudgetTest < ActiveSupport::TestCase 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) + test "budget_date_valid? allows future dates up to 2 years ahead" do + travel_to Date.current.beginning_of_month do + assert Budget.budget_date_valid?(Date.current.beginning_of_month + 1.month, family: @family) + assert Budget.budget_date_valid?(Date.current.beginning_of_month + 2.years, family: @family) + end + end + + test "budget_date_valid? does not allow future dates beyond 2 years ahead" do + travel_to Date.current.beginning_of_month do + refute Budget.budget_date_valid?(Date.current.beginning_of_month + 2.years + 1.month, family: @family) + end + end + + test "budget_date_valid? for custom month start allows dates up to 2 years ahead" do + @family.update!(month_start_day: 15) + + travel_to Date.current.beginning_of_month do + cap_start = @family.current_custom_month_period.start_date + 2.years + assert Budget.budget_date_valid?(cap_start, family: @family) + end + end + + test "budget_date_valid? for custom month start does not allow dates beyond 2 years ahead" do + @family.update!(month_start_day: 15) + + travel_to Date.current.beginning_of_month do + beyond_cap = @family.current_custom_month_period.start_date + 2.years + 1.month + refute Budget.budget_date_valid?(beyond_cap, family: @family) + end end test "previous_budget_param returns nil when date is too old" do @@ -75,6 +102,49 @@ class BudgetTest < ActiveSupport::TestCase assert_nil budget.previous_budget_param end + test "next_budget_param returns next month when current month budget is selected" do + travel_to Date.current.beginning_of_month do + budget = Budget.create!( + family: @family, + start_date: Date.current.beginning_of_month, + end_date: Date.current.end_of_month, + currency: "USD" + ) + + assert_equal Budget.date_to_param(Date.current.beginning_of_month + 1.month), budget.next_budget_param + end + end + + test "next_budget_param returns nil at future cap" do + travel_to Date.current.beginning_of_month do + cap_start = Date.current.beginning_of_month + 2.years + budget = Budget.create!( + family: @family, + start_date: cap_start, + end_date: cap_start.end_of_month, + currency: "USD" + ) + + assert_nil budget.next_budget_param + end + end + + test "next_budget_param returns nil at future cap for custom month start" do + @family.update!(month_start_day: 15) + + travel_to Date.current.beginning_of_month do + cap_start = @family.current_custom_month_period.start_date + 2.years + budget = Budget.create!( + family: @family, + start_date: cap_start, + end_date: cap_start + 1.month - 1.day, + currency: "USD" + ) + + assert_nil budget.next_budget_param + end + 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)