PS pension module: optional federal PSSA engine + UI (PSPC-accurate, EN/FR)

This commit is contained in:
krisf committed 2026-10-09 11:44:40 -04:00
1 parent fe62022a32
commit 583b5ee2f8
14 files changed
+1991 -27

No files matched your search

+171 -9
View File
@@ -4,7 +4,7 @@
(fmtMoney/fmtMoneyK/fmtMoney2/fmtInt/fmtPct) so every number follows the active locale. */
'use strict';
(function () {
var E = window.RetireEngine, C = window.RetireCanada;
var E = window.RetireEngine, C = window.RetireCanada, PS = window.RetirePS;
var LS = 'retire-planner-v1';
var $ = function (s) { return document.querySelector(s); };
/* locale-aware aliases (i18n.js globals) — chart/table code below stays untouched in shape */
@@ -51,6 +51,8 @@
retireAge: 65, spendingAnnual: 50000,
cppStartAge: 65, oasStartAge: 65,
pensionAnnual: 0, rentalAnnual: 0, otherAnnual: 0,
psPension: false, psJoinYear: null, psServiceYears: null, psAvgSalary: null,
psOccupation: null, psItLevel: null, psItStep: 0,
realReturnPre: 3.5, realReturnPost: 2.5, inflation: 2.0, lifeExp: 95,
interests: []
};
@@ -89,8 +91,37 @@
realReturnPre: (+a.realReturnPre) / 100, realReturnPost: (+a.realReturnPost) / 100,
inflation: (+a.inflation) / 100, lifeExp: +a.lifeExp
};
/* Optional federal public service pension (PSSA): when enabled, it
* replaces the manual DB-pension input and feeds pensionAnnual /
* pensionStartAge / pensionBridgeAnnual into the engine. Recomputed for
* the effective retireAge so what-if scenarios stay accurate. */
var ps = psCompute(p.retireAge);
if (ps) {
p.pensionAnnual = ps.pensionAtRetirement;
p.pensionStartAge = ps.startAge;
p.pensionBridgeAnnual = ps.bridge;
} else {
p.pensionStartAge = 0; p.pensionBridgeAnnual = 0;
}
return p;
}
function psOn() { return !!state.a.psPension; }
function psAugment(p) { /* re-derive PS pension for a scenario's retireAge */
var ps = psCompute(p.retireAge);
if (ps) { p.pensionAnnual = ps.pensionAtRetirement; p.pensionStartAge = ps.startAge; p.pensionBridgeAnnual = ps.bridge; }
return p;
}
function psCompute(retireAge) {
var a = state.a;
if (!a.psPension || typeof PS === 'undefined') return null;
var avg = +a.psAvgSalary || 0;
if (!avg) return null;
return PS.computePSPension({
joinYear: +a.psJoinYear || 0, retireAge: retireAge,
serviceYears: +a.psServiceYears || 0, avgSalary: avg,
ampe: (typeof CANADA !== 'undefined' && CANADA.ympe && CANADA.ympe.value) || 74600
});
}
/* ---------- formatting (i18n.js: Intl.NumberFormat, locale-aware) ---------- */
function pct2(v) { return fmtPct(v / 100, 2); } /* slider values arrive as plain numbers: 3.5 → 3.5% */
@@ -173,6 +204,70 @@
type: 'opts', opts: [[65, '65', 'q.oasstart.os65'], [70, '70', 'q.oasstart.os70']],
get: function () { return state.a.oasStartAge; }, set: function (v) { state.a.oasStartAge = +v; }
},
{
key: 'ps', titleK: 'q.ps.title', hintK: 'q.ps.hint', type: 'opts',
opts: [['yes', 'q.ps.yes', 'q.ps.yesD'], ['no', 'q.ps.no', 'q.ps.noD']],
get: function () { return state.a.psPension ? 'yes' : 'no'; },
set: function (v) { state.a.psPension = v === 'yes'; }
},
{
key: 'ps_join', show: psOn, titleK: 'q.ps_join.title', hintK: 'q.ps_join.hint', type: 'number', min: 1950, max: 2026,
get: function () { return state.a.psJoinYear; }, set: function (v) { state.a.psJoinYear = +v; }
},
{
key: 'ps_service', show: psOn, titleK: 'q.ps_service.title', hintK: 'q.ps_service.hint', type: 'number', min: 0, max: 45,
get: function () { return state.a.psServiceYears; }, set: function (v) { state.a.psServiceYears = +v; }
},
{
key: 'ps_salary', show: psOn, titleK: 'q.ps_salary.title',
hint: function () {
var r = state.a.psItLevel && PS && PS.itRange(state.a.psItLevel);
if (r) return t('q.ps_salary.hintIt', { lo: fmtMoneyK(r.min), hi: fmtMoneyK(r.top), asof: r.asof });
return t('q.ps_salary.hint');
},
type: 'number', min: 0, max: 1e6, money: true,
get: function () { return state.a.psAvgSalary; }, set: function (v) { state.a.psAvgSalary = +v; }
},
{
key: 'ps_occ', show: psOn, titleK: 'q.ps_occ.title', hintK: 'q.ps_occ.hint', type: 'opts',
opts: [['it', 'q.ps_occ.it', 'q.ps_occ.itD'], ['other', 'q.ps_occ.other', 'q.ps_occ.otherD']],
get: function () { return state.a.psOccupation; }, set: function (v) { state.a.psOccupation = v; }
},
{
key: 'ps_it', show: function () { return psOn() && state.a.psOccupation === 'it'; },
titleK: function () {
return state.a.psItLevel && state.a.psItLevel !== 'none'
? t('q.ps_it.stepTitle', { level: state.a.psItLevel }) : t('q.ps_it.title');
},
hint: function () {
var r = state.a.psItLevel && state.a.psItLevel !== 'none' && PS.itRange(state.a.psItLevel);
if (r) return t('q.ps_it.stepHint', { lo: fmtMoneyK(r.min), hi: fmtMoneyK(r.top), asof: r.asof });
return t('q.ps_it.hint', { asof: PS.IT_RATES_ASOF });
},
type: 'opts',
opts: function () {
if (state.a.psItLevel && state.a.psItLevel !== 'none') {
var r = PS.itRange(state.a.psItLevel);
return r.steps.map(function (v, i) { return [String(i + 1), fmtMoney(v), i === 0 ? 'q.ps_it.stepMin' : (i === r.steps.length - 1 ? 'q.ps_it.stepTop' : '')]; });
}
return PS.IT_LEVELS.map(function (lv) {
return [lv.id, lv.id, function () { var r = PS.itRange(lv.id); return fmtMoneyK(r.min) + ' – ' + fmtMoneyK(r.top); }];
}).concat([['none', 'q.ps_it.skip', 'q.ps_it.skipD']]);
},
get: function () {
if (state.a.psItLevel && state.a.psItLevel !== 'none' && state.a.psItStep) return String(state.a.psItStep);
return state.a.psItLevel || '';
},
set: function (v) {
if (state.a.psItLevel && state.a.psItLevel !== 'none' && /^\d+$/.test(v)) {
var r = PS.itRange(state.a.psItLevel);
state.a.psItStep = +v;
state.a.psAvgSalary = r.steps[+v - 1]; /* step value = 5-yr average proxy */
} else {
state.a.psItLevel = v; state.a.psItStep = 0;
}
}
},
{
key: 'extra', titleK: 'q.extra.title', hintK: 'q.extra.hint', type: 'fields', fields: [
{ labelK: 'q.extra.pension', key: 'pensionAnnual' }, { labelK: 'q.extra.rental', key: 'rentalAnnual' }, { labelK: 'q.extra.other', key: 'otherAnnual' }
@@ -219,15 +314,18 @@
btnNext.textContent = state.step === vs.length - 1 ? t('quiz.seePlan') : t('quiz.next');
var hintTxt = typeof s.hint === 'function' ? s.hint() : (s.hintK ? t(s.hintK) : '');
var h = '<h2>' + t(s.titleK) + '</h2>' + (hintTxt ? '<p class="hint">' + hintTxt + '</p>' : '');
var titleTxt = typeof s.titleK === 'function' ? s.titleK() : t(s.titleK);
var optList = typeof s.opts === 'function' ? s.opts() : s.opts;
var h = '<h2>' + titleTxt + '</h2>' + (hintTxt ? '<p class="hint">' + hintTxt + '</p>' : '');
if (s.type === 'grid') {
h += '<div class="opts">' + s.opts.map(function (o) {
h += '<div class="opts">' + optList.map(function (o) {
return '<button class="opt' + (s.get() === o[0] ? ' sel' : '') + '" data-v="' + o[0] + '">' + t(o[1]) + '</button>';
}).join('') + '</div>';
} else if (s.type === 'opts') {
h += '<div class="opts">' + s.opts.map(function (o) {
return '<button class="opt' + (String(s.get()) === String(o[0]) ? ' sel' : '') + '" data-v="' + o[0] + '">' + t(o[1]) + (o[2] ? '<small>' + t(o[2]) + '</small>' : '') + '</button>';
h += '<div class="opts">' + optList.map(function (o) {
var d = o[2] ? (typeof o[2] === 'function' ? o[2]() : t(o[2])) : '';
return '<button class="opt' + (String(s.get()) === String(o[0]) ? ' sel' : '') + '" data-v="' + o[0] + '">' + t(o[1]) + (d ? '<small>' + d + '</small>' : '') + '</button>';
}).join('') + '</div>';
} else if (s.type === 'chips') {
var cur = s.get() || [];
@@ -238,6 +336,11 @@
h += '<div class="fld"><input type="number" id="f1" value="' + (s.get() == null ? '' : s.get()) + '" min="' + (s.min != null ? s.min : 0) + '" max="' + (s.max != null ? s.max : 1e9) + '" inputmode="numeric" ' + (s.money ? 'placeholder="0"' : 'placeholder="' + fmtInt(s.max) + '"') + '></div>';
} else if (s.type === 'fields') {
h += s.fields.map(function (f, i) {
if (f.key === 'pensionAnnual' && psOn()) {
var pr = psCompute(+state.a.retireAge);
return '<div class="fld"><label>' + t(f.labelK) + '</label><p class="hint" style="margin:4px 0 0">' +
t('q.extra.psNote', { v: pr ? money(pr.pensionAtRetirement) + t('unit.yr') : t('q.extra.psNotePending') }) + '</p></div>';
}
return '<div class="fld"><label>' + t(f.labelK) + ' <span class="unit">' + t('fields.unit') + '</span></label><input type="number" id="f' + (i + 1) + '" data-k="' + f.key + '" value="' + (s.get(f.key) == null ? '' : s.get(f.key)) + '" min="0" inputmode="numeric" placeholder="0"></div>';
}).join('');
} else if (s.type === 'slider') {
@@ -319,6 +422,7 @@
var ins;
if (proj.depletionAge) ins = t('i.depletion', { age: proj.depletionAge });
else if (psCoverageInsight(p, proj)) ins = psCoverageInsight(p, proj);
else if (p.cppStartAge < 65) ins = t('i.cppEarly', { age: p.cppStartAge });
else if (p.oasStartAge >= 70) ins = t('i.oasLate', { age: p.oasStartAge });
else if (p.province === 'QC') ins = t('i.qc');
@@ -392,7 +496,7 @@
$(sel).innerHTML = chartH(sel) + (inner || '') + (legendItems ? legend(legendItems) : '');
}
var COL = { bal: '#2563eb', cpp: '#2563eb', oas: '#dc2626', gis: '#f59e0b', wdr: '#10b981', cash: '#0ea5e9', need: '#dc2626' };
var COL = { bal: '#2563eb', cpp: '#2563eb', oas: '#dc2626', gis: '#f59e0b', wdr: '#10b981', cash: '#0ea5e9', need: '#dc2626', ps: '#7c3aed' };
function g1_balance(proj) {
var yrs = proj.years, ages = yrs.map(function (r) { return r.age; });
@@ -421,9 +525,12 @@
var series = [
[t('g2.cpp'), COL.cpp, yrs.map(function (r) { return r.cpp; })],
[t('g2.oas'), COL.oas, yrs.map(function (r) { return Math.max(0, r.oasGross - r.oasClawback); })],
[t('g2.gis'), COL.gis, yrs.map(function (r) { return r.gis; })],
[t('g2.savings'), COL.wdr, yrs.map(function (r) { return r.rrifWithdrawal + r.nonregW + r.tfsaW; })]
[t('g2.gis'), COL.gis, yrs.map(function (r) { return r.gis; })]
];
if (yrs.some(function (r) { return (r.pension || 0) > 0; })) {
series.push([t('g2.psPension'), COL.ps, yrs.map(function (r) { return r.pension || 0; })]);
}
series.push([t('g2.savings'), COL.wdr, yrs.map(function (r) { return r.rrifWithdrawal + r.nonregW + r.tfsaW; })]);
var n = ages.length, tot = [], i, acc;
for (i = 0; i < n; i++) tot[i] = series.reduce(function (s, se) { return s + se[2][i]; }, 0);
var ymax = Math.max(1000, Math.max.apply(null, tot) * 1.08), Y = sy(ymax);
@@ -464,7 +571,7 @@
var scen = [
[t('g4.s0'), p],
[t('g4.s1'), Object.assign({}, p, { monthlyContribution: p.monthlyContribution + 200, employerMatchMonthly: p.employerMatchMonthly })],
[t('g4.s2'), Object.assign({}, p, { retireAge: Math.min(p.retireAge + 2, 75) })],
[t('g4.s2'), psAugment(Object.assign({}, p, { retireAge: Math.min(p.retireAge + 2, 75) }))],
[t('g4.s3'), Object.assign({}, p, { cppStartAge: 70 })]
];
var res = scen.map(function (s) { return [s[0], E.sustainableSpending(s[1]).annual]; });
@@ -545,6 +652,60 @@
el.innerHTML = '<div class="vcap"><b>' + t('vision.label', { slug: slug.replace(/^\w/, function (c) { return c.toUpperCase(); }) }) + '</b><br>' + cap + '</div>';
}
/* ---------- public service pension: insight + results card ---------- */
function psCoverageInsight(p) {
if (!psOn()) return null;
var ps = psCompute(p.retireAge);
if (!ps || !(p.spendingAnnual > 0)) return null;
var pct = Math.round(100 * ps.pensionAtRetirement / p.spendingAnnual);
if (pct < 10 || pct > 250) return null;
return t('i.psCover', { pct: fmtInt(pct) });
}
function renderPsCard(p) {
var el = $('#ps-card'); if (!el) return;
var ps = psCompute(p.retireAge);
if (!ps) { el.hidden = true; el.innerHTML = ''; return; }
el.hidden = false;
var m = fmtMoney2, S = PS.SOURCES, ampe = C.ympe.value;
var rows = [
[t('ps.card.group', { n: ps.group }), ''],
[t('ps.card.service', { y: fmtInt(ps.serviceCapped) }), ''],
[t('ps.card.avg'), m(+state.a.psAvgSalary || 0) + t('unit.yr')],
[t('ps.card.ampe', { v: m(ampe), y: C.ympe.year || 2026 }), ''],
[t('ps.card.lifetime'), m(ps.lifetime) + t('unit.yr')]
];
if (ps.bridge > 0) rows.push([t('ps.card.bridge'), m(ps.bridge) + t('unit.yr')]);
rows.push([t('ps.card.reduction', {
pct: fmtPct(ps.reductionPct, 1),
f: ps.reductionPct === 0 ? t('ps.card.none') : t(ps.formula === 'F1' ? 'ps.card.fF1' : ps.formula === 'F2' ? 'ps.card.fF2' : 'ps.card.fMin')
}), '']);
rows.push(['<b>' + t('ps.card.atRet', { age: fmtInt(ps.startAge) }) + '</b>', '<b>' + m(ps.pensionAtRetirement) + t('unit.yr') + ' · ' + m(ps.pensionAtRetirement / 12) + t('unit.mo') + '</b>']);
rows.push(['<b>' + t('ps.card.from65') + '</b>', '<b>' + m(ps.pensionFrom65) + t('unit.yr') + ' · ' + m(ps.pensionFrom65 / 12) + t('unit.mo') + '</b>']);
var h = '<div class="chart-h"><b>' + t('ps.card.title') + '</b></div><div class="tbl-wrap"><table class="cards">' +
rows.map(function (r) { return '<tr><td>' + r[0] + '</td><td>' + r[1] + '</td></tr>'; }).join('') + '</table></div>';
var notes = [];
if (ps.notes.indexOf('lt2') >= 0) notes.push(t('ps.card.lt2'));
if (ps.earlyOnlyNote === 'deferredG2') notes.push(t('ps.card.deferred'));
if (ps.notes.indexOf('f1Under50') >= 0) notes.push(t('ps.card.under50'));
if (ps.notes.indexOf('f1Under55') >= 0) notes.push(t('ps.card.under55'));
if (ps.startAge < 65 && ps.bridge > 0) notes.push(t('ps.card.bridgeEnds'));
notes.push(t('ps.card.nSurvivor'));
notes.push(t('ps.card.nIndex'));
notes.push(t('ps.card.nPartTime'));
notes.push(t('ps.card.nSplit'));
h += '<p class="cap">' + notes.join(' ') + '</p>';
var asOf = C.asOf, dPspc = '2025-08-15';
h += '<p class="cap">' + t('ps.card.sources') + ' ' +
'<a href="' + (ps.group === 1 ? S.g1En : S.g2En) + '" rel="noopener" target="_blank">' + t('ps.card.srcPspc', { d: dPspc }) + '</a> · ' +
'<a href="' + S.atAGlanceEn + '" rel="noopener" target="_blank">' + t('ps.card.srcGlance') + '</a> · ' +
'<a href="' + S.glossaryEn + '" rel="noopener" target="_blank">' + t('ps.card.srcGloss') + '</a> · ' +
'<a href="' + S.splittingCra + '" rel="noopener" target="_blank">' + t('ps.card.srcCra') + '</a> · ' +
'<a href="' + S.ympeCra + '" rel="noopener" target="_blank">' + t('ps.card.srcYmpe', { v: m(ampe), y: C.ympe.year || 2026 }) + '</a>' +
(state.a.psItLevel && state.a.psItLevel !== 'none' ? ' · <a href="' + S.itAgreement + '" rel="noopener" target="_blank">' + t('ps.card.srcIt', { d: PS.IT_RATES_ASOF }) + '</a>' : '') +
' · <span>' + t('ps.card.dataAsOf', { d: asOf }) + '</span></p>';
el.innerHTML = h;
}
/* ---------- results ---------- */
function renderResults(fast) {
var p = inputs(), proj = E.project(p), rec;
@@ -580,6 +741,7 @@
$('#gap-cards').innerHTML = cards;
g1_balance(proj); g2_income(proj, p); g3_cash(proj); g4_bars(p, fast);
renderPsCard(p);
var tbl = '<tr><th>' + t('t.age') + '</th><th>' + t('t.rrsp') + '</th><th>' + t('t.tfsa') + '</th><th>' + t('t.nonreg') + '</th><th>' + t('t.total') + '</th><th>' + t('t.wdr') + '</th><th>' + t('t.cpp') + '</th><th>' + t('t.oas') + '</th><th>' + t('t.gis') + '</th><th>' + t('t.tax') + '</th><th>' + t('t.unfunded') + '</th></tr>';
var n = 0;
+36 -6
View File
@@ -166,6 +166,8 @@
cppStartAge: 65, cppOverrideMonthlyAt65: 0, oasStartAge: 65,
rrspWithdrawalOverrideAnnual: 0,
pensionAnnual: 0, rentalAnnual: 0, otherAnnual: 0,
pensionStartAge: 0, // 0 = defaults to retireAge (public-service pension)
pensionBridgeAnnual: 0, // pre-65 bridge benefit, ends at 65 (PSPC)
realReturnPre: 0.035, realReturnPost: 0.025, inflation: 0.02, lifeExp: 95
};
function fillDefaults(inp) {
@@ -175,13 +177,31 @@
return o;
}
/* ---- Public-service pension: start age & bridge benefit ----
* pensionStartAge: pension income pays from max(retireAge, pensionStartAge)
* (0 = not set → starts at retirement, legacy behaviour).
* Bridge benefit is a term annuity payable from start to the 65th birthday. */
function pensionStartAge(p) { return Math.max(p.retireAge, +p.pensionStartAge || 0); }
function pensionBridgeAnnual(p, selfAge) {
var b = +p.pensionBridgeAnnual || 0;
if (!b) return 0;
return (selfAge >= pensionStartAge(p) && selfAge < 65) ? b : 0;
}
/* ---- Core year-by-year projection (single person) ---- */
function projectSingle(p) {
var at65 = p.cppOverrideMonthlyAt65 > 0 ? p.cppOverrideMonthlyAt65 : estimateCppMonthlyAt65(p.income, p.age);
var rrsp = Math.max(0, p.rrsp), tfsa = Math.max(0, p.tfsa), nonreg = Math.max(0, p.nonreg);
var years = [], depletionAge = null, totalTax = 0, totalClawback = 0, nestEgg = null;
var end = Math.ceil(p.lifeExp);
var other0 = (p.pensionAnnual || 0) + (p.rentalAnnual || 0) + (p.otherAnnual || 0);
var rentalOther = (p.rentalAnnual || 0) + (p.otherAnnual || 0);
var psStart = pensionStartAge(p); // pension income starts here
function penAt(a) { // pension + bridge payable at age a
var v = 0;
if (a >= psStart) v += (p.pensionAnnual || 0);
v += pensionBridgeAnnual(p, a); // 0 at/after 65, 0 before psStart
return v;
}
for (var a = p.age; a < end; a++) {
var row = { age: a };
@@ -189,7 +209,7 @@
rrsp = rrsp * (1 + p.realReturnPre) + 12 * ((p.monthlyContribution || 0) + (p.employerMatchMonthly || 0));
tfsa = tfsa * (1 + p.realReturnPre);
nonreg = nonreg * (1 + p.realReturnPre);
row.phase = 'accum'; row.cpp = 0; row.oasGross = 0; row.oasClawback = 0; row.gis = 0; row.other = 0;
row.phase = 'accum'; row.cpp = 0; row.oasGross = 0; row.oasClawback = 0; row.gis = 0; row.other = 0; row.pension = 0;
row.rrifWithdrawal = 0; row.nonregW = 0; row.tfsaW = 0; row.taxable = 0; row.tax = 0;
row.cash = 0; row.need = 0; row.unfunded = 0;
row.rrspBal = rrsp; row.tfsaBal = tfsa; row.nonregBal = nonreg; row.totalBal = rrsp + tfsa + nonreg;
@@ -203,6 +223,7 @@
var rrifMin = a >= 71 ? rrifMinPct(a) / 100 * rrspJan : 0;
var cpp = cppMonthlyAt(a, p.cppStartAge, at65) * 12;
var oasGross = oasMonthlyAt(a, p.oasStartAge) * 12;
var other0 = rentalOther + penAt(a);
var pre = cpp + oasGross + other0;
var need = p.spendingAnnual;
@@ -210,7 +231,7 @@
function solve(W) {
W = Math.min(Math.max(W, 0), rrspJan);
var taxable1 = pre + W;
var opts1 = { pensionIncome: (p.pensionAnnual || 0) + (a >= 65 ? W : 0) };
var opts1 = { pensionIncome: (a >= 65 && a >= psStart ? (p.pensionAnnual || 0) : 0) + (a >= 65 ? W : 0) };
var t1 = tax(taxable1, p.province, a, opts1);
var c1 = clawback(taxable1);
var g = gis(taxable1 - oasGross, 'single');
@@ -267,6 +288,7 @@
totalTax += r.tax.total; totalClawback += r.cw;
row.phase = 'draw';
row.cpp = cpp; row.oasGross = oasGross; row.oasClawback = r.cw; row.gis = r.gis; row.other = other0;
row.pension = penAt(a); // pension + bridge only (for chart breakdown)
row.rrifWithdrawal = r.W; row.nonregW = r.n; row.tfsaW = r.t;
row.taxable = r.taxable; row.tax = r.tax.total; row.cash = Math.min(r.cash, need);
row.need = need; row.unfunded = unfunded;
@@ -310,12 +332,18 @@
rrspBal: ra.rrspBal + rb.rrspBal, tfsaBal: ra.tfsaBal + rb.tfsaBal, nonregBal: ra.nonregBal + rb.nonregBal
};
row.totalBal = row.rrspBal + row.tfsaBal + row.nonregBal;
row.pension = (ra.pension || 0) + (rb.pension || 0); // chart breakdown (life pension + bridge)
row.need = ra.need + rb.need;
var taxSum = ra.tax + rb.tax, clawSum = ra.oasClawback + rb.oasClawback;
row.taxable = ra.taxable + rb.taxable;
// Pension income splitting (both 65+): split pension+RRIF 50/50, keep the cheaper total.
if (ra.phase === 'draw' && rb.phase === 'draw' && yr >= 65 && (ra.age >= 65 && rb.age >= 65)) {
var pa = (me.pensionAnnual || 0) + ra.rrifWithdrawal, pb2 = (partnerInp.pensionAnnual || 0) + rb.rrifWithdrawal;
// Pension income splitting (CRA): a life annuity from a superannuation plan is
// splittable at ANY age once payable (pensionStartAge); RRIF withdrawals only at
// 65+. The bridge benefit (term annuity) is excluded — documented modelling
// choice. Split 50/50 the eligible amounts, keep the cheaper total.
if (ra.phase === 'draw' && rb.phase === 'draw') {
var psA = pensionStartAge(me), psB = pensionStartAge(partnerInp);
var pa = ((me.pensionAnnual > 0 && psA <= yr) ? me.pensionAnnual : 0) + (yr >= 65 ? ra.rrifWithdrawal : 0);
var pb2 = ((partnerInp.pensionAnnual > 0 && psB <= yr) ? partnerInp.pensionAnnual : 0) + (yr >= 65 ? rb.rrifWithdrawal : 0);
var shift = 0.5 * (pa - pb2);
if (Math.abs(shift) > 1) {
var tA = tax(ra.taxable - shift, p.province, yr, { pensionIncome: pa - shift });
@@ -415,6 +443,8 @@
project: project,
sustainableSpending: sustainableSpending,
recommendations: recommendations,
pensionStartAge: pensionStartAge,
pensionBridgeAnnual: pensionBridgeAnnual,
defaults: DEFAULTS,
constants: C
};
+200
View File
@@ -0,0 +1,200 @@
/* ============================================================================
* pspension.js — Optional federal public service pension (PSSA) module.
*
* Implements the PSPC-published formula for members with 2+ years of
* pensionable service (contributed to the pension):
*
* Lifetime annual = 1.375% × min(avg5, AMPE) × min(S, 35)
* + 2% × max(0, avg5 − AMPE) × min(S, 35)
* Bridge benefit = 0.625% × min(avg5, AMPE) × min(S, 35) (pre-65 only;
* payable from retirement, ends at the start of the month
* of the 65th birthday — coincides with CPP/D coverage)
* Total pre-65 = 2% × avg5 × min(S, 35)
*
* avg5 = average salary over the 5 consecutive years of highest paid service
* (modelled as the user-entered figure; AMPE = Average Maximum
* Pensionable Earnings — indexed to YMPE, 100% of YMPE since 2010;
* we use the current YMPE from canada.js as a real-dollar proxy).
* S = years of recognised pensionable service, capped at 35.
*
* Early-reduction (annual allowance) decision tree — Group 1 (joined on/after
* 1999-04-01 and before 2013-01-01; payable from 50):
* unreduced if (age ≥ 60 and S ≥ 2) or (age ≥ 55 and S ≥ 30);
* F1 (S < 25): 5% × (60 − age)
* F2 (S ≥ 25): max( 5% × (55 − age), 5% × (30 − S) )
* age ≥ 55 with S ≥ 25: apply the LOWER of F1 and F2 (PSPC page);
* leaving before 50: F1 with age pinned at 50 → 50% reduction cap.
* Group 2 (joined on/after 2013-01-01; payable from 55):
* unreduced if (age ≥ 65 and S ≥ 2) or (age ≥ 60 and S ≥ 30);
* F1 (S < 25): 5% × (65 − age)
* F2 (S ≥ 25): max( 5% × (60 − age), 5% × (30 − S) )
* age ≥ 60 with S ≥ 25: apply the LOWER of F1 and F2;
* leaving before 55: F1 pinned at 55 → 50%; the planner models the
* PSPC-default path: deferred annuity, unreduced, payable at 65.
* Reduction applies to the WHOLE pension (lifetime + bridge); ages/service
* to the nearest tenth of a year. Under 2 years of service: return of
* contributions only (no pension).
*
* Sources verified 2026-02 (constants re-checked verbatim, page dated 2025-08-15).
* UMD: window.RetirePS in the browser, module.exports in Node.
* ==========================================================================*/
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.RetirePS = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
var PCT_LOW = 0.01375; // 1.375% — career average up to AMPE
var PCT_HIGH = 0.02; // 2% — career average above AMPE
var BRIDGE_PCT = 0.00625; // 0.625% — bridge benefit (pre-65), up to AMPE only
var MAX_SERVICE = 35; // pensionable-service cap
var EARLY_PCT = 0.05; // 5% per year (or part-year, to the tenth)
var GROUP1_CUTOFF_YEAR = 2012; // joined on/before 2012-12-31 → Group 1
var MAX_REDUCTION = 0.5; // 50% cap (F1 at the earliest payable age)
/* IT (Indian and Northern Government Services) annual rates of pay,
* TBS "IT" collective agreement, Appendix A, wage-adjustment row
* effective 2024-12-22. Overridable: the UI exposes these as defaults. */
var IT_RATES_ASOF = '2024-12-22';
var IT_RATES_SOURCE = 'https://www.canada.ca/en/treasury-board-secretariat/topics/pay/collective-agreements/it.html';
var IT_LEVELS = [
{ id: 'IT-01', steps: [69361, 71925, 74485, 77031, 79574, 82120, 84661, 89382] },
{ id: 'IT-02', steps: [85854, 88603, 91348, 94095, 96844, 99589, 102335, 105080] },
{ id: 'IT-03', steps: [101343, 104833, 108324, 111817, 115306, 118791, 122281, 125914] },
{ id: 'IT-04', steps: [116037, 120038, 124039, 128039, 132039, 136041, 140039, 144434] },
{ id: 'IT-05', steps: [133582, 138563, 143539, 148520, 153503, 158485, 163465, 168446, 174076] }
];
var SOURCES = {
g1En: 'https://www.canada.ca/en/public-services-procurement/services/pay-pension/public-service-pension-plan/retired/entitlement-information-packages/2-more-years-pensionable-service/member-on-before-december-31-2012.html',
g1Fr: 'https://www.canada.ca/fr/services-publics-approvisionnement/services/paye-pension/regime-retraite-fonction-publique/retraite/trousse-information-presentations/2-ans-plus-service-ouvrant-droit-pension/participant-31-decembre-2012-ou-avant.html',
g2En: 'https://www.canada.ca/en/public-services-procurement/services/pay-pension/public-service-pension-plan/retired/entitlement-information-packages/2-more-years-pensionable-service/member-on-after-january-1-2013.html',
g2Fr: 'https://www.canada.ca/fr/services-publics-approvisionnement/services/paye-pension/regime-retraite-fonction-publique/retraite/trousse-information-presentations/2-ans-plus-service-ouvrant-droit-pension/participant-1er-janvier-2013-ou-apres.html',
glossaryEn: 'https://www.canada.ca/en/treasury-board-secretariat/services/pension-plan/plan-information/glossary.html',
atAGlanceEn: 'https://www.canada.ca/en/treasury-board-secretariat/services/pension-plan/plan-information/public-service-pension-glance.html',
ympeCra: 'https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/payroll/payroll-deductions-contributions/canada-pension-plan-cpp/cpp-enhancement.html',
splittingCra: 'https://www.canada.ca/en/revenue-agency/services/tax/individuals/topics/pension-income-splitting.html',
itAgreement: IT_RATES_SOURCE
};
function tenth(x) { return Math.round(x * 10) / 10; }
function round2(x) { return Math.round(x * 100) / 100; }
/* 1 | 2 from the year the member joined the federal public service. */
function psGroup(joinYear) {
var y = +joinYear;
if (!y) return null;
return y <= GROUP1_CUTOFF_YEAR ? 1 : 2;
}
function itLevel(id) {
for (var i = 0; i < IT_LEVELS.length; i++) if (IT_LEVELS[i].id === id) return IT_LEVELS[i];
return null;
}
function itRange(id) {
var lv = itLevel(id);
if (!lv) return null;
return { min: lv.steps[0], top: lv.steps[lv.steps.length - 1], steps: lv.steps, asof: IT_RATES_ASOF };
}
/* Early-reduction (annual allowance) per the PSPC decision tree.
* Returns { unreduced, pct, formula: 'F1'|'F2'|'min(F1,F2)'|null, note }
* with pct in [0, 0.5]; ages/service to the nearest tenth (PSPC).
* note keys: 'unreduced' | 'f1Under50' | 'f1Under55' (fixed-age rules). */
function psEarlyReduction(group, retireAge, serviceYears) {
var age = tenth(+retireAge), S = tenth(+serviceYears);
var g = +group === 2 ? 2 : 1;
var R = function (pct, formula, unreduced, note) {
return { unreduced: !!unreduced, pct: pct, formula: formula, note: note || null };
};
if (g === 1) {
if (age >= 60 || (age >= 55 && S >= 30)) return R(0, null, true, 'unreduced');
var f1 = EARLY_PCT * Math.max(0, 60 - age);
if (age < 50) return R(MAX_REDUCTION, 'F1', false, 'f1Under50'); // payable from 50, age pinned at 50
if (S < 25) return R(Math.min(MAX_REDUCTION, f1), 'F1', false, null);
var f2a = Math.max(0, EARLY_PCT * (55 - age)), f2b = Math.max(0, EARLY_PCT * (30 - S));
var f2 = Math.max(f2a, f2b);
if (age >= 55) return R(Math.min(MAX_REDUCTION, Math.min(f1, f2)), 'min(F1,F2)', false, null);
return R(Math.min(MAX_REDUCTION, f2), 'F2', false, null);
}
if (age >= 65 || (age >= 60 && S >= 30)) return R(0, null, true, 'unreduced');
var g1 = EARLY_PCT * Math.max(0, 65 - age);
if (age < 55) return R(MAX_REDUCTION, 'F1', false, 'f1Under55'); // payable from 55, age pinned at 55
if (S < 25) return R(Math.min(MAX_REDUCTION, g1), 'F1', false, null);
var h2a = Math.max(0, EARLY_PCT * (60 - age)), h2b = Math.max(0, EARLY_PCT * (30 - S));
var h2 = Math.max(h2a, h2b);
if (age >= 60) return R(Math.min(MAX_REDUCTION, Math.min(g1, h2)), 'min(F1,F2)', false, null);
return R(Math.min(MAX_REDUCTION, h2), 'F2', false, null);
}
/* Lifetime annual pension (years capped at 35 by the caller's service). */
function psLifetimePension(avgSalary, ampe, years) {
var Sc = Math.min(tenth(+years || 0), MAX_SERVICE);
var capAvg = Math.min(Math.max(0, +avgSalary || 0), Math.max(0, +ampe || 0));
var over = Math.max(0, (+avgSalary || 0) - (+ampe || 0));
return round2(PCT_LOW * capAvg * Sc + PCT_HIGH * over * Sc);
}
/* Bridge benefit: 0.625% × min(avg,AMPE) × min(S,35) — caller zeroes at 65+. */
function psBridgeBenefit(avgSalary, ampe, years) {
var Sc = Math.min(tenth(+years || 0), MAX_SERVICE);
var capAvg = Math.min(Math.max(0, +avgSalary || 0), Math.max(0, +ampe || 0));
return round2(BRIDGE_PCT * capAvg * Sc);
}
/* Full benefit computation.
* Input: { joinYear, retireAge, serviceYears, avgSalary, ampe }
* Output (plan contract): { group, serviceCapped, lifetime, bridge,
* reductionPct, formula, pensionAtRetirement, pensionFrom65, startAge,
* deferred, earlyOnlyNote, notes[] } — $ annual, to the cent. */
function computePSPension(o) {
o = o || {};
var avg = Math.max(0, +o.avgSalary || 0);
var ampe = Math.max(0, +o.ampe || 0);
var S = tenth(+o.serviceYears || 0);
var age = tenth(+o.retireAge || 0);
var group = +o.group === 1 || +o.group === 2 ? +o.group : psGroup(o.joinYear) || 1;
var Sc = Math.min(S, MAX_SERVICE);
var lifetime = psLifetimePension(avg, ampe, Sc);
var out = {
group: group, serviceCapped: Sc, startAge: age, deferred: false,
lifetime: lifetime, bridge: 0,
reductionPct: 0, formula: null,
pensionAtRetirement: lifetime, pensionFrom65: lifetime,
earlyOnlyNote: null, notes: []
};
if (S < 2) { // return of contributions only — no annuity (PSPC)
out.lifetime = 0; out.pensionAtRetirement = 0; out.pensionFrom65 = 0;
out.notes.push('lt2');
return out;
}
var red = { pct: 0, formula: null };
if (group === 2 && age < 55) {
// PSPC default (no election within 1 year): deferred annuity, unreduced, at 65.
out.deferred = true; out.startAge = 65;
red = { pct: 0, formula: null };
out.earlyOnlyNote = 'deferredG2';
} else {
red = psEarlyReduction(group, age, S);
out.startAge = age;
if (red.note === 'f1Under50') { out.startAge = 50; out.notes.push('f1Under50'); }
if (red.note === 'f1Under55') { out.startAge = 55; out.notes.push('f1Under55'); }
}
out.reductionPct = red.pct;
out.formula = red.formula;
out.bridge = out.startAge < 65 ? psBridgeBenefit(avg, ampe, Sc) : 0;
out.pensionAtRetirement = round2((out.lifetime + out.bridge) * (1 - out.reductionPct));
out.pensionFrom65 = round2(out.lifetime * (1 - out.reductionPct));
return out;
}
return {
PCT_LOW: PCT_LOW, PCT_HIGH: PCT_HIGH, BRIDGE_PCT: BRIDGE_PCT,
MAX_SERVICE: MAX_SERVICE, GROUP1_CUTOFF_YEAR: GROUP1_CUTOFF_YEAR,
IT_LEVELS: IT_LEVELS, IT_RATES_ASOF: IT_RATES_ASOF, IT_RATES_SOURCE: IT_RATES_SOURCE,
SOURCES: SOURCES,
psGroup: psGroup, psLifetimePension: psLifetimePension,
psBridgeBenefit: psBridgeBenefit, psEarlyReduction: psEarlyReduction,
computePSPension: computePSPension,
itLevel: itLevel, itRange: itRange
};
});
+144 -6
View File
@@ -56,6 +56,7 @@
qc: 'ℹ️ Québec: QPP is <b>not</b> modelled — add your QPP estimate as “Pension $/yr”. QPP typically replaces ~35% of income.',
cliff: 'You’ll hit the RRIF-minimum tax cliff at 71. TFSA room now softens it later — check the gap cards.',
couple: 'With pension splitting + household GIS, your joint taxes drop — the panel shows the joint total.',
psCover: 'ℹ️ Your public service pension covers about <b>{pct}%</b> of your retirement spending goal.',
live: 'Live numbers — change any answer and this panel moves.'
},
prov: {
@@ -105,7 +106,42 @@
},
extra: {
title: 'Other income', hint: 'DB pension, rental, royalties… leave 0 if none.',
pension: 'Pension $/yr', rental: 'Rental $/yr', other: 'Other $/yr'
pension: 'Pension $/yr', rental: 'Rental $/yr', other: 'Other $/yr',
psNote: 'Covered by your public service pension module: {v} — see your plan.',
psNotePending: 'Answer the pension module questions — this is computed from them.'
},
ps: {
title: 'Do you have a federal public service pension?',
hint: 'Optional — for federal employees under the PSSA. We’ll compute it from PSPC rules and add it to your plan.',
yes: 'Yes — I am (or was) a federal public servant', yesD: 'Public Service Pension Plan (PSSA)',
no: 'No / not sure', noD: 'Skip the pension module'
},
ps_join: {
title: 'What year did you join the federal public service?',
hint: 'This sets your group: on/before Dec 31, 2012 = Group 1 (annual allowance from 50); on/after Jan 1, 2013 = Group 2 (from 55).'
},
ps_service: {
title: 'Years of pensionable service',
hint: 'Recognized by PSPC (the calculation caps it at 35 years).'
},
ps_salary: {
title: 'Average salary — your 5 consecutive highest-paid years',
hint: 'The annual average of your five consecutive years of highest paid service, in today’s dollars.',
hintIt: 'Your IT table shows {lo}–{hi} a year (TBS–PSAC, effective {asof}) — pick your step next, or type your own.'
},
ps_occ: {
title: 'Are you in the IT (Information Technology) group?',
hint: 'The pension formula is the same for every occupational group — we just show official TBS–PSAC pay tables to help with the salary.',
it: 'Yes — IT group', itD: 'IT-01 to IT-05, TBS–PSAC rates',
other: 'No — another group', otherD: 'I’ll type my average salary'
},
ps_it: {
title: 'Which IT level are you at?',
hint: 'Official annual rates, effective {asof} — you can override with your own salary.',
stepTitle: 'Which step of {level}?',
stepHint: 'Range {lo}–{hi} · rates effective {asof}. Your step sets the 5-year average (override it on the salary step).',
stepMin: 'first step', stepTop: 'top step',
skip: 'Not my pay scale', skipD: 'I’ll type my average salary'
},
assumptions: {
title: 'Assumptions', hint: 'Real (above-inflation) returns. Defaults match a balanced portfolio.',
@@ -131,7 +167,40 @@
h: 'Where retirement income comes from',
cap: 'CPP, OAS, GIS and what you must withdraw from savings — stacked, after-tax view.',
empty: 'Retirement after your plan horizon.',
cpp: 'CPP', oas: 'OAS (net)', gis: 'GIS', savings: 'From savings'
cpp: 'CPP', oas: 'OAS (net)', gis: 'GIS', savings: 'From savings', psPension: 'Public service pension'
},
ps: {
card: {
title: 'Your federal public service pension',
group: 'Group {n} (rules from your join year)',
service: 'Pensionable service used: {y} years (max 35)',
avg: '5-year average salary (highest paid)',
ampe: 'AMPE (proxy = YMPE {v}, {y})',
lifetime: 'Lifetime annual pension',
bridge: 'Bridge benefit (until 65)',
reduction: 'Early reduction (annual allowance): {pct} — {f}',
none: 'None — you qualify for an unreduced pension',
fF1: 'Formula 1', fF2: 'Formula 2', fMin: 'lower of F1 and F2',
atRet: 'Your pension at retirement (from {age})',
from65: 'From age 65 (bridge ends)',
lt2: 'Under 2 years of service: return of contributions only — no annuity.',
deferred: 'You retire before 55 (Group 2): modelled as a deferred annuity — unreduced pension from age 65. Alternative: an annual allowance from age 55 would be permanently reduced by 50%.',
under50: 'Retiring before 50 (Group 1): the annual allowance is payable from your 50th birthday, with Formula 1 computed at age 50 — a 50% reduction.',
under55: 'Payable from 55 with Formula 1 computed at age 55 — a 50% reduction.',
bridgeEnds: 'The bridge benefit ends the month you turn 65 (earlier only with CPP/QPP disability).',
nSurvivor: 'Survivor: 50% of your pre-65 pension before any reduction, plus child allowances to 18 (25 if in school).',
nIndex: 'Indexation: the pension is increased every January 1 by CPI.',
nPartTime: 'Part-time service is pro-rated for assigned hours — not modelled; full-time assumed.',
nSplit: 'The bridge benefit is a term annuity — excluded from pension income splitting (conservative). The lifetime pension is splittable at any age.',
sources: 'Sources:',
srcPspc: 'PSPC formula & reduction rules ({d})',
srcGlance: 'TBS pension at a glance',
srcGloss: 'TBS glossary (AMPE)',
srcCra: 'CRA pension splitting',
srcYmpe: 'CRA YMPE {v} ({y})',
srcIt: 'TBS–PSAC IT agreement rates ({d})',
dataAsOf: 'benefit constants as of {d}'
}
},
g3: {
h: 'Spending vs. what you\'ll actually have',
@@ -178,7 +247,7 @@
cpp: 'CPP', oas: 'OAS', gis: 'GIS', tax: 'Tax+claw', unfunded: 'Unfunded'
},
d: {
main: '<b>Estimates only — not financial advice.</b> Simplified model: GIS uses a simplified 50% phase-out; Quebec shown with the 16.5% federal abatement (QPP and Quebec payroll taxes not modelled); DB pensions not modelled; returns are constant real rates; Ontario surtax modelled on base tax.',
main: '<b>Estimates only — not financial advice.</b> Simplified model: GIS uses a simplified 50% phase-out; Quebec shown with the 16.5% federal abatement (QPP and Quebec payroll taxes not modelled); federal public service pension (PSSA) modelled when enabled — other DB pensions not; returns are constant real rates; Ontario surtax modelled on base tax.',
privacy: '🔒 100% client-side — answers never leave your browser. Your answers are saved only in this browser (localStorage).'
},
res: {
@@ -244,6 +313,7 @@
qc: 'ℹ️ Québec : le RRQ <b>n\'est pas</b> modélisé — ajoutez votre estimation du RRQ sous « Pension ($/an) ». Le RRQ remplace habituellement ~35 % du revenu.',
cliff: 'Vous heurterez le palier fiscal du minimum de FERR à 71 ans. Du CELI aujourd\'hui en atténue l\'effet plus tard — voyez les cartes d\'écart.',
couple: 'Avec le fractionnement du revenu de pension et la SRG du ménage, vos impôts conjoints baissent — le panneau affiche le total conjoint.',
psCover: 'ℹ️ Votre pension de la fonction publique couvre environ <b>{pct}%</b> de votre objectif de dépenses de retraite.',
live: 'Chiffres en direct — changez une réponse et ce panneau bouge.'
},
prov: {
@@ -293,7 +363,42 @@
},
extra: {
title: 'Autres revenus', hint: 'Pension à prestations, location, redevances… laissez 0 sinon.',
pension: 'Pension par année', rental: 'Location par année', other: 'Autre par année'
pension: 'Pension par année', rental: 'Location par année', other: 'Autre par année',
psNote: 'Couvert par votre module de pension fédérale : {v} — voir votre plan.',
psNotePending: 'Répondez aux questions du module de pension — ce montant en découle.'
},
ps: {
title: 'Avez-vous une pension de la fonction publique fédérale?',
hint: 'Facultatif — pour les fonctionnaires fédéraux (LRFP). Nous la calculons selon les règles de la PSCC et l\'ajoutons à votre plan.',
yes: 'Oui — je suis (ou j\'étais) fonctionnaire fédéral', yesD: 'Régime de retraite de la fonction publique (LRFP)',
no: 'Non / je ne sais pas', noD: 'Passer le module de pension'
},
ps_join: {
title: 'En quelle année avez-vous adhéré à la fonction publique fédérale?',
hint: 'Cela détermine votre groupe : avant le 31 déc. 2012 = groupe 1 (allocation annuelle dès 50 ans); à partir du 1er janv. 2013 = groupe 2 (dès 55 ans).'
},
ps_service: {
title: 'Années de service ouvrant droit à pension',
hint: 'Reconnues par la PSCC (le calcul est plafonné à 35 années).'
},
ps_salary: {
title: 'Salaire moyen — vos 5 années consécutives les mieux rémunérées',
hint: 'La moyenne annuelle de vos cinq années de service consécutives les mieux rémunérées, en dollars d\'aujourd\'hui.',
hintIt: 'Votre barre TI affiche {lo}–{hi} par année (SECT–PSAC, en vigueur le {asof}) — choisissez votre échelon, ou saisissez votre propre salaire.'
},
ps_occ: {
title: 'Faites-vous partie du groupe TI (technologies de l\'information)?',
hint: 'La formule de pension est la même pour tous les groupes professionnels — les barèmes officiels SECT–PSAC aident seulement à estimer le salaire.',
it: 'Oui — groupe TI', itD: 'TI-01 à TI-05, taux SECT–PSAC',
other: 'Non — un autre groupe', otherD: 'Je saisirai mon salaire moyen'
},
ps_it: {
title: 'Quel niveau TI occupez-vous?',
hint: 'Taux annuels officiels, en vigueur le {asof} — vous pouvez les remplacer par votre propre salaire.',
stepTitle: 'Quel échelon du niveau {level}?',
stepHint: 'Fourchette {lo}–{hi} · taux en vigueur le {asof}. Votre échelon fixe la moyenne sur 5 ans (modifiable à l\'étape du salaire).',
stepMin: 'premier échelon', stepTop: 'échelon supérieur',
skip: 'Ce n\'est pas mon barème', skipD: 'Je saisirai mon salaire moyen'
},
assumptions: {
title: 'Hypothèses', hint: 'Rendements réels (supérieurs à l\'inflation). Les valeurs par défaut correspondent à un portefeuille équilibré.',
@@ -319,7 +424,40 @@
h: 'D\'où vient le revenu de retraite',
cap: 'RPC, SV, SRG et ce que vous retirez de l\'épargne — empilé, après impôt.',
empty: 'Retraite après l\'horizon du plan.',
cpp: 'RPC', oas: 'SV (net)', gis: 'SRG', savings: 'Épargne'
cpp: 'RPC', oas: 'SV (net)', gis: 'SRG', savings: 'Épargne', psPension: 'Pension de la fonction publique'
},
ps: {
card: {
title: 'Votre pension de la fonction publique fédérale',
group: 'Groupe {n} (règles selon votre année d\'adhésion)',
service: 'Service ouvrant droit à pension retenu : {y} années (max 35)',
avg: 'Salaire moyen sur 5 ans (les mieux rémunérées)',
ampe: 'MMGP (proxy = PGMA {v}, {y})',
lifetime: 'Pension viagère annuelle',
bridge: 'Prestation de raccordement (jusqu\'à 65 ans)',
reduction: 'Réduction hâtive (allocation annuelle) : {pct} — {f}',
none: 'Aucune — vous avez droit à une pension sans réduction',
fF1: 'Formule 1', fF2: 'Formule 2', fMin: 'la plus faible de F1 et F2',
atRet: 'Votre pension à la retraite (dès {age} ans)',
from65: 'À partir de 65 ans (fin du raccordement)',
lt2: 'Moins de 2 années de service : remboursement des cotisations seulement — pas de rente.',
deferred: 'Vous prenez votre retraite avant 55 ans (groupe 2) : modélisé comme pension différée — pension sans réduction dès 65 ans. Autre option : une allocation annuelle dès 55 ans serait réduite de 50 % de façon permanente.',
under50: 'Retraite avant 50 ans (groupe 1) : l\'allocation annuelle est payable à partir de votre 50e anniversaire, calculée à 50 ans selon la formule 1 — réduction de 50 %.',
under55: 'Payable dès 55 ans, calculée à 55 ans selon la formule 1 — réduction de 50 %.',
bridgeEnds: 'La prestation de raccordement prend fin le mois de votre 65e anniversaire (avant 65 ans seulement en cas d\'invalidité RPC/RRQ).',
nSurvivor: 'Conjoint survivant : 50 % de la pension avant 65 ans calculée avant toute réduction, plus les allocations aux enfants jusqu\'à 18 ans (25 ans aux études).',
nIndex: 'Indexation : la pension est augmentée chaque 1er janvier selon l\'IPC.',
nPartTime: 'Le service à temps partiel est prorogé selon les heures de service assignées — non modélisé; temps plein présumé.',
nSplit: 'La prestation de raccordement est une rente temporaire — exclue du fractionnement du revenu de pension (choix prudent). La pension viagère est fractionnable à tout âge.',
sources: 'Sources :',
srcPspc: 'PSCC — formule et réductions ({d})',
srcGlance: 'SECT — la pension en bref',
srcGloss: 'SECT — glossaire (MMGP)',
srcCra: 'ARC — fractionnement du revenu de pension',
srcYmpe: 'ARC — PGMA {v} ({y})',
srcIt: 'Convention collective TI — SECT–PSAC ({d})',
dataAsOf: 'constantes au {d}'
}
},
g3: {
h: 'Dépenses vs ce que vous aurez',
@@ -366,7 +504,7 @@
cpp: 'RPC', oas: 'SV', gis: 'SRG', tax: 'Impôt + récup.', unfunded: 'Non financé'
},
d: {
main: '<b>Estimations seulement — il ne s\'agit pas de conseils financiers.</b> Modèle simplifié : retrait progressif simplifié de 50 % pour la SRG; Québec présenté avec le dégagement fédéral de 16,5 % (RRQ et cotisations sociales québécoises non modélisés); pensions à prestations non modélisées; rendements à taux réels constants; surtaxe de l\'Ontario calculée sur l\'impôt de base.',
main: '<b>Estimations seulement — il ne s\'agit pas de conseils financiers.</b> Modèle simplifié : retrait progressif simplifié de 50 % pour la SRG; Québec présenté avec le dégagement fédéral de 16,5 % (RRQ et cotisations sociales québécoises non modélisés); pension de la fonction publique fédérale (LRFP) modélisée si activée — autres pensions à prestations non modélisées; rendements à taux réels constants; surtaxe de l\'Ontario calculée sur l\'impôt de base.',
privacy: '🔒 100 % côté client — vos réponses ne quittent jamais votre navigateur. Vos réponses ne sont enregistrées que dans ce navigateur (localStorage).'
},
res: {
+3 -1
View File
@@ -70,6 +70,7 @@
<!-- Chart titles/captions render inside each card's .chart-h via app.js chart() (round 2 Fix 2) -->
<div id="g1" class="chartbox"></div>
<div id="g2" class="chartbox"></div>
<div id="ps-card" class="chartbox" hidden></div>
<div id="g3" class="chartbox"></div>
<div id="g4" class="chartbox"></div>
<p id="g4-note" class="cap"></p>
@@ -94,7 +95,7 @@
</details>
<div class="disclaimer">
<p data-i18n-html="d.main"><b>Estimates only — not financial advice.</b> Simplified model: GIS uses a simplified 50% phase-out; Quebec shown with the 16.5% federal abatement (QPP and Quebec payroll taxes not modelled); DB pensions not modelled; returns are constant real rates; Ontario surtax modelled on base tax.</p>
<p data-i18n-html="d.main"><b>Estimates only — not financial advice.</b> Simplified model: GIS uses a simplified 50% phase-out; Quebec shown with the 16.5% federal abatement (QPP and Quebec payroll taxes not modelled); federal public service pension (PSSA) modelled when enabled — other DB pensions not; returns are constant real rates; Ontario surtax modelled on base tax.</p>
<p class="stamp" id="stamp">tax rates 2026</p>
<p class="privacy" data-i18n="d.privacy">🔒 100% client-side — answers never leave your browser. Your answers are saved only in this browser (localStorage).</p>
<div class="btn-row">
@@ -108,6 +109,7 @@
<script src="engine/canada.js"></script>
<script src="engine/engine.js"></script>
<script src="engine/pspension.js"></script>
<script src="i18n.js"></script>
<script src="app.js"></script>
</body>
+4
View File
@@ -155,6 +155,10 @@ input[type=range]{width:100%;accent-color:var(--blue)}
.tbl-wrap{overflow:auto;max-height:340px;border:1px solid var(--line);border-radius:8px;margin-top:8px}
table{border-collapse:collapse;font-size:12.5px;width:100%}
th,td{padding:5px 8px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}
/* Card-style 2-col breakdown tables (PS pension card): wrap text, fixed layout → fits 320px. */
table.cards{table-layout:fixed}
table.cards td{white-space:normal;word-wrap:break-word;text-align:left;vertical-align:top}
table.cards td:last-child{text-align:right}
th{position:sticky;top:0;background:var(--bg);text-align:right}
td:first-child,th:first-child{position:sticky;left:0;background:var(--bg)}
tr.dep td{color:var(--red)}