mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
Generalize pension system: multi-country strategy pattern
- Add JSONB pension_params column to retirement_configs
- Add data JSONB column to pension_entries
- Create pension calculator strategy classes (Base, DeGrv, UsSocialSecurity, UkStatePension, FrRegimeGeneral, EsSocialSecurity)
- Update RetirementConfig model to delegate to calculators
- Make PensionEntry.current_points optional for non-points systems
- Update controller strong params (pension_params: {})
- Add Stimulus pension_system_controller for dynamic form fields
- Update views with per-country field groups and conditional points columns
- Expand i18n (EN, DE) and add ES, FR locale files
- Update fixtures and tests for new schema
Addresses review feedback from jjmata on PR #1057
This commit is contained in:
@@ -80,17 +80,15 @@ class RetirementController < ApplicationController
|
||||
:country, :pension_system, :birth_year, :retirement_age,
|
||||
:target_monthly_income, :currency, :expected_return_pct,
|
||||
:inflation_pct, :tax_rate_pct, :current_monthly_savings,
|
||||
:contribution_start_year, :expected_annual_points, :rentenwert
|
||||
pension_params: {}
|
||||
)
|
||||
end
|
||||
|
||||
# Params are scoped under :pension_entry because the form uses
|
||||
# form_with url: ..., scope: :pension_entry. Switching to form_with model:
|
||||
# would change the nesting — update this permit list accordingly.
|
||||
def pension_entry_params
|
||||
params.require(:pension_entry).permit(
|
||||
:recorded_at, :current_points, :current_monthly_pension,
|
||||
:projected_monthly_pension, :notes
|
||||
:projected_monthly_pension, :notes,
|
||||
data: {}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
32
app/javascript/controllers/pension_system_controller.js
Normal file
32
app/javascript/controllers/pension_system_controller.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Toggles visibility of country-specific pension field groups
|
||||
// based on the selected pension system.
|
||||
//
|
||||
// Usage:
|
||||
// <div data-controller="pension-system">
|
||||
// <select data-pension-system-target="select" data-action="change->pension-system#toggle">
|
||||
// <div data-pension-system-target="fields" data-pension-system-key="de_grv"> ... </div>
|
||||
// <div data-pension-system-target="fields" data-pension-system-key="us_ss"> ... </div>
|
||||
// </div>
|
||||
export default class extends Controller {
|
||||
static targets = ["select", "fields"];
|
||||
|
||||
connect() {
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
const selected = this.selectTarget.value;
|
||||
|
||||
this.fieldsTargets.forEach((el) => {
|
||||
if (el.dataset.pensionSystemKey === selected) {
|
||||
el.classList.remove("hidden");
|
||||
el.querySelectorAll("input, select").forEach((i) => (i.disabled = false));
|
||||
} else {
|
||||
el.classList.add("hidden");
|
||||
el.querySelectorAll("input, select").forEach((i) => (i.disabled = true));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,22 @@ class PensionEntry < ApplicationRecord
|
||||
belongs_to :retirement_config
|
||||
|
||||
validates :recorded_at, presence: true, uniqueness: { scope: :retirement_config_id }
|
||||
validates :current_points, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :current_points, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :current_monthly_pension, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :projected_monthly_pension, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
|
||||
scope :chronological, -> { order(recorded_at: :asc) }
|
||||
scope :reverse_chronological, -> { order(recorded_at: :desc) }
|
||||
|
||||
# Calculate the points gained since the previous entry.
|
||||
# For bulk rendering, prefer RetirementConfig#pension_entries_with_gains
|
||||
# to avoid N+1 queries.
|
||||
def points_gained
|
||||
return nil unless current_points
|
||||
|
||||
previous = retirement_config.pension_entries
|
||||
.where("recorded_at < ?", recorded_at)
|
||||
.order(recorded_at: :desc)
|
||||
.first
|
||||
|
||||
return current_points unless previous
|
||||
return current_points unless previous&.current_points
|
||||
current_points - previous.current_points
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
class RetirementConfig < ApplicationRecord
|
||||
PENSION_SYSTEMS = %w[de_grv custom].freeze
|
||||
DEFAULT_RENTENWERT = 39.32 # Updated annually by German government (as of 2025)
|
||||
SAFE_WITHDRAWAL_RATE = 0.04 # 4% rule for safe withdrawal
|
||||
PENSION_SYSTEMS = {
|
||||
"custom" => { calculator: "RetirementConfig::PensionCalculator::Base" },
|
||||
"de_grv" => { calculator: "RetirementConfig::PensionCalculator::DeGrv" },
|
||||
"us_ss" => { calculator: "RetirementConfig::PensionCalculator::UsSocialSecurity" },
|
||||
"uk_sp" => { calculator: "RetirementConfig::PensionCalculator::UkStatePension" },
|
||||
"fr_regime" => { calculator: "RetirementConfig::PensionCalculator::FrRegimeGeneral" },
|
||||
"es_ss" => { calculator: "RetirementConfig::PensionCalculator::EsSocialSecurity" }
|
||||
}.freeze
|
||||
|
||||
SAFE_WITHDRAWAL_RATE = 0.04
|
||||
|
||||
belongs_to :family
|
||||
has_many :pension_entries, dependent: :destroy
|
||||
|
||||
validates :country, presence: true
|
||||
validates :pension_system, inclusion: { in: PENSION_SYSTEMS }
|
||||
validates :pension_system, inclusion: { in: PENSION_SYSTEMS.keys }
|
||||
validates :birth_year, presence: true,
|
||||
numericality: { greater_than: 1900, less_than_or_equal_to: -> { Date.current.year } }
|
||||
validates :retirement_age, presence: true,
|
||||
@@ -17,50 +24,49 @@ class RetirementConfig < ApplicationRecord
|
||||
validates :inflation_pct, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 20 }
|
||||
validates :tax_rate_pct, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
|
||||
|
||||
# Current age based on birth year
|
||||
def current_age
|
||||
Date.current.year - birth_year
|
||||
end
|
||||
|
||||
# Years until retirement
|
||||
def years_to_retirement
|
||||
[ retirement_age - current_age, 0 ].max
|
||||
end
|
||||
|
||||
# Whether the user has already reached retirement age
|
||||
def retired?
|
||||
current_age >= retirement_age
|
||||
end
|
||||
|
||||
# Estimated monthly pension.
|
||||
# GRV: calculated from Entgeltpunkte × Rentenwert, with latest statement override.
|
||||
# Custom: uses latest pension entry's projected value if available, otherwise 0.
|
||||
# Delegates pension estimation to the active calculator
|
||||
def estimated_monthly_pension
|
||||
if latest_pension_entry&.projected_monthly_pension
|
||||
return latest_pension_entry.projected_monthly_pension
|
||||
if pension_system == "custom"
|
||||
return latest_pension_entry&.projected_monthly_pension || 0
|
||||
end
|
||||
|
||||
return 0 unless pension_system == "de_grv"
|
||||
|
||||
points = total_projected_points
|
||||
rw = rentenwert || DEFAULT_RENTENWERT
|
||||
points * rw
|
||||
pension_calculator.estimated_monthly_pension
|
||||
end
|
||||
|
||||
# Total projected pension points at retirement
|
||||
def total_projected_points
|
||||
current = latest_pension_entry&.current_points || 0
|
||||
annual = expected_annual_points || 1.0
|
||||
current + (annual * years_to_retirement)
|
||||
def pension_calculator
|
||||
@pension_calculator ||= begin
|
||||
klass_name = PENSION_SYSTEMS.dig(pension_system, :calculator)
|
||||
klass_name.constantize.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
# Whether the current system uses pension points (e.g. DE Entgeltpunkte)
|
||||
def points_based?
|
||||
pension_calculator.points_based?
|
||||
end
|
||||
|
||||
# Read a system-specific parameter from JSONB
|
||||
def pension_param(key)
|
||||
pension_params&.dig(key.to_s)
|
||||
end
|
||||
|
||||
# Monthly pension gap: how much more you need beyond GRV pension
|
||||
def monthly_pension_gap
|
||||
gap = target_monthly_income - estimated_monthly_pension_after_tax
|
||||
[ gap, 0 ].max
|
||||
end
|
||||
|
||||
# Estimated pension after taxes
|
||||
def estimated_monthly_pension_after_tax
|
||||
estimated_monthly_pension * (1 - (tax_rate_pct / 100.0))
|
||||
end
|
||||
@@ -158,7 +164,11 @@ class RetirementConfig < ApplicationRecord
|
||||
sorted = pension_entries.chronological.to_a
|
||||
sorted.each_with_index do |entry, idx|
|
||||
prev = idx > 0 ? sorted[idx - 1] : nil
|
||||
delta = prev ? entry.current_points - prev.current_points : entry.current_points
|
||||
if points_based? && entry.current_points
|
||||
delta = prev&.current_points ? entry.current_points - prev.current_points : entry.current_points
|
||||
else
|
||||
delta = nil
|
||||
end
|
||||
entry.define_singleton_method(:points_gained) { delta }
|
||||
end
|
||||
sorted.reverse
|
||||
|
||||
36
app/models/retirement_config/pension_calculator/base.rb
Normal file
36
app/models/retirement_config/pension_calculator/base.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
class RetirementConfig
|
||||
module PensionCalculator
|
||||
class Base
|
||||
attr_reader :config
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def estimated_monthly_pension
|
||||
raise NotImplementedError, "#{self.class} must implement #estimated_monthly_pension"
|
||||
end
|
||||
|
||||
# Override in subclasses to define system-specific parameters for forms
|
||||
# Returns array of hashes: [{ key:, type:, label_i18n:, default:, step:, min:, max: }]
|
||||
def self.param_definitions
|
||||
[]
|
||||
end
|
||||
|
||||
# Override in subclasses that use a points-based system
|
||||
def points_based?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pension_param(key)
|
||||
config.pension_params&.dig(key.to_s)
|
||||
end
|
||||
|
||||
def latest_entry
|
||||
config.send(:latest_pension_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/models/retirement_config/pension_calculator/de_grv.rb
Normal file
37
app/models/retirement_config/pension_calculator/de_grv.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class RetirementConfig
|
||||
module PensionCalculator
|
||||
class DeGrv < Base
|
||||
DEFAULT_RENTENWERT = 39.32
|
||||
|
||||
def estimated_monthly_pension
|
||||
if latest_entry&.projected_monthly_pension
|
||||
return latest_entry.projected_monthly_pension
|
||||
end
|
||||
|
||||
points = total_projected_points
|
||||
rw = pension_param("rentenwert")&.to_f || DEFAULT_RENTENWERT
|
||||
points * rw
|
||||
end
|
||||
|
||||
def points_based?
|
||||
true
|
||||
end
|
||||
|
||||
def self.param_definitions
|
||||
[
|
||||
{ key: "expected_annual_points", type: :number, step: 0.01, min: 0, default: 1.0 },
|
||||
{ key: "rentenwert", type: :number, step: 0.01, min: 0, default: DEFAULT_RENTENWERT },
|
||||
{ key: "contribution_start_year", type: :integer, min: 1960, max: Date.current.year }
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def total_projected_points
|
||||
current = latest_entry&.current_points || 0
|
||||
annual = pension_param("expected_annual_points")&.to_f || 1.0
|
||||
current + (annual * config.years_to_retirement)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class RetirementConfig
|
||||
module PensionCalculator
|
||||
class EsSocialSecurity < Base
|
||||
def estimated_monthly_pension
|
||||
if latest_entry&.projected_monthly_pension
|
||||
return latest_entry.projected_monthly_pension
|
||||
end
|
||||
|
||||
# Users enter projected pension from their informe de vida laboral
|
||||
pension_param("estimated_monthly_pension")&.to_f || 0
|
||||
end
|
||||
|
||||
def self.param_definitions
|
||||
[
|
||||
{ key: "contribution_years", type: :integer, min: 0, max: 50, default: 0 },
|
||||
{ key: "estimated_monthly_pension", type: :number, step: 1, min: 0, default: 0 }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class RetirementConfig
|
||||
module PensionCalculator
|
||||
class FrRegimeGeneral < Base
|
||||
def estimated_monthly_pension
|
||||
if latest_entry&.projected_monthly_pension
|
||||
return latest_entry.projected_monthly_pension
|
||||
end
|
||||
|
||||
# Users can enter projected pension from their relevé de carrière
|
||||
pension_param("estimated_monthly_pension")&.to_f || 0
|
||||
end
|
||||
|
||||
def self.param_definitions
|
||||
[
|
||||
{ key: "trimestres", type: :integer, min: 0, max: 200, default: 0 },
|
||||
{ key: "estimated_monthly_pension", type: :number, step: 1, min: 0, default: 0 }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
class RetirementConfig
|
||||
module PensionCalculator
|
||||
class UkStatePension < Base
|
||||
FULL_WEEKLY_RATE = 221.20 # £/week as of 2025/26
|
||||
|
||||
def estimated_monthly_pension
|
||||
if latest_entry&.projected_monthly_pension
|
||||
return latest_entry.projected_monthly_pension
|
||||
end
|
||||
|
||||
qualifying_years = pension_param("qualifying_years")&.to_f || 0
|
||||
rate = pension_param("full_weekly_rate")&.to_f || FULL_WEEKLY_RATE
|
||||
|
||||
return 0 if qualifying_years <= 0
|
||||
(qualifying_years / 35.0) * rate * 52 / 12
|
||||
end
|
||||
|
||||
def self.param_definitions
|
||||
[
|
||||
{ key: "qualifying_years", type: :integer, min: 0, max: 50, default: 0 },
|
||||
{ key: "full_weekly_rate", type: :number, step: 0.01, min: 0, default: FULL_WEEKLY_RATE }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
class RetirementConfig
|
||||
module PensionCalculator
|
||||
class UsSocialSecurity < Base
|
||||
def estimated_monthly_pension
|
||||
if latest_entry&.projected_monthly_pension
|
||||
return latest_entry.projected_monthly_pension
|
||||
end
|
||||
|
||||
# Users enter their estimated benefit from their SSA statement
|
||||
pension_param("estimated_monthly_benefit")&.to_f || 0
|
||||
end
|
||||
|
||||
def self.param_definitions
|
||||
[
|
||||
{ key: "estimated_monthly_benefit", type: :number, step: 1, min: 0, default: 0 }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -38,13 +38,17 @@
|
||||
</fieldset>
|
||||
|
||||
<%# Pension System %>
|
||||
<fieldset class="space-y-4">
|
||||
<fieldset class="space-y-4" data-controller="pension-system">
|
||||
<legend class="text-lg font-medium text-primary"><%= t(".pension_section") %></legend>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="retirement_config_pension_system" class="block text-sm font-medium text-secondary mb-1"><%= t(".pension_system") %></label>
|
||||
<%= f.select :pension_system, RetirementConfig::PENSION_SYSTEMS.map { |s| [t("retirement.pension_systems.#{s}", default: s.humanize), s] }, {}, class: "form-select w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<%= f.select :pension_system,
|
||||
RetirementConfig::PENSION_SYSTEMS.keys.map { |k| [t("retirement.pension_systems.#{k}", default: k.humanize), k] },
|
||||
{},
|
||||
class: "form-select w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2",
|
||||
data: { pension_system_target: "select", action: "change->pension-system#toggle" } %>
|
||||
</div>
|
||||
<div>
|
||||
<label for="retirement_config_country" class="block text-sm font-medium text-secondary mb-1"><%= t(".country") %></label>
|
||||
@@ -52,22 +56,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<%# German GRV fields %>
|
||||
<div data-pension-system-target="fields" data-pension-system-key="de_grv" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="retirement_config_expected_annual_points" class="block text-sm font-medium text-secondary mb-1"><%= t(".expected_annual_points") %></label>
|
||||
<%= f.number_field :expected_annual_points, step: 0.01, min: 0, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".expected_annual_points_hint") %></p>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".de_expected_annual_points") %></label>
|
||||
<%= f.number_field "pension_params[expected_annual_points]", step: 0.01, min: 0,
|
||||
value: f.object&.pension_params&.dig("expected_annual_points"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".de_expected_annual_points_hint") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="retirement_config_rentenwert" class="block text-sm font-medium text-secondary mb-1"><%= t(".rentenwert") %></label>
|
||||
<%= f.number_field :rentenwert, step: 0.01, min: 0, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".rentenwert_hint") %></p>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".de_rentenwert") %></label>
|
||||
<%= f.number_field "pension_params[rentenwert]", step: 0.01, min: 0,
|
||||
value: f.object&.pension_params&.dig("rentenwert"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".de_rentenwert_hint") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="retirement_config_contribution_start_year" class="block text-sm font-medium text-secondary mb-1"><%= t(".contribution_start_year") %></label>
|
||||
<%= f.number_field :contribution_start_year, min: 1960, max: Date.current.year, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".de_contribution_start_year") %></label>
|
||||
<%= f.number_field "pension_params[contribution_start_year]", min: 1960, max: Date.current.year,
|
||||
value: f.object&.pension_params&.dig("contribution_start_year"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# US Social Security fields %>
|
||||
<div data-pension-system-target="fields" data-pension-system-key="us_ss" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".us_estimated_monthly_benefit") %></label>
|
||||
<%= f.number_field "pension_params[estimated_monthly_benefit]", step: 0.01, min: 0,
|
||||
value: f.object&.pension_params&.dig("estimated_monthly_benefit"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".us_estimated_monthly_benefit_hint") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# UK State Pension fields %>
|
||||
<div data-pension-system-target="fields" data-pension-system-key="uk_sp" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".uk_qualifying_years") %></label>
|
||||
<%= f.number_field "pension_params[qualifying_years]", step: 1, min: 0, max: 35,
|
||||
value: f.object&.pension_params&.dig("qualifying_years"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".uk_qualifying_years_hint") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".uk_full_weekly_rate") %></label>
|
||||
<%= f.number_field "pension_params[full_weekly_rate]", step: 0.01, min: 0,
|
||||
value: f.object&.pension_params&.dig("full_weekly_rate"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".uk_full_weekly_rate_hint") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# French Régime Général fields %>
|
||||
<div data-pension-system-target="fields" data-pension-system-key="fr_regime" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".fr_estimated_monthly_pension") %></label>
|
||||
<%= f.number_field "pension_params[estimated_monthly_pension]", step: 0.01, min: 0,
|
||||
value: f.object&.pension_params&.dig("estimated_monthly_pension"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".fr_estimated_monthly_pension_hint") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".fr_trimestres") %></label>
|
||||
<%= f.number_field "pension_params[trimestres]", step: 1, min: 0,
|
||||
value: f.object&.pension_params&.dig("trimestres"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Spanish Social Security fields %>
|
||||
<div data-pension-system-target="fields" data-pension-system-key="es_ss" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".es_estimated_monthly_pension") %></label>
|
||||
<%= f.number_field "pension_params[estimated_monthly_pension]", step: 0.01, min: 0,
|
||||
value: f.object&.pension_params&.dig("estimated_monthly_pension"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".es_estimated_monthly_pension_hint") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-1"><%= t(".es_contribution_years") %></label>
|
||||
<%= f.number_field "pension_params[contribution_years]", step: 1, min: 0,
|
||||
value: f.object&.pension_params&.dig("contribution_years"),
|
||||
class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Custom / Other — just use estimated pension from entries %>
|
||||
<div data-pension-system-target="fields" data-pension-system-key="custom" class="text-sm text-secondary">
|
||||
<p><%= t(".custom_hint") %></p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<%# Assumptions %>
|
||||
|
||||
@@ -24,9 +24,7 @@
|
||||
|
||||
<%# Estimated Pension %>
|
||||
<div class="bg-container shadow-border-xs rounded-lg p-4">
|
||||
<p class="text-sm text-secondary mb-1">
|
||||
<%= @retirement_config.pension_system == "de_grv" ? t(".estimated_pension_grv") : t(".estimated_pension") %>
|
||||
</p>
|
||||
<p class="text-sm text-secondary mb-1"><%= t(".estimated_pension") %></p>
|
||||
<p class="text-2xl font-semibold text-primary">
|
||||
<%= number_to_currency(@retirement_config.estimated_monthly_pension, unit: retirement_currency_unit(@retirement_config)) %>
|
||||
</p>
|
||||
@@ -154,10 +152,12 @@
|
||||
<%= f.label :recorded_at, t(".date"), class: "block text-sm font-medium text-secondary mb-1" %>
|
||||
<%= f.date_field :recorded_at, value: Date.current, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2", required: true %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :current_points, t(".pension_points"), class: "block text-sm font-medium text-secondary mb-1" %>
|
||||
<%= f.number_field :current_points, step: 0.0001, min: 0, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2", required: true %>
|
||||
</div>
|
||||
<% if @retirement_config.points_based? %>
|
||||
<div>
|
||||
<%= f.label :current_points, t(".pension_points"), class: "block text-sm font-medium text-secondary mb-1" %>
|
||||
<%= f.number_field :current_points, step: 0.0001, min: 0, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<%= f.label :current_monthly_pension, t(".current_pension"), class: "block text-sm font-medium text-secondary mb-1" %>
|
||||
<%= f.number_field :current_monthly_pension, step: 0.01, min: 0, class: "form-input w-full rounded-lg border border-secondary bg-container text-sm text-primary px-3 py-2" %>
|
||||
@@ -185,8 +185,10 @@
|
||||
<thead>
|
||||
<tr class="border-b border-secondary">
|
||||
<th class="text-left text-secondary font-medium py-2 px-3"><%= t(".date") %></th>
|
||||
<th class="text-right text-secondary font-medium py-2 px-3"><%= t(".pension_points") %></th>
|
||||
<th class="text-right text-secondary font-medium py-2 px-3"><%= t(".points_gained") %></th>
|
||||
<% if @retirement_config.points_based? %>
|
||||
<th class="text-right text-secondary font-medium py-2 px-3"><%= t(".pension_points") %></th>
|
||||
<th class="text-right text-secondary font-medium py-2 px-3"><%= t(".points_gained") %></th>
|
||||
<% end %>
|
||||
<th class="text-right text-secondary font-medium py-2 px-3"><%= t(".current_pension") %></th>
|
||||
<th class="text-right text-secondary font-medium py-2 px-3"><%= t(".projected_pension") %></th>
|
||||
<th class="text-left text-secondary font-medium py-2 px-3"><%= t(".notes") %></th>
|
||||
@@ -197,12 +199,24 @@
|
||||
<% @pension_entries.each do |entry| %>
|
||||
<tr class="border-b border-secondary/50 hover:bg-surface-hover">
|
||||
<td class="py-3 px-3 text-primary"><%= l(entry.recorded_at) %></td>
|
||||
<td class="py-3 px-3 text-right text-primary"><%= number_with_precision(entry.current_points, precision: 4) %></td>
|
||||
<td class="py-3 px-3 text-right">
|
||||
<span class="<%= entry.points_gained > 0 ? 'text-success' : entry.points_gained < 0 ? 'text-destructive' : 'text-secondary' %>">
|
||||
<%= entry.points_gained >= 0 ? '+' : '' %><%= number_with_precision(entry.points_gained, precision: 4) %>
|
||||
</span>
|
||||
</td>
|
||||
<% if @retirement_config.points_based? %>
|
||||
<td class="py-3 px-3 text-right text-primary">
|
||||
<% if entry.current_points %>
|
||||
<%= number_with_precision(entry.current_points, precision: 4) %>
|
||||
<% else %>
|
||||
<span class="text-secondary">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="py-3 px-3 text-right">
|
||||
<% if entry.points_gained %>
|
||||
<span class="<%= entry.points_gained > 0 ? 'text-success' : entry.points_gained < 0 ? 'text-destructive' : 'text-secondary' %>">
|
||||
<%= entry.points_gained >= 0 ? '+' : '' %><%= number_with_precision(entry.points_gained, precision: 4) %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="text-secondary">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
<td class="py-3 px-3 text-right text-primary">
|
||||
<% if entry.current_monthly_pension %>
|
||||
<%= number_to_currency(entry.current_monthly_pension, unit: retirement_currency_unit(@retirement_config)) %>
|
||||
|
||||
@@ -9,7 +9,6 @@ de:
|
||||
one: "%{count} Jahr bis zur Rente"
|
||||
other: "%{count} Jahre bis zur Rente"
|
||||
estimated_pension: Geschätzte monatliche Rente
|
||||
estimated_pension_grv: Geschätzte GRV-Rente
|
||||
after_tax: Nach Steuern
|
||||
pension_gap: Monatliche Rentenlücke
|
||||
target: "Ziel"
|
||||
@@ -28,7 +27,7 @@ de:
|
||||
inflation: Inflationsrate
|
||||
tax_rate: Steuersatz
|
||||
monthly_savings: Monatliche Sparrate
|
||||
pension_history: Rentenhistorie (Renteninformation)
|
||||
pension_history: Rentenhistorie
|
||||
add_entry: Neuen Eintrag hinzufügen
|
||||
date: Datum
|
||||
pension_points: Entgeltpunkte
|
||||
@@ -53,11 +52,30 @@ de:
|
||||
pension_section: Rentensystem
|
||||
pension_system: Rentensystem
|
||||
country: Land
|
||||
expected_annual_points: Erwartete jährliche Entgeltpunkte
|
||||
expected_annual_points_hint: Durchschnittsgehalt = ~1,0 Punkte/Jahr
|
||||
rentenwert: Aktueller Rentenwert
|
||||
rentenwert_hint: Aktueller Wert in EUR (2025 ~ 39,32)
|
||||
contribution_start_year: Beitragsbeginn (Jahr)
|
||||
# German GRV
|
||||
de_expected_annual_points: Erwartete jährliche Entgeltpunkte
|
||||
de_expected_annual_points_hint: Durchschnittsgehalt = ~1,0 Punkte/Jahr
|
||||
de_rentenwert: Aktueller Rentenwert
|
||||
de_rentenwert_hint: Aktueller Wert in EUR (2025 ~ 39,32)
|
||||
de_contribution_start_year: Beitragsbeginn (Jahr)
|
||||
# US Social Security
|
||||
us_estimated_monthly_benefit: Geschätzte monatliche Leistung
|
||||
us_estimated_monthly_benefit_hint: Aus deiner SSA-Bescheinigung (ssa.gov)
|
||||
# UK State Pension
|
||||
uk_qualifying_years: Anspruchsjahre (NI)
|
||||
uk_qualifying_years_hint: "35 Jahre für volle State Pension"
|
||||
uk_full_weekly_rate: Voller Wochensatz (£)
|
||||
uk_full_weekly_rate_hint: "2024/25: £221,20/Woche"
|
||||
# France Régime Général
|
||||
fr_estimated_monthly_pension: Geschätzte monatliche Rente
|
||||
fr_estimated_monthly_pension_hint: Aus deinem relevé de situation individuelle
|
||||
fr_trimestres: Validierte Trimester
|
||||
# Spain Social Security
|
||||
es_estimated_monthly_pension: Geschätzte monatliche Rente
|
||||
es_estimated_monthly_pension_hint: Aus deinem informe de vida laboral
|
||||
es_contribution_years: Beitragsjahre
|
||||
# Custom
|
||||
custom_hint: Füge Renteneinträge manuell hinzu, um deine geschätzte Rente zu verfolgen.
|
||||
assumptions_section: Annahmen
|
||||
expected_return: Erwartete jährliche Rendite (%)
|
||||
inflation: Erwartete Inflation (%)
|
||||
@@ -82,4 +100,8 @@ de:
|
||||
pension_entry_removed: Renteneintrag entfernt
|
||||
pension_systems:
|
||||
de_grv: Deutsche Gesetzliche Rentenversicherung (GRV)
|
||||
us_ss: US-Sozialversicherung
|
||||
uk_sp: UK State Pension
|
||||
fr_regime: Französisches Régime Général
|
||||
es_ss: Spanische Sozialversicherung
|
||||
custom: Benutzerdefiniert / Andere
|
||||
|
||||
@@ -9,7 +9,6 @@ en:
|
||||
one: "%{count} year to retirement"
|
||||
other: "%{count} years to retirement"
|
||||
estimated_pension: Estimated Monthly Pension
|
||||
estimated_pension_grv: Estimated GRV Pension
|
||||
after_tax: After tax
|
||||
pension_gap: Monthly Pension Gap
|
||||
target: "Target"
|
||||
@@ -28,7 +27,7 @@ en:
|
||||
inflation: Inflation Rate
|
||||
tax_rate: Tax Rate
|
||||
monthly_savings: Monthly Savings
|
||||
pension_history: Pension History (Renteninformation)
|
||||
pension_history: Pension History
|
||||
add_entry: Add new pension entry
|
||||
date: Date
|
||||
pension_points: Pension Points
|
||||
@@ -36,11 +35,11 @@ en:
|
||||
current_pension: Current Pension
|
||||
projected_pension: Projected Pension
|
||||
notes: Notes
|
||||
notes_placeholder: "e.g. Annual Renteninformation 2024"
|
||||
notes_placeholder: "e.g. Annual pension statement 2024"
|
||||
save_entry: Save Entry
|
||||
confirm_delete: Are you sure you want to delete this entry?
|
||||
no_entries: No pension entries yet
|
||||
no_entries_hint: Add entries from your Renteninformation to track your pension progress
|
||||
no_entries_hint: Add entries from your pension statements to track your progress
|
||||
setup_required: Please set up retirement planning first
|
||||
form_fields: &retirement_form_fields_en
|
||||
personal_section: Personal Information
|
||||
@@ -53,11 +52,30 @@ en:
|
||||
pension_section: Pension System
|
||||
pension_system: Pension System
|
||||
country: Country
|
||||
expected_annual_points: Expected Annual Points
|
||||
expected_annual_points_hint: Average salary = ~1.0 points/year
|
||||
rentenwert: Current Pension Value (Rentenwert)
|
||||
rentenwert_hint: Current value in EUR (2025 ~ 39.32)
|
||||
contribution_start_year: Contribution Start Year
|
||||
# German GRV
|
||||
de_expected_annual_points: Expected Annual Points
|
||||
de_expected_annual_points_hint: Average salary = ~1.0 points/year
|
||||
de_rentenwert: Current Pension Value (Rentenwert)
|
||||
de_rentenwert_hint: Current value in EUR (2025 ~ 39.32)
|
||||
de_contribution_start_year: Contribution Start Year
|
||||
# US Social Security
|
||||
us_estimated_monthly_benefit: Estimated Monthly Benefit
|
||||
us_estimated_monthly_benefit_hint: From your SSA statement (ssa.gov)
|
||||
# UK State Pension
|
||||
uk_qualifying_years: Qualifying Years (NI)
|
||||
uk_qualifying_years_hint: "35 years for full new State Pension"
|
||||
uk_full_weekly_rate: Full Weekly Rate (£)
|
||||
uk_full_weekly_rate_hint: "2024/25: £221.20/week"
|
||||
# France Régime Général
|
||||
fr_estimated_monthly_pension: Estimated Monthly Pension
|
||||
fr_estimated_monthly_pension_hint: From your relevé de situation individuelle
|
||||
fr_trimestres: Trimestres Validated
|
||||
# Spain Social Security
|
||||
es_estimated_monthly_pension: Estimated Monthly Pension
|
||||
es_estimated_monthly_pension_hint: From your informe de vida laboral
|
||||
es_contribution_years: Contribution Years
|
||||
# Custom
|
||||
custom_hint: Add pension entries manually to track your estimated pension.
|
||||
assumptions_section: Assumptions
|
||||
expected_return: Expected Annual Return (%)
|
||||
inflation: Expected Inflation (%)
|
||||
@@ -82,4 +100,8 @@ en:
|
||||
pension_entry_removed: Pension entry removed
|
||||
pension_systems:
|
||||
de_grv: German Statutory Pension (GRV)
|
||||
us_ss: US Social Security
|
||||
uk_sp: UK State Pension
|
||||
fr_regime: French Régime Général
|
||||
es_ss: Spanish Social Security
|
||||
custom: Custom / Other
|
||||
|
||||
101
config/locales/views/retirement/es.yml
Normal file
101
config/locales/views/retirement/es.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
es:
|
||||
retirement:
|
||||
show:
|
||||
page_title: Planificación de Jubilación
|
||||
subtitle: Sigue tu camino hacia la independencia financiera y la preparación para la jubilación
|
||||
current_age: Edad Actual
|
||||
years_to_retirement:
|
||||
one: "%{count} año para la jubilación"
|
||||
other: "%{count} años para la jubilación"
|
||||
estimated_pension: Pensión Mensual Estimada
|
||||
after_tax: Después de impuestos
|
||||
pension_gap: Brecha Mensual de Pensión
|
||||
target: "Objetivo"
|
||||
required_savings: Ahorro Mensual Necesario
|
||||
per_month: al mes para cerrar la brecha de pensión
|
||||
fire_progress: Progreso FIRE
|
||||
current_portfolio: Cartera Actual
|
||||
fire_number: Número FIRE
|
||||
progress: Progreso
|
||||
estimated_fire_date: Fecha FIRE Estimada
|
||||
assumptions: Supuestos
|
||||
edit: Editar
|
||||
birth_year: Año de Nacimiento
|
||||
retirement_age: Edad de Jubilación
|
||||
expected_return: Rendimiento Esperado
|
||||
inflation: Tasa de Inflación
|
||||
tax_rate: Tasa Impositiva
|
||||
monthly_savings: Ahorro Mensual
|
||||
pension_history: Historial de Pensión
|
||||
add_entry: Añadir nueva entrada de pensión
|
||||
date: Fecha
|
||||
pension_points: Puntos de Pensión
|
||||
points_gained: Puntos Ganados
|
||||
current_pension: Pensión Actual
|
||||
projected_pension: Pensión Proyectada
|
||||
notes: Notas
|
||||
notes_placeholder: "ej. Informe de vida laboral 2024"
|
||||
save_entry: Guardar Entrada
|
||||
confirm_delete: "¿Estás seguro de querer eliminar esta entrada?"
|
||||
no_entries: Aún no hay entradas de pensión
|
||||
no_entries_hint: Añade entradas de tus informes de pensión para seguir tu progreso
|
||||
setup_required: Por favor configura primero la planificación de jubilación
|
||||
form_fields: &retirement_form_fields_es
|
||||
personal_section: Información Personal
|
||||
birth_year: Año de Nacimiento
|
||||
retirement_age: Edad de Jubilación Deseada
|
||||
financial_section: Objetivos Financieros
|
||||
target_monthly_income: Ingreso Mensual Objetivo (Jubilación)
|
||||
current_monthly_savings: Ahorro Mensual Actual
|
||||
currency: Moneda
|
||||
pension_section: Sistema de Pensiones
|
||||
pension_system: Sistema de Pensiones
|
||||
country: País
|
||||
de_expected_annual_points: Puntos Anuales Esperados
|
||||
de_expected_annual_points_hint: Salario promedio = ~1,0 puntos/año
|
||||
de_rentenwert: Valor Actual de Pensión (Rentenwert)
|
||||
de_rentenwert_hint: Valor actual en EUR (2025 ~ 39,32)
|
||||
de_contribution_start_year: Año de Inicio de Contribuciones
|
||||
us_estimated_monthly_benefit: Beneficio Mensual Estimado
|
||||
us_estimated_monthly_benefit_hint: De tu declaración SSA (ssa.gov)
|
||||
uk_qualifying_years: Años Calificados (NI)
|
||||
uk_qualifying_years_hint: "35 años para la State Pension completa"
|
||||
uk_full_weekly_rate: Tarifa Semanal Completa (£)
|
||||
uk_full_weekly_rate_hint: "2024/25: £221,20/semana"
|
||||
fr_estimated_monthly_pension: Pensión Mensual Estimada
|
||||
fr_estimated_monthly_pension_hint: De tu relevé de situation individuelle
|
||||
fr_trimestres: Trimestres Validados
|
||||
es_estimated_monthly_pension: Pensión Mensual Estimada
|
||||
es_estimated_monthly_pension_hint: De tu informe de vida laboral
|
||||
es_contribution_years: Años de Cotización
|
||||
custom_hint: Añade entradas de pensión manualmente para seguir tu pensión estimada.
|
||||
assumptions_section: Supuestos
|
||||
expected_return: Rendimiento Anual Esperado (%)
|
||||
inflation: Inflación Esperada (%)
|
||||
tax_rate: Tasa Impositiva en Jubilación (%)
|
||||
setup:
|
||||
page_title: Configurar Planificación de Jubilación
|
||||
subtitle: Configura tus parámetros de planificación de jubilación y FIRE
|
||||
cancel: Cancelar
|
||||
save: Guardar y Continuar
|
||||
edit:
|
||||
page_title: Editar Configuración de Jubilación
|
||||
subtitle: Actualiza tus parámetros de planificación de jubilación
|
||||
cancel: Cancelar
|
||||
save: Guardar Cambios
|
||||
create:
|
||||
created: La planificación de jubilación se ha configurado correctamente
|
||||
update:
|
||||
updated: Configuración de jubilación actualizada correctamente
|
||||
add_pension_entry:
|
||||
pension_entry_added: Entrada de pensión añadida correctamente
|
||||
destroy_pension_entry:
|
||||
pension_entry_removed: Entrada de pensión eliminada
|
||||
pension_systems:
|
||||
de_grv: Pensión Estatal Alemana (GRV)
|
||||
us_ss: Seguridad Social de EE.UU.
|
||||
uk_sp: Pensión Estatal del Reino Unido
|
||||
fr_regime: Régimen General Francés
|
||||
es_ss: Seguridad Social Española
|
||||
custom: Personalizado / Otro
|
||||
101
config/locales/views/retirement/fr.yml
Normal file
101
config/locales/views/retirement/fr.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
fr:
|
||||
retirement:
|
||||
show:
|
||||
page_title: Planification de la Retraite
|
||||
subtitle: Suivez votre chemin vers l'indépendance financière et la préparation à la retraite
|
||||
current_age: Âge Actuel
|
||||
years_to_retirement:
|
||||
one: "%{count} an avant la retraite"
|
||||
other: "%{count} ans avant la retraite"
|
||||
estimated_pension: Pension Mensuelle Estimée
|
||||
after_tax: Après impôts
|
||||
pension_gap: Écart Mensuel de Pension
|
||||
target: "Objectif"
|
||||
required_savings: Épargne Mensuelle Nécessaire
|
||||
per_month: par mois pour combler l'écart de pension
|
||||
fire_progress: Progression FIRE
|
||||
current_portfolio: Portefeuille Actuel
|
||||
fire_number: Nombre FIRE
|
||||
progress: Progression
|
||||
estimated_fire_date: Date FIRE Estimée
|
||||
assumptions: Hypothèses
|
||||
edit: Modifier
|
||||
birth_year: Année de Naissance
|
||||
retirement_age: Âge de Retraite
|
||||
expected_return: Rendement Attendu
|
||||
inflation: Taux d'Inflation
|
||||
tax_rate: Taux d'Imposition
|
||||
monthly_savings: Épargne Mensuelle
|
||||
pension_history: Historique de Pension
|
||||
add_entry: Ajouter une entrée de pension
|
||||
date: Date
|
||||
pension_points: Points de Pension
|
||||
points_gained: Points Gagnés
|
||||
current_pension: Pension Actuelle
|
||||
projected_pension: Pension Projetée
|
||||
notes: Notes
|
||||
notes_placeholder: "ex. Relevé de situation individuelle 2024"
|
||||
save_entry: Enregistrer
|
||||
confirm_delete: "Êtes-vous sûr de vouloir supprimer cette entrée ?"
|
||||
no_entries: Aucune entrée de pension
|
||||
no_entries_hint: Ajoutez des entrées de vos relevés de pension pour suivre votre progression
|
||||
setup_required: Veuillez d'abord configurer la planification de retraite
|
||||
form_fields: &retirement_form_fields_fr
|
||||
personal_section: Informations Personnelles
|
||||
birth_year: Année de Naissance
|
||||
retirement_age: Âge de Retraite Souhaité
|
||||
financial_section: Objectifs Financiers
|
||||
target_monthly_income: Revenu Mensuel Cible (Retraite)
|
||||
current_monthly_savings: Épargne Mensuelle Actuelle
|
||||
currency: Devise
|
||||
pension_section: Système de Retraite
|
||||
pension_system: Système de Retraite
|
||||
country: Pays
|
||||
de_expected_annual_points: Points Annuels Attendus
|
||||
de_expected_annual_points_hint: Salaire moyen = ~1,0 point/an
|
||||
de_rentenwert: Valeur Actuelle de la Pension (Rentenwert)
|
||||
de_rentenwert_hint: Valeur actuelle en EUR (2025 ~ 39,32)
|
||||
de_contribution_start_year: Année de Début des Cotisations
|
||||
us_estimated_monthly_benefit: Prestation Mensuelle Estimée
|
||||
us_estimated_monthly_benefit_hint: De votre relevé SSA (ssa.gov)
|
||||
uk_qualifying_years: Années de Qualification (NI)
|
||||
uk_qualifying_years_hint: "35 ans pour la State Pension complète"
|
||||
uk_full_weekly_rate: Taux Hebdomadaire Complet (£)
|
||||
uk_full_weekly_rate_hint: "2024/25 : £221,20/semaine"
|
||||
fr_estimated_monthly_pension: Pension Mensuelle Estimée
|
||||
fr_estimated_monthly_pension_hint: De votre relevé de situation individuelle
|
||||
fr_trimestres: Trimestres Validés
|
||||
es_estimated_monthly_pension: Pension Mensuelle Estimée
|
||||
es_estimated_monthly_pension_hint: De votre informe de vida laboral
|
||||
es_contribution_years: Années de Cotisation
|
||||
custom_hint: Ajoutez des entrées de pension manuellement pour suivre votre pension estimée.
|
||||
assumptions_section: Hypothèses
|
||||
expected_return: Rendement Annuel Attendu (%)
|
||||
inflation: Inflation Attendue (%)
|
||||
tax_rate: Taux d'Imposition à la Retraite (%)
|
||||
setup:
|
||||
page_title: Configurer la Planification de Retraite
|
||||
subtitle: Configurez vos paramètres de planification de retraite et FIRE
|
||||
cancel: Annuler
|
||||
save: Enregistrer et Continuer
|
||||
edit:
|
||||
page_title: Modifier les Paramètres de Retraite
|
||||
subtitle: Mettez à jour vos paramètres de planification de retraite
|
||||
cancel: Annuler
|
||||
save: Enregistrer les Modifications
|
||||
create:
|
||||
created: La planification de la retraite a été configurée avec succès
|
||||
update:
|
||||
updated: Paramètres de retraite mis à jour avec succès
|
||||
add_pension_entry:
|
||||
pension_entry_added: Entrée de pension ajoutée avec succès
|
||||
destroy_pension_entry:
|
||||
pension_entry_removed: Entrée de pension supprimée
|
||||
pension_systems:
|
||||
de_grv: Pension Légale Allemande (GRV)
|
||||
us_ss: Sécurité Sociale Américaine
|
||||
uk_sp: Pension d'État Britannique
|
||||
fr_regime: Régime Général Français
|
||||
es_ss: Sécurité Sociale Espagnole
|
||||
custom: Personnalisé / Autre
|
||||
32
db/migrate/20260409100000_generalize_pension_systems.rb
Normal file
32
db/migrate/20260409100000_generalize_pension_systems.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class GeneralizePensionSystems < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
# Add JSONB column for system-specific parameters (replaces rentenwert, expected_annual_points)
|
||||
add_column :retirement_configs, :pension_params, :jsonb, null: false, default: {}
|
||||
|
||||
# Add JSONB column for system-specific entry data
|
||||
add_column :pension_entries, :data, :jsonb, null: false, default: {}
|
||||
|
||||
# Make current_points nullable (only used by points-based systems like DE)
|
||||
change_column_null :pension_entries, :current_points, true
|
||||
|
||||
# Migrate existing data: move rentenwert and expected_annual_points into pension_params
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
execute <<-SQL.squish
|
||||
UPDATE retirement_configs
|
||||
SET pension_params = jsonb_build_object(
|
||||
'rentenwert', COALESCE(rentenwert, 39.32),
|
||||
'expected_annual_points', COALESCE(expected_annual_points, 1.0),
|
||||
'contribution_start_year', contribution_start_year
|
||||
)
|
||||
WHERE pension_system = 'de_grv'
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
# Remove old German-specific columns
|
||||
remove_column :retirement_configs, :rentenwert, :decimal, precision: 8, scale: 2
|
||||
remove_column :retirement_configs, :expected_annual_points, :decimal, precision: 5, scale: 2
|
||||
remove_column :retirement_configs, :contribution_start_year, :integer
|
||||
end
|
||||
end
|
||||
@@ -44,7 +44,12 @@ class RetirementControllerTest < ActionDispatch::IntegrationTest
|
||||
country: "DE",
|
||||
expected_return_pct: 7.0,
|
||||
inflation_pct: 2.0,
|
||||
tax_rate_pct: 26.38
|
||||
tax_rate_pct: 26.38,
|
||||
pension_params: {
|
||||
expected_annual_points: 1.0,
|
||||
rentenwert: 39.32,
|
||||
contribution_start_year: 2015
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
22
test/fixtures/retirement_configs.yml
vendored
22
test/fixtures/retirement_configs.yml
vendored
@@ -10,9 +10,10 @@ dylan_retirement:
|
||||
inflation_pct: 2.0
|
||||
tax_rate_pct: 26.38
|
||||
current_monthly_savings: 500.0
|
||||
contribution_start_year: 2015
|
||||
expected_annual_points: 1.0
|
||||
rentenwert: 39.32
|
||||
pension_params:
|
||||
expected_annual_points: 1.0
|
||||
rentenwert: 39.32
|
||||
contribution_start_year: 2015
|
||||
|
||||
custom_retirement:
|
||||
family: empty
|
||||
@@ -26,3 +27,18 @@ custom_retirement:
|
||||
inflation_pct: 2.5
|
||||
tax_rate_pct: 22.0
|
||||
current_monthly_savings: 800.0
|
||||
|
||||
us_retirement:
|
||||
family: inactive_trial
|
||||
country: US
|
||||
pension_system: us_ss
|
||||
birth_year: 1980
|
||||
retirement_age: 67
|
||||
target_monthly_income: 5000.0
|
||||
currency: USD
|
||||
expected_return_pct: 7.0
|
||||
inflation_pct: 2.5
|
||||
tax_rate_pct: 24.0
|
||||
current_monthly_savings: 1000.0
|
||||
pension_params:
|
||||
estimated_monthly_benefit: 2800.0
|
||||
|
||||
@@ -15,9 +15,9 @@ class PensionEntryTest < ActiveSupport::TestCase
|
||||
assert_not @entry_2024.valid?
|
||||
end
|
||||
|
||||
test "requires current_points" do
|
||||
test "current_points is optional" do
|
||||
@entry_2024.current_points = nil
|
||||
assert_not @entry_2024.valid?
|
||||
assert @entry_2024.valid?
|
||||
end
|
||||
|
||||
test "current_points must be non-negative" do
|
||||
|
||||
Reference in New Issue
Block a user