Files
sure/app/controllers/retirement_controller.rb
Guillem Arias ec6fc1d685 feat(retirement): PR4b "Why this target?" card + trimmed-mean anchor
- IncomeStatement#trimmed_mean_expense(months:, trim_pct:) — trailing-N-
  month mean monthly expense with the top/bottom trim_pct% of months
  dropped, so one-off spikes don't skew the anchor. Family#retirement_
  spending_baseline now uses it (was median).
- Goal::Retirement#fi_number — 25× the annual target (4% rule).
- "Why this target?" card on the show page: Last-12-months anchor →
  Target → FI number (25×), with a "Use my average" button that sets
  target_spend to the trimmed-mean baseline. Money is privacy-sensitive.
- Header gains a green-dot "Active plan" DS::Pill badge when projectable.

Tests: trimmed_mean returns non-negative; fi_number = 25× annual target;
baseline returns Money. Rubocop + erb_lint clean.
2026-05-29 12:34:40 +02:00

43 lines
1.5 KiB
Ruby

class RetirementController < ApplicationController
include RetirementScoped
def show
@glide = @plan.glide_payload
@baseline = Current.family.retirement_spending_baseline(user: Current.user)
@pension_sources = @plan.pension_sources.order(:start_age)
@adjustments = @plan.adjustments.ordered
@statements = @plan.statements.chronological.reverse
@bucket_account_ids = @plan.retirement_bucket_entries.pluck(:account_id).to_set
@bucket_candidates = Current.family.accounts.visible.alphabetically
@breadcrumbs = [
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.retirement"), nil ]
]
end
def update
if @plan.update(retirement_params: merged_plan_params)
redirect_to retirement_path, notice: t(".updated")
else
redirect_to retirement_path, alert: @plan.errors.full_messages.to_sentence
end
end
# Live what-if: recompute against transient inputs WITHOUT persisting, and
# stream the KPI cards back. The plan is only saved via #update.
def forecast
@plan.assign_attributes(retirement_params: merged_plan_params)
render turbo_stream: turbo_stream.replace(
"retirement_kpis", partial: "retirement/kpis", locals: { plan: @plan }
)
end
private
def merged_plan_params
raw = params.fetch(:retirement, {}).permit(
:birth_year, :retire_age, :target_spend, :monthly_savings, :real_return_pct
).to_h
(@plan.retirement_params || {}).merge(raw.reject { |_, v| v.to_s.strip.empty? })
end
end