mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
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.
55 lines
1.7 KiB
Ruby
55 lines
1.7 KiB
Ruby
class Goal::RetirementStatement < ApplicationRecord
|
|
include Monetizable
|
|
|
|
self.table_name = "goal_retirement_statements"
|
|
|
|
belongs_to :goal_retirement, class_name: "Goal::Retirement", foreign_key: :goal_retirement_id
|
|
belongs_to :pension_source
|
|
|
|
validates :received_on, presence: true
|
|
validates :projected_monthly_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
validates :projected_currency, presence: true
|
|
|
|
# Append-only audit: soft-deleted rows stay in the table for history but
|
|
# drop out of every normal read. Edits go through soft_replace!.
|
|
default_scope { where(deleted: false) }
|
|
|
|
scope :chronological, -> { order(:received_on) }
|
|
|
|
monetize :projected_monthly_amount
|
|
|
|
# Δ in pension points vs the prior active statement for the same source.
|
|
# nil for the earliest row (renders "—" in the journal).
|
|
def points_delta
|
|
return nil if current_points.nil?
|
|
|
|
prior = pension_source.statements
|
|
.where("received_on < ?", received_on)
|
|
.chronological
|
|
.last
|
|
return nil if prior.nil? || prior.current_points.nil?
|
|
|
|
current_points - prior.current_points
|
|
end
|
|
|
|
# Edit = soft-delete this row + insert a replacement, preserving the
|
|
# audit trail. Returns the new statement.
|
|
def soft_replace!(attrs)
|
|
new_statement = nil
|
|
self.class.transaction do
|
|
update_column(:deleted, true)
|
|
new_statement = pension_source.statements.create!(
|
|
attributes
|
|
.except("id", "deleted", "created_at", "updated_at")
|
|
.merge(attrs.stringify_keys)
|
|
)
|
|
end
|
|
new_statement
|
|
end
|
|
|
|
private
|
|
def monetizable_currency
|
|
projected_currency
|
|
end
|
|
end
|