fix(retirement): isolate retirement goals from savings goal routes

Addresses Codex P2 on #2044. A Goal::Retirement row lives in
Current.family.goals, so the shared GoalsController and
GoalPledgesController loaded it through `family.goals.find(...)` —
never calling Goal::Retirement#editable_by?. Any preview-enabled
family member could therefore open /goals/:id and edit/archive/delete
another member's owner-scoped retirement plan, hit its pledge routes,
and see it listed in the savings Goals grid.

Adds `Goal.savings` (base type only) and scopes both savings
controllers to it, so retirement goals are unreachable through the
shared routes (RecordNotFound -> goals_path redirect) and absent from
the savings index. Owner-only retirement access stays in
RetirementController; editable_by? is retained for it.

Tests: savings scope excludes retirement; retirement goal absent from
goals index; show + pledge routes redirect not-found for retirement.

(The Codex schema.rb null:false finding is a false positive — this
branch's schema.rb retains null:false on all IBKR payload columns and
the diff vs the base branch touches no IBKR lines; Codex compared
against main rather than the PR base.)
This commit is contained in:
Guillem Arias
2026-05-29 10:25:05 +02:00
parent ca73a2f389
commit 839d6b36ad
6 changed files with 58 additions and 6 deletions

View File

@@ -239,4 +239,28 @@ class GoalsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to goals_path
assert_equal I18n.t("goals.errors.not_found"), flash[:alert]
end
test "retirement goals are excluded from the savings index" do
Goal::Retirement.create!(
family: @user.family, owner: @user,
name: "My Retirement Plan", target_amount: 1_000_000, currency: "USD"
)
get goals_url
assert_response :success
assert_no_match(/My Retirement Plan/, response.body)
end
test "retirement goal is not reachable via the savings show route" do
retirement = Goal::Retirement.create!(
family: @user.family, owner: @user,
name: "Retire", target_amount: 1_000_000, currency: "USD"
)
get goal_url(retirement)
assert_redirected_to goals_path
assert_equal I18n.t("goals.errors.not_found"), flash[:alert]
end
end