Files
sure/app/models/pension_source.rb
Guillem Arias bf0f10c21f feat(retirement): PR2 data models — pension sources, statements, bucket
Data plane for Retirement v2 (no FIRE math yet — that is PR3). Five
migrations + four AR models, wired to Goal::Retirement.

Models:
- PensionSource — state/workplace/other source with country, pension
  system, tax treatment, payout shape (string-backed + inclusion
  validations rather than PG enums, so v2 can add countries without
  ALTER TYPE). monetize :amount; end_age required for fixed-term.
- Goal::RetirementStatement — append-only audit journal. default_scope
  excludes soft-deleted rows; soft_replace! does soft-delete + insert;
  points_delta drives the "—"/signed Δ column; monetize against
  projected_currency.
- Goal::RetirementAdjustment — signed today's-money deltas to the
  spending target, ordered, applicable_at?(age).
- RetirementBucketEntry — account selection join, unique per plan,
  same-family guard.

Goal::Retirement gains the four associations + bucket_accounts and an
ADJUSTMENTS_LIMIT (10) cap. retirement_params jsonb added to goals for
PR3 plan settings.

Namespaced fixture classes mapped via set_fixture_class so the
goal_retirement association resolves. Minimal fixtures + model tests
(112 runs green, incl. goal/family/controller regression sweep). No
new gems.
2026-05-29 10:36:18 +02:00

46 lines
1.8 KiB
Ruby

class PensionSource < ApplicationRecord
include Monetizable
KINDS = %w[state workplace other].freeze
COUNTRIES = %w[DE US UK].freeze
PENSION_SYSTEMS = %w[de_grv de_bav de_riester us_ss uk_state uk_workplace custom].freeze
TAX_TREATMENTS = %w[
de_renten de_bav de_riester de_private
uk_state uk_dc_drawdown uk_dc_25pct uk_isa
custom_post_tax
].freeze
PAYOUT_SHAPES = %w[monthly_for_life monthly_fixed_term lump_sum lump_plus_annuity].freeze
belongs_to :goal_retirement, class_name: "Goal::Retirement", foreign_key: :goal_retirement_id
has_many :statements, class_name: "Goal::RetirementStatement", dependent: :destroy
validates :name, presence: true, length: { maximum: 255 }
validates :kind, inclusion: { in: KINDS }
validates :country, inclusion: { in: COUNTRIES }
validates :pension_system, inclusion: { in: PENSION_SYSTEMS }
validates :tax_treatment, inclusion: { in: TAX_TREATMENTS }
validates :payout_shape, inclusion: { in: PAYOUT_SHAPES }
validates :start_age, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 }
validates :end_age,
numericality: { only_integer: true, greater_than: :start_age, less_than_or_equal_to: 120 },
allow_nil: true
validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :currency, presence: true
validates :effective_rate_override,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
validate :end_age_required_for_fixed_term
monetize :amount
def latest_statement
statements.order(:received_on).last
end
private
def end_age_required_for_fixed_term
return unless payout_shape == "monthly_fixed_term"
errors.add(:end_age, :required_for_fixed_term) if end_age.blank?
end
end