feat(retirement): PR1 scaffold + preview-gated /retirement page

Lays the foundation for Retirement v2 as a preview feature stacked on
Goals v2. Math, lens UI, pension sources and bucket all defer to later
PRs; this PR ships only the data-model spine and a placeholder landing.

- STI on goals: add `type` (default "Goal") + `user_id` columns;
  partial index for `Goal::Retirement` rows; check constraint
  requiring an owner on retirement rows. Existing goals backfill to
  `type='Goal'`; base `Goal#editable_by?` stays family-scoped.
- `Goal::Retirement` subclass with single-user owner and
  `editable_by?` narrowed to owner-only. Parent depository-only
  linked-account validations no-op'd; PR2 introduces
  `RetirementBucketEntry`.
- `families.retirement_disabled` killswitch (default false) +
  `Family#retirement_enabled?(user)` helper as tier 2 of the gate.
  Tier 1 is the existing `PreviewGateable` flow.
- `RetirementController#show`: `require_preview_features!` then
  `ensure_module_enabled!` then a placeholder body. Unknown to users
  without preview features; 404 when the family killswitch is on (the
  feature behaves as if it does not exist).
- Sidebar: new `sun`-icon entry after Goals, hidden unless the user has
  preview features AND the family has retirement enabled, so the
  killswitch hides the nav rather than leaving a link that 404s.
- Locales: EN copy for nav, breadcrumb, page header, placeholder body,
  and the new `owner.must_belong_to_family` validation message under
  the goal model. DE deferred to PR4.
- Tests: STI roundtrip, owner presence + family-membership
  validations, `editable_by?` on both Goal and Goal::Retirement, gate
  matrix on the controller, nav-item visibility under both preview and
  family flags, base-row STI backfill.

Stack ahead: PR2 ships the data plane (PensionSource, statements,
adjustments, bucket entries); PR3 wires the `Retirement::Fire::*`
forecast engine + WHAT-IF Turbo Stream slider loop; PR4 lands the
single combined-page UI per Claude's 2026-05-29 design (glide chart
with hover-tooltip income breakdown, no separate stacked-area chart).
This commit is contained in:
Guillem Arias
2026-05-29 09:18:06 +02:00
parent e401f43fe1
commit ca73a2f389
17 changed files with 313 additions and 12 deletions

View File

@@ -0,0 +1,58 @@
require "test_helper"
class RetirementControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:family_admin)
@family = @user.family
@user.update!(preferences: (@user.preferences || {}).merge("preview_features_enabled" => true))
@family.update!(retirement_disabled: false)
sign_in @user
ensure_tailwind_build
end
test "redirects when preview features disabled" do
@user.update!(preferences: (@user.preferences || {}).merge("preview_features_enabled" => false))
get retirement_url
assert_redirected_to root_path
assert_match(/preview/i, flash[:alert])
end
test "404 when family retirement_disabled is true" do
@family.update!(retirement_disabled: true)
get retirement_url
assert_response :not_found
end
test "200 when preview features and family flag both allow" do
get retirement_url
assert_response :success
assert_match(/Retirement/i, response.body)
end
test "nav item rendered when preview enabled" do
get root_url
assert_select "a[href=?]", retirement_path
end
test "nav item hidden when preview disabled" do
@user.update!(preferences: (@user.preferences || {}).merge("preview_features_enabled" => false))
get root_url
assert_select "a[href=?]", retirement_path, count: 0
end
test "nav item hidden when family retirement disabled" do
@family.update!(retirement_disabled: true)
get root_url
assert_select "a[href=?]", retirement_path, count: 0
end
end