Commit Graph

5 Commits

Author SHA1 Message Date
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
Guillem Arias
ee9f5d8b63 feat(retirement): PR4a glide-path chart (D3)
The dashboard centerpiece. Goal::Retirement#glide_payload derives, from
the forecast, the active-plan series + a zero-savings (Walletburst)
shadow + a ±1pp real-return band + the per-age income breakdown for the
hover tooltip + lump markers + the retire/Coast crossover points (three
extra deterministic Forecast runs; cheap).

retirement_glide_chart_controller (D3, mirrors goal_projection_chart's
import / ResizeObserver / theme-observer idiom): portfolio-by-age line +
area, accumulation/drawdown phase shading, the ±1pp band cone, the
dashed Walletburst shadow, a "Retire · age N / $X" chip on the retire
line, a blue Coast crossover ring, purple lump bars, and a hover tooltip
(PR #2029 bg-container/rounded-xl/shadow style) showing the monthly
State / Workplace / Drawdown breakdown + Total-vs-target with a Covered
badge. Wired into the show page above the what-if; container is
privacy-sensitive.

Browser-verified: renders the band, shading, retire chip ($571K), Coast
dot, and shadow against the demo plan. glide_payload + lump_markers
unit-tested. Rubocop + erb_lint + biome clean.

Remaining for PR4: DS::SelectableCard bucket, "Why this target?" anchor
card, skinned DS::Dialog deletes, DE locale, demo seed, system test.
2026-05-29 11:58:56 +02:00
Guillem Arias
174dd66914 feat(retirement): PR3b what-if KPIs + live forecast Turbo Stream
Surfaces the forecast on the page and makes the levers live.

- KPI cards (_kpis): Freedom date, Coast FIRE, Money-lasts-to + terminal
  value, with a "set your birth year" prompt until a plan is projectable.
  Wrapped in #retirement_kpis for Turbo Stream replacement; money carries
  privacy-sensitive.
- What-if form: birth_year / retire_age / target_spend / monthly_savings /
  real_return_pct. On input, retirement_what_if_controller debounces and
  POSTs the current values to PATCH /retirement/forecast, which recomputes
  against transient inputs and streams the KPI cards back WITHOUT
  persisting. "Save plan" submits to #update to persist retirement_params.
- RetirementController gains #update (persist) and #forecast (transient
  recompute → turbo_stream). Both reuse merged_plan_params, which drops
  blank fields so a partial what-if doesn't clobber stored values.

Tests: KPI section renders; update persists params; forecast streams
#retirement_kpis without writing the slider value back. Rubocop +
erb_lint + biome clean.

PR4 replaces this minimal form with the designed slider rail + glide
chart; the #forecast endpoint and the engine stay.
2026-05-29 11:45:31 +02:00
Guillem Arias
26bb333c34 feat(retirement): PR2 CRUD for sources, statements, adjustments, bucket
Functional data-entry surface on the (still preview) /retirement page.
The polished combined-page UI is PR4; this ships plain forms + lists so
a preview user can populate a plan end to end.

- RetirementScoped concern: tier-1 preview gate + tier-2 family
  killswitch + per-owner plan bootstrap (Goal::Retirement.for_owner
  find-or-creates, so children always have a parent). RetirementController
  now uses it.
- Nested controllers under Retirement::: PensionSources (full CRUD),
  Statements (new/create + soft-delete destroy — append-only audit),
  Adjustments (full CRUD), Buckets (replace-all account selection,
  same-family filtered). All scoped to the current user's own plan, so
  cross-user access is impossible by construction.
- Routes nested under `resource :retirement` via `scope module:`.
- Views: show page rewritten into management sections (sources,
  adjustments, bucket checkboxes, statement journal) + plain
  styled_form_with forms. Money carries privacy-sensitive.
- Goal gains a target_amount_required? hook (true); Goal::Retirement
  overrides it false — the forecast owns the target (PR3), so a plan
  can exist before any target is set.
- EN locale for the new surface. 111 controller+model tests green.

Note: delete uses Turbo confirm for now; PR4 swaps in the skinned
DS::Dialog per the design.
2026-05-29 10:49:18 +02:00
Guillem Arias
ca73a2f389 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).
2026-05-29 09:24:47 +02:00