PS pension module: optional federal PSSA engine + UI (PSPC-accurate, EN/FR)
This commit is contained in:
1 parent
fe62022a32
commit
583b5ee2f8
14 files changed
+1991
-27
No files matched your search
+171
-9
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in new issue
Block a user