mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 15:59:02 +00:00
Introduces retirement and FIRE (Financial Independence, Retire Early) planning as a new top-level feature in the sidebar navigation. Key features: - RetirementConfig model: stores retirement planning parameters per family (birth year, retirement age, target income, pension system, etc.) - PensionEntry model: tracks pension statements (Renteninformation) over time with pension points, current/projected monthly pension - German GRV pension calculations: - Estimated monthly pension from Entgeltpunkte x Rentenwert - After-tax pension estimation - Monthly pension gap analysis - FIRE calculations: - FIRE number (capital needed via 4% rule, inflation-adjusted) - FIRE progress percentage from current portfolio value - Estimated FIRE date (iterative monthly projection) - Required monthly savings to close pension gap - Dashboard view with overview cards, FIRE progress bar, assumptions panel, and pension history table with add/delete entries - Setup and edit views for configuring retirement parameters - Full i18n support (English + German) - Minitest coverage for models and controller Database: 2 new tables (retirement_configs, pension_entries) with UUID PKs Routes: singular resource with setup, add/destroy pension entry actions This is an initial implementation focused on the German GRV pension system. The architecture supports extending to other pension systems (custom/other). Open to suggestions and improvements from the community - contributions for additional pension systems, visualization charts, or calculation refinements are very welcome.
166 lines
4.3 KiB
Ruby
166 lines
4.3 KiB
Ruby
require "test_helper"
|
|
|
|
class RetirementControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
sign_in @user = users(:family_admin)
|
|
@family = @user.family
|
|
end
|
|
|
|
test "show redirects to setup when no config exists" do
|
|
@family.retirement_config&.destroy
|
|
get retirement_path
|
|
assert_redirected_to setup_retirement_path
|
|
end
|
|
|
|
test "show renders successfully when config exists" do
|
|
# Ensure config exists
|
|
@family.create_retirement_config!(
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR"
|
|
) unless @family.retirement_config
|
|
|
|
get retirement_path
|
|
assert_response :ok
|
|
end
|
|
|
|
test "setup renders successfully" do
|
|
get setup_retirement_path
|
|
assert_response :ok
|
|
end
|
|
|
|
test "create saves retirement config" do
|
|
@family.retirement_config&.destroy
|
|
|
|
assert_difference "RetirementConfig.count", 1 do
|
|
post retirement_path, params: {
|
|
retirement_config: {
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR",
|
|
pension_system: "de_grv",
|
|
country: "DE",
|
|
expected_return_pct: 7.0,
|
|
inflation_pct: 2.0,
|
|
tax_rate_pct: 26.38
|
|
}
|
|
}
|
|
end
|
|
|
|
assert_redirected_to retirement_path
|
|
end
|
|
|
|
test "edit renders successfully" do
|
|
@family.create_retirement_config!(
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR"
|
|
) unless @family.retirement_config
|
|
|
|
get edit_retirement_path
|
|
assert_response :ok
|
|
end
|
|
|
|
test "update modifies retirement config" do
|
|
config = @family.retirement_config || @family.create_retirement_config!(
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR"
|
|
)
|
|
|
|
patch retirement_path, params: {
|
|
retirement_config: {
|
|
target_monthly_income: 4000
|
|
}
|
|
}
|
|
|
|
assert_redirected_to retirement_path
|
|
config.reload
|
|
assert_equal 4000, config.target_monthly_income.to_i
|
|
end
|
|
|
|
test "add_pension_entry creates new entry" do
|
|
config = @family.retirement_config || @family.create_retirement_config!(
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR"
|
|
)
|
|
|
|
assert_difference "PensionEntry.count", 1 do
|
|
post add_pension_entry_retirement_path, params: {
|
|
pension_entry: {
|
|
recorded_at: "2025-01-15",
|
|
current_points: 10.5,
|
|
current_monthly_pension: 400.0,
|
|
projected_monthly_pension: 1900.0,
|
|
notes: "Test entry"
|
|
}
|
|
}
|
|
end
|
|
|
|
assert_redirected_to retirement_path
|
|
end
|
|
|
|
test "destroy_pension_entry removes entry" do
|
|
config = @family.retirement_config || @family.create_retirement_config!(
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR"
|
|
)
|
|
|
|
entry = config.pension_entries.create!(
|
|
recorded_at: "2025-06-01",
|
|
current_points: 11.0
|
|
)
|
|
|
|
assert_difference "PensionEntry.count", -1 do
|
|
delete destroy_pension_entry_retirement_path(id: entry.id)
|
|
end
|
|
|
|
assert_redirected_to retirement_path
|
|
end
|
|
|
|
test "destroy_pension_entry cannot delete another family's entry" do
|
|
# Create config and entry for the current user's family
|
|
config = @family.retirement_config || @family.create_retirement_config!(
|
|
birth_year: 1990,
|
|
retirement_age: 67,
|
|
target_monthly_income: 3000,
|
|
currency: "EUR"
|
|
)
|
|
own_entry = config.pension_entries.create!(
|
|
recorded_at: "2025-06-01",
|
|
current_points: 11.0
|
|
)
|
|
|
|
# Create config and entry for a different family
|
|
other_family = families(:empty)
|
|
other_config = other_family.retirement_config || other_family.create_retirement_config!(
|
|
birth_year: 1985,
|
|
retirement_age: 65,
|
|
target_monthly_income: 2500,
|
|
currency: "EUR"
|
|
)
|
|
other_entry = other_config.pension_entries.create!(
|
|
recorded_at: "2025-06-01",
|
|
current_points: 8.0
|
|
)
|
|
|
|
# Attempting to delete another family's entry should return 404
|
|
assert_no_difference "PensionEntry.count" do
|
|
delete destroy_pension_entry_retirement_path(id: other_entry.id)
|
|
end
|
|
|
|
assert_response :not_found
|
|
|
|
# The other family's entry should still exist
|
|
assert PensionEntry.exists?(other_entry.id)
|
|
end
|
|
end
|