1/** 2 * What-if comparisons â the two questions advisors ask first: 3 * 1. Social Security claiming age: the same plan run at 62 / 67 / 70 (and the 4 * plan's own age), the benefit restated through the PIA (js/ss-claiming.js). 5 * 2. Scenarios: named snapshots of the whole input set ("retire at 63", 6 * "$60k spending"), run side by side against the current plan. 7 * 8 * Every row is a full simulation at the plan's own path count, through the 9 * same request builder the Run button uses. PRO feature (each comparison is 10 * three or more simulations). Results live on AppState.whatif so the client 11 * report can print them (pdf-export.js renderWhatIfPage). 12 * 13 * Classic script: relies on AppState, activeAPI, QC_SS, formatCurrency, 14 * showUpgradePopup being globals. Exposes window.QC_WHATIF. 15 */ 16(function () { 17 'use strict'; 18 19 const STORE = 'quantcalc_whatif_scenarios'; 20 const MAX_SCENARIOS = 6; 21 22 function whatifState() { 23 if (!AppState.whatif) AppState.whatif = { claiming: null, scenarios: null }; 24 return AppState.whatif; 25 } 26 27 /* ---------------- data ---------------- */ 28 29 function summarize(r) { 30 const rs = r && r.realised_spending; 31 return { 32 success_rate: r ? r.success_rate : null, 33 median_final: r ? r.median_final_value : null, 34 p10_final: r && r.percentiles ? r.percentiles.p10 : null, 35 spend_p50: rs && rs.p50 > 0 ? rs.p50 : null, 36 spend_p10: rs && rs.p50 > 0 ? rs.p10 : null, 37 num_simulations: r ? r.num_simulations : null 38 }; 39 } 40 41 /* Run the plan with some inputs overridden, through the real request 42 * builder, without leaving the override behind. */ 43 async function runVariant(overrides) { 44 const saved = AppState.inputs; 45 let request; 46 AppState.inputs = Object.assign({}, saved, overrides); 47 try { 48 request = AppState.getSimulationRequest(); 49 } finally { 50 AppState.inputs = saved; 51 } 52 // api.js writes AppState.taxResults (and results) on every simulate 53 // response; a what-if variant must not leave the LAST scenario's tax 54 // figures behind for the current plan's report (found 2026-09-08: the 55 // exported tax page showed the last variant's lifetime tax). 56 const savedTax = AppState.taxResults, savedResults = AppState.results; 57 let response; 58 try { 59 response = await activeAPI.simulate(request); 60 } finally { 61 AppState.taxResults = savedTax; 62 AppState.results = savedResults; 63 } 64 if (!response || !response.success || !response.results) throw new Error('Simulation failed'); 65 return summarize(response.results); 66 } 67 68 function gate() { 69 if (!AppState.isPro || !AppState.isPro()) { 70 showUpgradePopup({ context: 'What-if comparisons run several full simulations of your plan. PRO includes them.', source: 'whatif' }); 71 return false; 72 } 73 if (!AppState.results) { 74 if (typeof showError === 'function') showError('Run a simulation first, then compare.'); 75 return false; 76 } 77 return true; 78 } 79 80 async function compareClaiming(fra) { 81 if (!gate()) return null; 82 fra = Number(fra) || QC_SS.DEFAULT_FRA; 83 const i = AppState.inputs; 84 const ages = QC_SS.comparisonAges(i.ssStartAge); 85 const rows = []; 86 setStatus('claiming', 'Running ' + ages.length + ' simulationsâ¦'); 87 try { 88 for (let k = 0; k < ages.length; k++) { 89 const age = ages[k]; 90 const benefit = Math.round(QC_SS.benefitAtAge(i.socialSecurity, i.ssStartAge, age, fra)); 91 setStatus('claiming', 'Running ' + (k + 1) + ' of ' + ages.length + ' (claim at ' + age + ')â¦'); 92 const s = await runVariant({ ssStartAge: age, socialSecurity: benefit }); 93 rows.push(Object.assign({ age, benefit, isPlan: age === Math.round(i.ssStartAge) }, s)); 94 } 95 } catch (e) { 96 setStatus('claiming', 'Comparison stopped: ' + (e.message || e)); 97 return null; 98 } 99 whatifState().claiming = { fra, planAge: i.ssStartAge, planBenefit: i.socialSecurity, rows, ts: new Date().toISOString() }; 100 setStatus('claiming', ''); 101 renderClaiming(); 102 track('whatif_claiming', { rows: rows.length }); 103 return rows; 104 } 105
106 /* ---------------- scenarios ---------------- */ 107 108 function loadScenarios() { 109 try { 110 const raw = JSON.parse(localStorage.getItem(STORE) || '[]'); 111 return Array.isArray(raw) ? raw.filter((s) => s && s.name && s.inputs) : []; 112 } catch (e) { return []; } 113 } 114 115 function persist(list) { 116 try { localStorage.setItem(STORE, JSON.stringify(list)); } catch (e) { /* storage full */ } 117 } 118 119 function saveScenario(name) { 120 name = String(name || '').trim().slice(0, 40); 121 if (!name) return null; 122 const list = loadScenarios().filter((s) => s.name !== name); 123 // Snapshot only JSON-safe input values; lots and arrays copy by value. 124 const inputs = JSON.parse(JSON.stringify(AppState.inputs)); 125 list.push({ name, inputs, created: new Date().toISOString() }); 126 while (list.length > MAX_SCENARIOS) list.shift(); 127 persist(list); 128 renderScenarioChips(); 129 return list[list.length - 1]; 130 } 131 132 function removeScenario(name) { 133 persist(loadScenarios().filter((s) => s.name !== name)); 134 renderScenarioChips(); 135 } 136 137 function applyScenario(name) { 138 const sc = loadScenarios().find((s) => s.name === name); 139 if (!sc) return false; 140 Object.assign(AppState.inputs, sc.inputs); 141 if (typeof AppState.saveInputsToStorage === 'function') AppState.saveInputsToStorage(); 142 // Inputs bind to the DOM at init; a reload is the one reliable way to 143 // show every field from the snapshot. 144 window.location.reload(); 145 return true; 146 } 147 148 const RULE_SHORT = { 'guyton-klinger': 'guardrails', 'vpw': 'VPW', 'bucket': 'bucket' }; 149 150 function describe(inputs) { 151 const a = inputs || {}; 152 return 'retire ' + a.retirementAge + ' · $' + Number(a.annualSpending || 0).toLocaleString() 153 + '/yr · SS ' + a.ssStartAge + (RULE_SHORT[a.withdrawalStrategy] ? ' · ' + RULE_SHORT[a.withdrawalStrategy] : ''); 154 } 155 156 async function runScenarios() { 157 if (!gate()) return null; 158 const list = loadScenarios(); 159 if (!list.length) { 160 if (typeof showError === 'function') showError('Save a scenario first (change some inputs, then "Save as scenario").'); 161 return null; 162 } 163 const rows = [Object.assign({ name: 'Current plan', isCurrent: true, summary: describe(AppState.inputs) }, summarize(AppState.results))]; 164 try { 165 for (let k = 0; k < list.length; k++) { 166 setStatus('scenarios', 'Running ' + (k + 1) + ' of ' + list.length + ' (' + list[k].name + ')â¦'); 167 const s = await runVariant(list[k].inputs); 168 rows.push(Object.assign({ name: list[k].name, summary: describe(list[k].inputs) }, s)); 169 } 170 } catch (e) { 171 setStatus('scenarios', 'Comparison stopped: ' + (e.message || e)); 172 return null; 173 } 174 whatifState().scenarios = { rows, ts: new Date().toISOString() }; 175 setStatus('scenarios', ''); 176 renderScenarios(); 177 track('whatif_scenarios', { rows: rows.length }); 178 return rows; 179 } 180 181 /* ---------------- rendering ---------------- */ 182 183 function fmt$(v) { 184 if (v == null || !isFinite(v)) return 'â'; 185 return typeof formatCurrency === 'function' ? formatCurrency(v) : '$' + Math.round(v).toLocaleString(); 186 } 187 function pct(v) { return v == null ? 'â' : (v * 100).toFixed(1) + '%'; } 188 189 function table(headers, rows, highlightIdx) { 190 const t = document.createElement('table'); 191 t.className = 'whatif-table'; 192 const thead = document.createElement('thead'); 193 const hr = document.createElement('tr'); 194 headers.forEach((h) => { const th = document.createElement('th'); th.textContent = h; hr.appendChild(th); }); 195 thead.appendChild(hr); t.appendChild(thead); 196 const tb = document.createElement('tbody'); 197 rows.forEach((cells, idx) => { 198 const tr = document.createElement('tr'); 199 if (idx === highlightIdx) tr.className = 'is-plan';
200 cells.forEach((c) => { const td = document.createElement('td'); td.textContent = c; tr.appendChild(td); }); 201 tb.appendChild(tr); 202 }); 203 t.appendChild(tb); 204 return t; 205 } 206 207 function setStatus(which, text) { 208 const el = document.getElementById(which === 'claiming' ? 'whatifClaimingStatus' : 'whatifScenariosStatus'); 209 if (el) el.textContent = text; 210 } 211 212 function renderClaiming() { 213 const host = document.getElementById('whatifClaimingResult'); 214 const c = whatifState().claiming; 215 if (!host || !c) return; 216 host.innerHTML = ''; 217 const dyn = c.rows.some((r) => r.spend_p50 != null); 218 const headers = ['Claim at', 'Benefit (today\'s $/yr)', 'Survival', 'Median ending', '10th pct ending'].concat(dyn ? ['Median realised spending'] : []); 219 const rows = c.rows.map((r) => [String(r.age) + (r.isPlan ? ' (plan)' : ''), fmt$(r.benefit), pct(r.success_rate), fmt$(r.median_final), fmt$(r.p10_final)].concat(dyn ? [fmt$(r.spend_p50)] : [])); 220 host.appendChild(table(headers, rows, c.rows.findIndex((r) => r.isPlan))); 221 const note = document.createElement('p'); 222 note.className = 'whatif-note'; 223 note.textContent = 'Benefit restated through the primary insurance amount with SSA reduction and delayed-credit factors (full retirement age ' + c.fra + '). Each row is a full simulation of the same plan.'; 224 host.appendChild(note); 225 } 226
227 function renderScenarios() { 228 const host = document.getElementById('whatifScenariosResult'); 229 const s = whatifState().scenarios; 230 if (!host || !s) return; 231 host.innerHTML = ''; 232 const dyn = s.rows.some((r) => r.spend_p50 != null); 233 const headers = ['Scenario', 'Inputs', 'Survival', 'Median ending', '10th pct ending'].concat(dyn ? ['Median realised spending'] : []); 234 const rows = s.rows.map((r) => [r.name, r.summary, pct(r.success_rate), fmt$(r.median_final), fmt$(r.p10_final)].concat(dyn ? [fmt$(r.spend_p50)] : [])); 235 host.appendChild(table(headers, rows, 0)); 236 } 237 238 function renderScenarioChips() { 239 const host = document.getElementById('whatifScenarioList'); 240 if (!host) return; 241 host.innerHTML = ''; 242 const list = loadScenarios(); 243 if (!list.length) { 244 const p = document.createElement('span'); p.className = 'whatif-note'; 245 p.textContent = 'No saved scenarios yet. Change some inputs, then save them under a name.'; 246 host.appendChild(p); return; 247 } 248 list.forEach((sc) => { 249 const chip = document.createElement('span'); 250 chip.className = 'whatif-chip'; 251 const label = document.createElement('span'); label.textContent = sc.name; label.title = describe(sc.inputs); 252 const apply = document.createElement('button'); apply.type = 'button'; apply.textContent = 'load'; apply.title = 'Load this scenario into the form'; 253 apply.addEventListener('click', () => applyScenario(sc.name)); 254 const del = document.createElement('button'); del.type = 'button'; del.textContent = 'Ã'; del.title = 'Delete scenario'; 255 del.addEventListener('click', () => removeScenario(sc.name)); 256 chip.appendChild(label); chip.appendChild(apply); chip.appendChild(del); 257 host.appendChild(chip); 258 }); 259 } 260 261 function track(name, params) { 262 if (window.QuantCalcAnalytics && window.QuantCalcAnalytics.trackProUsage) { 263 window.QuantCalcAnalytics.trackProUsage(name, params || {}); 264 } 265 } 266 267 function init() { 268 const card = document.getElementById('whatifCard'); 269 if (!card) return; 270 const btnC = document.getElementById('whatifClaimingBtn'); 271 if (btnC) btnC.addEventListener('click', () => { 272 const fraSel = document.getElementById('whatifFra'); 273 compareClaiming(fraSel ? fraSel.value : QC_SS.DEFAULT_FRA); 274 }); 275 const btnS = document.getElementById('whatifSaveScenarioBtn'); 276 if (btnS) btnS.addEventListener('click', () => { 277 const nameEl = document.getElementById('whatifScenarioName'); 278 if (saveScenario(nameEl ? nameEl.value : '') && nameEl) nameEl.value = ''; 279 }); 280 const btnR = document.getElementById('whatifRunScenariosBtn'); 281 if (btnR) btnR.addEventListener('click', () => runScenarios()); 282 renderScenarioChips(); 283 if (!(AppState.isPro && AppState.isPro())) { 284 const lock = document.getElementById('whatifProChip'); 285 if (lock) lock.hidden = false; 286 } 287 } 288 289 window.QC_WHATIF = { init, compareClaiming, saveScenario, removeScenario, applyScenario, runScenarios, runVariant, summarize, loadScenarios, describe, renderClaiming, renderScenarios, renderScenarioChips }; 290 window.initWhatIf = init; 291})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.