Files
retire-planner/site/engine/engine.js
T

452 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* retire-planner engine — pure functions, no DOM. UMD so node can unit-test.
* All money is in REAL (today's) dollars. Annual units unless noted.
* Sources/stamps for every constant live in canada.js (window.RetireCanada). */
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) {
var C; try { C = require('./canada.js'); } catch (e) { C = {}; }
module.exports = factory(C);
} else {
root.RetireEngine = factory(root.RetireCanada || {});
}
})(typeof self !== 'undefined' ? self : this, function (C) {
'use strict';
// In-engine fallbacks mirror PLAN.md Stage 1 fallback table (stamped FALLBACK in canada.js).
var FALLBACK = {
cppMax65Monthly: 1433.00, ympe: 69700, oasMax6574Monthly: 713.34, oas75Boost: 1.10,
oasClawbackThreshold: 93454, gisMaxSingleMonthly: 1086.88, gisMaxCoupleMonthly: 1436.68,
gisSingleThr: 20000, gisCoupleThr: 26000
};
function V(key) {
var e = C && C[key];
if (!e) return FALLBACK[key];
if (typeof e === 'number') return e;
return (e && typeof e.value === 'number') ? e.value : FALLBACK[key];
}
function fedBrackets() { return (C.fedBrackets && C.fedBrackets.value) || (C.fedBrackets && C.fedBrackets.brackets) || [
{ up: 57375, rate: 0.15 }, { up: 114750, rate: 0.205 }, { up: 177882, rate: 0.26 },
{ up: 253414, rate: 0.29 }, { up: null, rate: 0.33 }]; }
function fedBPA() { return (C.fedBPA && (C.fedBPA.value !== undefined ? C.fedBPA.value : C.fedBPA)) || 15705; }
function fedAgeAmount() { return (C.fedAgeAmount && (C.fedAgeAmount.value !== undefined ? C.fedAgeAmount.value : C.fedAgeAmount)) || 8790; }
function provTable(p) { return (C.provBrackets && (C.provBrackets.value || C.provBrackets))[p] || null; }
function rrifTable() { return (C.rrifMinPct && (C.rrifMinPct.value || C.rrifMinPct)) || null; }
var AGE_CLAW_START = 44017; // federal age amount clawback threshold (2025)
var BPA_CLAW_FED = 165000; // simplified net-income clawback starts (plan)
var BPA_CLAW_PROV = 173000;
var PIA = 2000; // pension income amount (fed + prov)
function bracketTax(income, brackets) {
var t = 0, prev = 0;
for (var i = 0; i < brackets.length; i++) {
var b = brackets[i], up = (b.up === null || b.up === undefined) ? Infinity : b.up;
if (income <= prev) break;
var seg = Math.min(income, up) - prev;
if (seg > 0) t += seg * b.rate;
prev = up;
}
return Math.max(0, t);
}
/* ---- CPP ---- */
// 0.25 × min(avgEarnings, YMPE) × min(1, contribYears/40) / 12, capped at max.
// contributory period 18→65 (47 yrs), general drop-out ≈17% → /40 effective. ESTIMATE ONLY.
function estimateCppMonthlyAt65(income, age) {
var contribYears = Math.min(47, Math.max(0, age - 22));
var eff = Math.min(1, contribYears / 40);
var m = 0.25 * Math.min(income || 0, V('ympe')) * eff / 12;
return Math.min(m, V('cppMax65Monthly'));
}
// Pension STARTS at startAge with actuarial adj (−0.6%/mo before 65, +0.7%/mo after), stays flat (real).
function cppMonthlyAt(age, startAge, at65) {
if (age < startAge) return 0;
var adj;
if (startAge < 65) adj = 1 - 0.006 * (65 - startAge) * 12; // 60 → 0.64
else if (startAge > 65) adj = 1 + 0.007 * (startAge - 65) * 12; // 70 → 1.42
else adj = 1;
return at65 * adj;
}
/* ---- OAS ---- */
function oasMonthlyAt(age, startAge) {
if (age < startAge) return 0;
var m = V('oasMax6574Monthly');
if (startAge > 65) m *= 1 + 0.006 * (startAge - 65) * 12; // +36% at 70
if (age >= 75) m *= V('oas75Boost'); // +10% at 75
return m;
}
/* ---- OAS recovery tax (clawback) ---- */
function clawback(netIncome) {
return 0.15 * Math.max(0, (netIncome || 0) - V('oasClawbackThreshold'));
}
/* ---- GIS (SIMPLIFIED — stamp: simplified) ---- */
// Only below ~$20k (single) / ~$26k (couple, both 65+) income ex-OAS.
// gis = max(0, gisMax − 0.5 × max(0, incomeExOAS − 5000)) (first $5k employment exemption approximated).
// Returns ANNUAL $, non-taxable.
function gis(incomeExOas, household) {
var single = household !== 'couple';
var thr = single ? V('gisSingleThr') : V('gisCoupleThr');
var maxMo = single ? V('gisMaxSingleMonthly') : V('gisMaxCoupleMonthly');
var i = incomeExOas || 0;
if (i >= thr) return 0;
// All-annual units: gisMax annual = maxMo*12; reduction 0.5 × (income − 5000).
return Math.max(0, maxMo * 12 - 0.5 * Math.max(0, i - 5000));
}
/* ---- Tax (2025 brackets; simplified credits) ---- */
function taxProvincial(taxable, prov, age, opts) {
var pb = provTable(prov);
if (!pb) return Math.max(0, taxable) * 0.05; // flat 5% surrogate — stamped in canada.js
var gross = bracketTax(taxable, pb.brackets);
var base = pb.brackets[0].rate;
var credit = 0;
var bpa = Math.max(0, (pb.bpa || 0) - 0.15 * Math.max(0, taxable - BPA_CLAW_PROV));
credit += bpa * base;
if (age >= 65 && pb.ageAmt) {
credit += Math.max(0, pb.ageAmt - 0.25 * Math.max(0, taxable - (pb.ageThr || 35000))) * base;
}
if (age >= 65 && opts && opts.pensionIncome > 0) credit += Math.min(opts.pensionIncome, PIA) * base;
var t = Math.max(0, gross - credit);
if (pb.surtax) {
for (var i = 0; i < pb.surtax.length; i++) {
var s = pb.surtax[i];
t += Math.max(0, gross - s.from) * s.rate;
}
}
return t;
}
// taxableIncome = T1 line-15000-style taxable income. age drives senior credits.
// opts: {pensionIncome: eligible pension/RRIF $ for PIA}
function tax(taxableIncome, province, age, opts) {
opts = opts || {};
taxableIncome = Math.max(0, taxableIncome || 0);
var ni = taxableIncome;
var fgross = bracketTax(taxableIncome, fedBrackets());
var fcr = 0;
var fbase = fedBrackets()[0].rate; // lowest federal rate (15% 2025, 14% 2026+)
var bpa = Math.max(0, fedBPA() - 0.15 * Math.max(0, ni - BPA_CLAW_FED));
fcr += bpa * fbase;
if (age >= 65) {
var ageAmt = Math.max(0, fedAgeAmount() - 0.15 * Math.max(0, ni - AGE_CLAW_START));
fcr += ageAmt * fbase;
if (opts.pensionIncome > 0) fcr += Math.min(opts.pensionIncome, PIA) * fbase;
}
var federal = Math.max(0, fgross - fcr);
if (province === 'QC') federal *= (1 - 0.165); // Quebec abatement (simplified: no QPP/YAP/RDAP)
var provincial = taxProvincial(taxableIncome, province, age, opts);
var r2 = function (x) { return Math.round(x * 100) / 100; };
return { federal: r2(federal), provincial: r2(provincial), total: r2(federal + provincial) };
}
function combinedTax(taxable, prov, age, opts) { // tax + clawback
return tax(taxable, prov, age, opts).total + clawback(taxable);
}
function marginalRate(taxable, prov, age, opts) {
var step = 2000;
return (combinedTax(taxable + step, prov, age, opts) - combinedTax(taxable, prov, age, opts)) / step;
}
/* ---- RRIF minimums: (115−age)/(120−age), 20% at 95+ ---- */
function rrifMinPct(age) {
if (age < 71) return 0;
if (age >= 95) return 20.0;
var t = rrifTable();
if (t && t[age] !== undefined) return t[age];
return Math.round((115 - age) / (120 - age) * 10000) / 100;
}
/* ---- Inputs defaults (stamped in UI) ---- */
var DEFAULTS = {
age: 45, province: 'ON', household: 'single', partner: null,
income: 60000, rrsp: 100000, tfsa: 30000, nonreg: 20000,
monthlyContribution: 500, employerMatchMonthly: 0,
retireAge: 65, spendingAnnual: 50000,
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) {
var o = {}, k;
for (k in DEFAULTS) o[k] = DEFAULTS[k];
for (k in (inp || {})) if (inp[k] !== undefined && inp[k] !== null) o[k] = inp[k];
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 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 };
if (a < p.retireAge) {
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.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;
years.push(row);
continue;
}
// post-retirement real growth on opening balances, then withdrawals
rrsp *= (1 + p.realReturnPost); tfsa *= (1 + p.realReturnPost); nonreg *= (1 + p.realReturnPost);
if (nestEgg === null && a === Math.floor(p.retireAge)) nestEgg = rrsp + tfsa + nonreg;
var rrspJan = rrsp;
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;
// Given RRSP/RRIF withdrawal W: waterfall RRSP → non-reg → TFSA, taxes & clawback, GIS.
function solve(W) {
W = Math.min(Math.max(W, 0), rrspJan);
var taxable1 = pre + W;
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');
var cash1 = pre + W + g - t1.total - c1;
var n = 0, t = 0, taxable = taxable1, tt = t1, cc = c1, opts = opts1;
var rem = need - cash1;
if (rem > 0 && nonreg > 0) {
var m = marginalRate(taxable1, p.province, a, { pensionIncome: 0 });
n = Math.min(nonreg, Math.max(0, rem / Math.max(0.2, 1 - 0.5 * m)));
taxable = taxable1 + 0.5 * n;
opts = { pensionIncome: opts1.pensionIncome };
tt = tax(taxable, p.province, a, opts);
cc = clawback(taxable);
g = gis(taxable - oasGross, 'single');
rem = need - (pre + W + g + n - tt.total - cc);
}
if (rem > 0 && tfsa > 0) {
t = Math.min(tfsa, rem);
g = gis(taxable - oasGross, 'single');
rem = need - (pre + W + g + n + t - tt.total - cc);
}
var cash = pre + W + g + n + t - tt.total - cc;
return { W: W, n: n, t: t, taxable: taxable, tax: tt, cw: cc, gis: g, cash: cash, rem: rem };
}
var r;
if (p.rrspWithdrawalOverrideAnnual > 0) {
r = solve(Math.min(Math.max(p.rrspWithdrawalOverrideAnnual, rrifMin), rrspJan));
} else {
var lo = Math.min(rrifMin, rrspJan), hi = Math.max(rrspJan, lo);
var flo = solve(lo), fhi = solve(hi);
if (fhi.cash <= need + 0.01) {
r = fhi; // everything spent; shortfall covered below
} else if (flo.cash >= need - 0.01) {
r = flo; // minimum withdrawal already covers need
} else {
for (var it = 0; it < 25; it++) {
var mid = (lo + hi) / 2;
var fm = solve(mid);
if (fm.cash < need) lo = mid; else hi = mid;
}
r = solve((lo + hi) / 2);
}
}
var surplus = Math.max(0, r.cash - need);
var unfunded = Math.max(0, need - r.cash);
rrsp = Math.max(0, rrspJan - r.W);
nonreg = Math.max(0, nonreg - r.n);
tfsa = Math.max(0, tfsa - r.t) + surplus; // surplus saved tax-free (TFSA proxy)
var totalBal = rrsp + tfsa + nonreg;
if (totalBal <= 0.5 && unfunded > 0.5 && depletionAge === null) depletionAge = a;
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;
row.rrspBal = rrsp; row.tfsaBal = tfsa; row.nonregBal = nonreg; row.totalBal = totalBal;
row.taxFederal = r.tax.federal; row.taxProvincial = r.tax.provincial;
years.push(row);
}
var last = years.length ? years[years.length - 1] : null;
return {
years: years, nestEgg: nestEgg === null ? (last ? last.totalBal : 0) : nestEgg,
depletionAge: depletionAge, totalTax: totalTax, totalClawback: totalClawback,
finalBal: last ? last.totalBal : 0
};
}
/* ---- Couple: per-person runs, household spending split 50/50, merge ---- */
function projectCouple(p) {
var me = fillDefaults(Object.assign({}, p, { household: 'single', partner: null, spendingAnnual: p.spendingAnnual * 0.5 }));
var pt = p.partner || {};
var partnerInp = fillDefaults(Object.assign({}, p, {
household: 'single', partner: null,
age: pt.age !== undefined ? pt.age : p.age,
income: pt.income || 0, rrsp: pt.rrsp || 0, tfsa: pt.tfsa || 0, nonreg: pt.nonreg || 0,
monthlyContribution: pt.monthlyContribution || 0, employerMatchMonthly: 0,
spendingAnnual: p.spendingAnnual * 0.5
}));
var A = projectSingle(me), B = projectSingle(partnerInp);
function byYear(res) { var m = {}; res.years.forEach(function (y) { m[y.age] = y; }); return m; }
var ya = byYear(A), yb = byYear(B);
var start = Math.min(p.age, partnerInp.age), end = Math.ceil(p.lifeExp);
var merged = [], totalTax = 0, totalClawback = 0, depletionAge = null;
for (var yr = start; yr < end; yr++) {
var ra = ya[yr], rb = yb[yr];
if (!ra && !rb) continue;
ra = ra || { cpp: 0, oasGross: 0, gis: 0, other: 0, rrifWithdrawal: 0, nonregW: 0, tfsaW: 0, taxable: 0, tax: 0, oasClawback: 0, cash: 0, need: 0, unfunded: 0, rrspBal: 0, tfsaBal: 0, nonregBal: 0, totalBal: 0, phase: 'none' };
rb = rb || { cpp: 0, oasGross: 0, gis: 0, other: 0, rrifWithdrawal: 0, nonregW: 0, tfsaW: 0, taxable: 0, tax: 0, oasClawback: 0, cash: 0, need: 0, unfunded: 0, rrspBal: 0, tfsaBal: 0, nonregBal: 0, totalBal: 0, phase: 'none' };
var row = {
age: yr, phase: (ra.phase === 'draw' || rb.phase === 'draw') ? 'draw' : 'accum',
cpp: ra.cpp + rb.cpp, oasGross: ra.oasGross + rb.oasGross, other: ra.other + rb.other,
rrifWithdrawal: ra.rrifWithdrawal + rb.rrifWithdrawal, nonregW: ra.nonregW + rb.nonregW, tfsaW: ra.tfsaW + rb.tfsaW,
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 (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 });
var tB = tax(rb.taxable + shift, p.province, yr, { pensionIncome: pb2 + shift });
var newTax = tA.total + tB.total, newClaw = clawback(ra.taxable - shift) + clawback(rb.taxable + shift);
if (newTax + newClaw < taxSum + clawSum) { taxSum = newTax; clawSum = newClaw; }
}
}
row.tax = taxSum; row.oasClawback = clawSum;
// GIS: household rate when both 65+, else sum of singles
var incA = ra.taxable - ra.oasGross, incB = rb.taxable - rb.oasGross;
var gisSum;
if (ra.phase === 'draw' && rb.phase === 'draw' && ra.age >= 65 && rb.age >= 65) {
gisSum = gis(incA + incB, 'couple');
} else {
gisSum = (ra.phase === 'draw' ? gis(incA, 'single') : 0) + (rb.phase === 'draw' ? gis(incB, 'single') : 0);
}
row.gis = gisSum;
var cash = ra.cash + rb.cash + (ra.gis + rb.gis ? gisSum - ra.gis - rb.gis : 0) + (taxSum ? (ra.tax + rb.tax - taxSum) : 0);
cash = ra.cash + rb.cash - (ra.gis + rb.gis) + gisSum + (ra.tax + rb.tax) - taxSum;
row.cash = Math.min(cash, row.need);
row.unfunded = Math.max(0, row.need - cash);
if (row.phase === 'draw' && row.totalBal <= 0.5 && row.unfunded > 0.5 && depletionAge === null) depletionAge = yr;
totalTax += taxSum; totalClawback += clawSum;
merged.push(row);
}
return {
years: merged, nestEgg: A.nestEgg + B.nestEgg, depletionAge: depletionAge,
totalTax: totalTax, totalClawback: totalClawback,
finalBal: merged.length ? merged[merged.length - 1].totalBal : 0
};
}
function project(inputs) {
var p = fillDefaults(inputs);
if (p.household === 'couple' && p.partner) return projectCouple(p);
return projectSingle(p);
}
/* ---- Sustainable spending: bisection on spending that survives to lifeExp ---- */
function sustainableSpending(inputs) {
var lo = 4000, hi = 400000;
var survives = function (s) {
return project(Object.assign({}, inputs, { spendingAnnual: s })).depletionAge === null;
};
if (!survives(lo)) return { annual: 0, monthly: 0 };
if (survives(hi)) return { annual: hi, monthly: hi / 12 };
for (var i = 0; i < 30; i++) {
var mid = (lo + hi) / 2;
if (survives(mid)) lo = mid; else hi = mid;
}
lo = Math.floor(lo / 100) * 100;
return { annual: lo, monthly: lo / 12 };
}
/* ---- Three gap closers ---- */
function recommendations(inputs, projection) {
var p = fillDefaults(inputs);
var proj = projection || project(p);
var s = sustainableSpending(p).annual;
var gap = p.spendingAnnual - s;
var out = { extraMonthly: 0, retireDelayYears: 0, spendingReduction: 0, gapAnnual: Math.max(0, Math.round(gap)) };
if (gap <= 100) { out.gapAnnual = 0; return out; }
out.spendingReduction = Math.round(gap / 100) * 100;
// (a) extra monthly contribution that closes the gap
var lo = 0, hi = 30000;
var closes = function (c) {
return sustainableSpending(Object.assign({}, p, {
monthlyContribution: (p.monthlyContribution || 0) + c
})).annual >= p.spendingAnnual - 100;
};
if (closes(hi)) {
for (var i = 0; i < 20; i++) {
var mid = (lo + hi) / 2;
if (closes(mid)) hi = mid; else lo = mid;
}
out.extraMonthly = Math.ceil(hi / 10) * 10;
} else out.extraMonthly = -1; // not closable by saving alone below $30k/mo
// (b) retirement delay
for (var d = 1; d <= 10; d++) {
if (sustainableSpending(Object.assign({}, p, { retireAge: Math.min(p.retireAge + d, 75) })).annual >= p.spendingAnnual - 100) {
out.retireDelayYears = d; break;
}
}
return out;
}
return {
estimateCppMonthlyAt65: estimateCppMonthlyAt65,
cppMonthlyAt: cppMonthlyAt,
oasMonthlyAt: oasMonthlyAt,
tax: tax,
clawback: clawback,
gis: gis,
rrifMinPct: rrifMinPct,
marginalRate: marginalRate,
project: project,
sustainableSpending: sustainableSpending,
recommendations: recommendations,
pensionStartAge: pensionStartAge,
pensionBridgeAnnual: pensionBridgeAnnual,
defaults: DEFAULTS,
constants: C
};
});