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

@@ -7,12 +7,12 @@ class GoalsController < ApplicationController
ACTIVE_STATUS_RANK = { behind: 0, on_track: 1, no_target_date: 2 }.freeze
def index
state_counts = Current.family.goals.group(:state).count
state_counts = Current.family.goals.savings.group(:state).count
@counts = STATE_FILTERS.each_with_object({}) do |state, h|
h[state] = state == "all" ? state_counts.values.sum : (state_counts[state] || 0)
end
all_goals = Current.family.goals
all_goals = Current.family.goals.savings
.alphabetically
.includes(:open_pledges, linked_accounts: :account_providers)
.to_a
@@ -46,7 +46,7 @@ class GoalsController < ApplicationController
end
def new
@goal = Current.family.goals.new(
@goal = Current.family.goals.savings.new(
color: Goal::COLORS.sample,
currency: Current.family.primary_currency_code
)
@@ -59,7 +59,7 @@ class GoalsController < ApplicationController
end
def create
@goal = Current.family.goals.new(goal_params)
@goal = Current.family.goals.savings.new(goal_params)
accounts = lookup_accounts(params.dig(:goal, :account_ids))
@goal.currency = accounts.first.currency if accounts.any? && @goal.currency.blank?
@@ -148,7 +148,7 @@ class GoalsController < ApplicationController
private
def set_goal
@goal = Current.family.goals
@goal = Current.family.goals.savings
.includes(:open_pledges, linked_accounts: :account_providers)
.find(params[:id])
end