From 448c3258f0b0c9e4f3addb8faf8afd2317330b7b Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Thu, 22 Jan 2026 23:30:19 -0500 Subject: [PATCH 1/8] initial --- src/App.tsx | 9 +- src/components/features/WhatIfAnalysis.tsx | 329 +++++++++++++++++++++ src/services/calculationEngine.ts | 26 +- src/types/core.ts | 2 + 4 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 src/components/features/WhatIfAnalysis.tsx diff --git a/src/App.tsx b/src/App.tsx index 0b5af52..a988c40 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,12 +7,13 @@ import AccumulationStrategy from './components/features/AccumulationStrategy'; import TaxReference from './components/features/TaxReference'; import FireAnalysis from './components/features/FireAnalysis'; import { calculateStrategy, calculateLongevity } from './services/calculationEngine'; -import { TrendingUp, Calculator, AlertTriangle, BookOpen, Sun, Moon, PiggyBank, Settings, Flame } from 'lucide-react'; +import { TrendingUp, Calculator, AlertTriangle, BookOpen, Sun, Moon, PiggyBank, Settings, Flame, RefreshCw } from 'lucide-react'; import Footer from './components/layout/Footer'; import WizardModal from './components/features/wizard/WizardModal'; import SettingsModal from './components/features/SettingsModal'; import { db } from './services/db'; import { projectAssets } from './services/projection'; +import WhatIfAnalysis from './components/features/WhatIfAnalysis'; const INITIAL_PROFILE: UserProfile = { age: 65, // Retirement Start Age @@ -35,7 +36,7 @@ const App: React.FC = () => { const [profile, setProfile] = useState(INITIAL_PROFILE); const [strategyResult, setStrategyResult] = useState(null); const [longevityResult, setLongevityResult] = useState(null); - const [activeTab, setActiveTab] = useState<'withdrawal' | 'accumulation' | 'longevity' | 'reference' | 'fire'>('accumulation'); + const [activeTab, setActiveTab] = useState<'withdrawal' | 'accumulation' | 'longevity' | 'reference' | 'fire' | 'scenarios'>('accumulation'); const [isDarkMode, setIsDarkMode] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); @@ -254,6 +255,7 @@ const App: React.FC = () => { { id: 'fire', icon: Flame, label: 'FIRE Analysis' }, { id: 'withdrawal', icon: Calculator, label: 'Withdrawal' }, { id: 'longevity', icon: TrendingUp, label: 'Longevity' }, + { id: 'scenarios', icon: RefreshCw, label: 'Scenarios' }, { id: 'reference', icon: BookOpen, label: 'Reference' } ].map(tab => ( + {isOpen && ( +
+

This tool projects your portfolio growth and withdrawal strategies to compare the long-term impact of different accumulation choices.

+ +
+
+

Total Tax Paid

+

The sum of all estimated federal taxes paid during retirement.

+
    +
  • Traditional Withdrawals: Taxed as Ordinary Income
  • +
  • Brokerage Withdrawals: Taxed as Capital Gains (assumes 50% gain, long-term rates)
  • +
  • Roth/HSA Withdrawals: Tax-Free
  • +
+
+
+

Ending Balance (Age 100)

+

The projected total portfolio value specifically at Age 100.

+
    +
  • If portfolio depletes before 100, this is $0.
  • +
  • Adjusted for inflation to show "Present Value" (Today's buying power) for realistic comparison.
  • +
+
+
+
+ )} + + ); + }; + + return ( +
+ + + + {/* Header / Summary */} +
+
+

+ + What-If Scenarios +

+ +
+

+ Adjust your savings strategy to see how prioritizing different accounts affects your **Total Lifetime Taxes** and **Ending Wealth**. +

+ +
+ {/* Comparison Stats */} +
+
+
Total Tax Paid
+
{formatCurrency(currentResult.totalTaxPaid)}
+ +
+ + + {formatCurrency(scenarioResult.totalTaxPaid)} + +
+
+ {scenarioResult.totalTaxPaid < currentResult.totalTaxPaid + ? `Save ${formatCurrency(currentResult.totalTaxPaid - scenarioResult.totalTaxPaid)}` + : `Pay ${formatCurrency(scenarioResult.totalTaxPaid - currentResult.totalTaxPaid)} more`} +
+
+ +
+
Ending Balance (Age 100)
+
{formatCurrency(currentResult.endingBalance)}
+
+ ≈ {formatCurrency(currentResult.endingBalancePV)} (Today's Value) +
+ +
+ + currentResult.endingBalance ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(scenarioResult.endingBalance)} + +
+
+ {scenarioResult.endingBalance > currentResult.endingBalance + ? `Gain ${formatCurrency(scenarioResult.endingBalance - currentResult.endingBalance)}` + : `Lose ${formatCurrency(currentResult.endingBalance - scenarioResult.endingBalance)}`} +
+
+
+ + {/* Chart */} +
+ + + + + formatCurrency(value)} + contentStyle={{ backgroundColor: isDarkMode ? '#1e293b' : '#ffffff', borderColor: isDarkMode ? '#334155' : '#e2e8f0', borderRadius: '8px', color: isDarkMode ? '#f1f5f9' : '#0f172a' }} + itemStyle={{ color: isDarkMode ? '#f1f5f9' : '#0f172a' }} + /> + + + + + +
+
+
+ + {/* Controls */} +
+

Adjust Scenario Contributions

+
+
+ +
+ $ + handleContributionChange('traditionalIRA', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ +
+ $ + handleContributionChange('rothIRA', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ +
+ $ + handleContributionChange('brokerage', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ +
+ $ + handleContributionChange('hsa', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ + {/* Total Annual Savings Check */} +
+ Total Annual Savings: + + {formatCurrency(scenarioContributions.traditionalIRA + scenarioContributions.rothIRA + scenarioContributions.brokerage + scenarioContributions.hsa)} + +
+
+
+ ); +}; + +export default WhatIfAnalysis; diff --git a/src/services/calculationEngine.ts b/src/services/calculationEngine.ts index c401792..2e07359 100644 --- a/src/services/calculationEngine.ts +++ b/src/services/calculationEngine.ts @@ -523,6 +523,28 @@ export const calculateLongevity = (profile: UserProfile, strategy: StrategyResul const totalAssets = currentBrokerage + currentTrad + currentRoth + currentHSA; + // 4b. Calculate Estimated Tax for this Year + const ordIncome = basePension + fromTrad; // Trad Withdrawals are ordinary income + const qDivRatio = profile.income.qualifiedDividendRatio || 0.9; + const ordDividends = currentDividends * (1 - qDivRatio); + const qualDividends = currentDividends * qDivRatio; + + // Brokerage Withdrawals: Simplified assumption (50% is gain). + // In a real app, we'd track basis depletion. + const brokerageGain = fromBrokerage * 0.5; + + const totalOrdinaryForTax = ordIncome + ordDividends; + const totalCapGainsForTax = qualDividends + brokerageGain; + + const taxableSS = calculateTaxableSocialSecurity(currentSS, totalOrdinaryForTax, profile.filingStatus); + const stdDeduction = STANDARD_DEDUCTION[profile.filingStatus] + + (age >= 65 ? AGE_DEDUCTION[profile.filingStatus] * (profile.filingStatus === FilingStatus.MarriedJoint ? 2 : 1) : 0); + + const estimatedTax = calculateFederalTax(totalOrdinaryForTax + taxableSS, totalCapGainsForTax, profile.filingStatus, stdDeduction); + const totalCashFlow = fromBrokerage + fromTrad + fromRoth + fromHSA + totalFixedIncome; + const effectiveTaxRate = totalCashFlow > 0 ? estimatedTax / totalCashFlow : 0; + + projection.push({ age, year: i, @@ -547,7 +569,9 @@ export const calculateLongevity = (profile: UserProfile, strategy: StrategyResul withdrawalTradSEPP: fromTradSEPP, withdrawalTradPenalty: fromTradPenalty, earlyWithdrawalPenalty: penaltyAmount, - isDepleted: totalAssets <= 0 + isDepleted: totalAssets <= 0, + estimatedTax, + effectiveTaxRate }); if (totalAssets <= 0 && !depletionAge) depletionAge = age; diff --git a/src/types/core.ts b/src/types/core.ts index 671eb25..b88e8a6 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -102,6 +102,8 @@ export interface YearProjection { withdrawalTradPenalty: number; // Penalized withdrawal amount earlyWithdrawalPenalty: number; // 10% penalty amount isDepleted: boolean; + estimatedTax: number; // New: Estimated federal tax for this year + effectiveTaxRate: number; // New: Effective tax rate } export interface LongevityResult { From 123007e78a7b43bcf0510f4e20f565defb5e6418 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 19:37:11 -0500 Subject: [PATCH 2/8] feat: display retirement start assets in both current and scenario what-if analysis results. --- src/components/features/WhatIfAnalysis.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/features/WhatIfAnalysis.tsx b/src/components/features/WhatIfAnalysis.tsx index 10d2573..f8c9ddf 100644 --- a/src/components/features/WhatIfAnalysis.tsx +++ b/src/components/features/WhatIfAnalysis.tsx @@ -221,6 +221,9 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode }) ? `Save ${formatCurrency(currentResult.totalTaxPaid - scenarioResult.totalTaxPaid)}` : `Pay ${formatCurrency(scenarioResult.totalTaxPaid - currentResult.totalTaxPaid)} more`} +
+ Ret. Start Assets: {formatCurrency(currentResult.projectionStartAssets)} +
@@ -241,6 +244,9 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode }) ? `Gain ${formatCurrency(scenarioResult.endingBalance - currentResult.endingBalance)}` : `Lose ${formatCurrency(currentResult.endingBalance - scenarioResult.endingBalance)}`}
+
+ Ret. Start Assets: {formatCurrency(scenarioResult.projectionStartAssets)} +
From d3ae545c913e29c2cf74683f5691845013f0037d Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 19:39:29 -0500 Subject: [PATCH 3/8] feat: Update What-If Analysis chart to display a year-by-year comparison of estimated annual taxes between current and what-if scenarios. --- src/components/features/WhatIfAnalysis.tsx | 78 +++++++++++++++------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/components/features/WhatIfAnalysis.tsx b/src/components/features/WhatIfAnalysis.tsx index f8c9ddf..6d4455c 100644 --- a/src/components/features/WhatIfAnalysis.tsx +++ b/src/components/features/WhatIfAnalysis.tsx @@ -3,7 +3,7 @@ import { UserProfile, Contributions, FilingStatus } from '../../types'; import { projectAssets } from '../../services/projection'; import { calculateStrategy, calculateLongevity } from '../../services/calculationEngine'; import { TrendingUp, DollarSign, ArrowRight, RefreshCw, AlertCircle } from 'lucide-react'; -import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell, CartesianGrid } from 'recharts'; interface WhatIfAnalysisProps { profile: UserProfile; @@ -102,25 +102,40 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode }) // Diagnostic Assets const projectionStartAssets = retirementProfile.assets.traditionalIRA + retirementProfile.assets.rothIRA + retirementProfile.assets.brokerage + retirementProfile.assets.hsa; - return { totalTaxPaid, endingBalance, endingBalancePV, depletionAge, projectionStartAssets }; + return { + totalTaxPaid, + endingBalance, + endingBalancePV, + depletionAge, + projectionStartAssets, + projection: longevity.projection + }; }; const currentResult = useMemo(() => runSimulation(profile.contributions), [profile]); const scenarioResult = useMemo(() => runSimulation(scenarioContributions), [profile, scenarioContributions]); // --- Comparison Data --- - const comparisonData = [ - { - name: 'Current Plan', - Tax: currentResult.totalTaxPaid, - Legacy: currentResult.endingBalance, - }, - { - name: 'What-If Scenario', - Tax: scenarioResult.totalTaxPaid, - Legacy: scenarioResult.endingBalance, - }, - ]; + const mergedProjection = useMemo(() => { + const currentData = currentResult.projection || []; + const scenarioData = scenarioResult.projection || []; + + // Find common age range + const ages = Array.from(new Set([ + ...currentData.map(p => p.age), + ...scenarioData.map(p => p.age) + ])).sort((a, b) => a - b); + + return ages.map(age => { + const currentYear = currentData.find(p => p.age === age); + const scenarioYear = scenarioData.find(p => p.age === age); + return { + age, + currentTax: currentYear?.estimatedTax || 0, + scenarioTax: scenarioYear?.estimatedTax || 0 + }; + }); + }, [currentResult.projection, scenarioResult.projection]); const formatCurrency = (val: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(val); @@ -250,20 +265,35 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode }) - {/* Chart */} -
+ {/* Chart: Annual Tax Comparison */} +
+
+ Current Plan Tax + What-If Tax +
- - - + + + + `$${(val / 1000).toFixed(0)}k`} + tick={{ fill: isDarkMode ? '#94a3b8' : '#475569', fontSize: 10 }} + axisLine={false} + tickLine={false} + width={40} + /> formatCurrency(value)} - contentStyle={{ backgroundColor: isDarkMode ? '#1e293b' : '#ffffff', borderColor: isDarkMode ? '#334155' : '#e2e8f0', borderRadius: '8px', color: isDarkMode ? '#f1f5f9' : '#0f172a' }} - itemStyle={{ color: isDarkMode ? '#f1f5f9' : '#0f172a' }} + contentStyle={{ backgroundColor: isDarkMode ? '#1e293b' : '#ffffff', borderColor: isDarkMode ? '#334155' : '#e2e8f0', borderRadius: '8px', fontSize: '11px' }} + itemStyle={{ padding: '0' }} /> - - - + +
From c45044ce9f58f33ec7e8c03d6e0b5b88b8ae8c7b Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 19:45:49 -0500 Subject: [PATCH 4/8] feat: Extend What-If Analysis chart to compare asset balances alongside tax projections using a composed chart with dual Y-axes. --- src/components/features/WhatIfAnalysis.tsx | 50 +++++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/components/features/WhatIfAnalysis.tsx b/src/components/features/WhatIfAnalysis.tsx index 6d4455c..8154782 100644 --- a/src/components/features/WhatIfAnalysis.tsx +++ b/src/components/features/WhatIfAnalysis.tsx @@ -3,7 +3,7 @@ import { UserProfile, Contributions, FilingStatus } from '../../types'; import { projectAssets } from '../../services/projection'; import { calculateStrategy, calculateLongevity } from '../../services/calculationEngine'; import { TrendingUp, DollarSign, ArrowRight, RefreshCw, AlertCircle } from 'lucide-react'; -import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell, CartesianGrid } from 'recharts'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell, CartesianGrid, ComposedChart, Line } from 'recharts'; interface WhatIfAnalysisProps { profile: UserProfile; @@ -132,7 +132,9 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode }) return { age, currentTax: currentYear?.estimatedTax || 0, - scenarioTax: scenarioYear?.estimatedTax || 0 + scenarioTax: scenarioYear?.estimatedTax || 0, + currentAssets: currentYear?.totalAssets || 0, + scenarioAssets: scenarioYear?.totalAssets || 0 }; }); }, [currentResult.projection, scenarioResult.projection]); @@ -265,36 +267,62 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode })
- {/* Chart: Annual Tax Comparison */} + {/* Chart: Annual Tax & Balance Comparison */}
-
- Current Plan Tax +
+ Current Tax + Current Assets What-If Tax + What-If Assets
- + + {/* Left Y-Axis: Taxes */} `$${(val / 1000).toFixed(0)}k`} tick={{ fill: isDarkMode ? '#94a3b8' : '#475569', fontSize: 10 }} axisLine={false} tickLine={false} - width={40} + width={35} + /> + {/* Right Y-Axis: Assets */} + { + if (val >= 1000000) return `$${(val / 1000000).toFixed(1)}M`; + if (val >= 1000) return `$${(val / 1000).toFixed(0)}k`; + return `$${val}`; + }} + tick={{ fill: isDarkMode ? '#94a3b8' : '#475569', fontSize: 10 }} + axisLine={false} + tickLine={false} + width={45} /> formatCurrency(value)} + formatter={(value: number, name: string) => { + const formatted = formatCurrency(value); + if (name.includes('Assets')) return [formatted, name]; + return [formatted, name]; + }} contentStyle={{ backgroundColor: isDarkMode ? '#1e293b' : '#ffffff', borderColor: isDarkMode ? '#334155' : '#e2e8f0', borderRadius: '8px', fontSize: '11px' }} itemStyle={{ padding: '0' }} /> - - - + + + + + +
From 18550d5cc10ff68dcb77575b148ad7f67a9cd0a2 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 19:59:53 -0500 Subject: [PATCH 5/8] feat: enhance What-If Analysis chart with a custom detailed tooltip, updated bar and line styling, and an area fill for scenario assets. --- src/components/features/WhatIfAnalysis.tsx | 63 +++++++++++++++++----- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/components/features/WhatIfAnalysis.tsx b/src/components/features/WhatIfAnalysis.tsx index 8154782..64cc5c0 100644 --- a/src/components/features/WhatIfAnalysis.tsx +++ b/src/components/features/WhatIfAnalysis.tsx @@ -3,7 +3,7 @@ import { UserProfile, Contributions, FilingStatus } from '../../types'; import { projectAssets } from '../../services/projection'; import { calculateStrategy, calculateLongevity } from '../../services/calculationEngine'; import { TrendingUp, DollarSign, ArrowRight, RefreshCw, AlertCircle } from 'lucide-react'; -import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell, CartesianGrid, ComposedChart, Line } from 'recharts'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell, CartesianGrid, ComposedChart, Line, Area } from 'recharts'; interface WhatIfAnalysisProps { profile: UserProfile; @@ -187,7 +187,7 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode })

The projected total portfolio value specifically at Age 100.

  • If portfolio depletes before 100, this is $0.
  • -
  • Adjusted for inflation to show "Present Value" (Today's buying power) for realistic comparison.
  • +
  • Adjusted for inflation to show \"Present Value\" (Today's buying power) for realistic comparison.
@@ -309,19 +309,58 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode }) width={45} /> { - const formatted = formatCurrency(value); - if (name.includes('Assets')) return [formatted, name]; - return [formatted, name]; + content={({ active, payload, label }) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

Age {label}

+
+
+
+
+

Current Plan

+
+
+
+ Assets: + {formatCurrency(data.currentAssets)} +
+
+ Annual Tax: + {formatCurrency(data.currentTax)} +
+
+
+
+
+
+

What-If Scenario

+
+
+
+ Assets: + {formatCurrency(data.scenarioAssets)} +
+
+ Annual Tax: + {formatCurrency(data.scenarioTax)} +
+
+
+
+
+ ); + } + return null; }} - contentStyle={{ backgroundColor: isDarkMode ? '#1e293b' : '#ffffff', borderColor: isDarkMode ? '#334155' : '#e2e8f0', borderRadius: '8px', fontSize: '11px' }} - itemStyle={{ padding: '0' }} /> - - + + - - + + + From 596e13ab67def91dfeb5b4238ffeaac84b253f6e Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 20:23:24 -0500 Subject: [PATCH 6/8] feat: update brokerage withdrawal capital gains assumption from 50% to 100% for calculation and UI description. --- src/components/features/WhatIfAnalysis.tsx | 2 +- src/services/calculationEngine.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/features/WhatIfAnalysis.tsx b/src/components/features/WhatIfAnalysis.tsx index 64cc5c0..ed4d02b 100644 --- a/src/components/features/WhatIfAnalysis.tsx +++ b/src/components/features/WhatIfAnalysis.tsx @@ -178,7 +178,7 @@ const WhatIfAnalysis: React.FC = ({ profile, isDarkMode })

The sum of all estimated federal taxes paid during retirement.

  • Traditional Withdrawals: Taxed as Ordinary Income
  • -
  • Brokerage Withdrawals: Taxed as Capital Gains (assumes 50% gain, long-term rates)
  • +
  • Brokerage Withdrawals: Taxed as Capital Gains (assumes 100% is gain/growth, long-term rates)
  • Roth/HSA Withdrawals: Tax-Free
diff --git a/src/services/calculationEngine.ts b/src/services/calculationEngine.ts index 2e07359..64b9063 100644 --- a/src/services/calculationEngine.ts +++ b/src/services/calculationEngine.ts @@ -292,7 +292,7 @@ export const calculateStrategy = (profile: UserProfile): StrategyResult => { // Add to Plan const taxableAmt = (step.taxType === 'None') ? 0 : - (step.taxType === 'CapitalGains' ? pull * 0.5 : pull); // Simplifed 50% gain ratio + (step.taxType === 'CapitalGains' ? pull * 1.0 : pull); // Assumes 100% gain ratio (LTCG) withdrawalPlan.push({ source: step.source, @@ -529,9 +529,8 @@ export const calculateLongevity = (profile: UserProfile, strategy: StrategyResul const ordDividends = currentDividends * (1 - qDivRatio); const qualDividends = currentDividends * qDivRatio; - // Brokerage Withdrawals: Simplified assumption (50% is gain). - // In a real app, we'd track basis depletion. - const brokerageGain = fromBrokerage * 0.5; + // Brokerage Withdrawals: Assumes 100% is gain (most conservative for long-term savings) + const brokerageGain = fromBrokerage * 1.0; const totalOrdinaryForTax = ordIncome + ordDividends; const totalCapGainsForTax = qualDividends + brokerageGain; From 5e26be2c84be02842fb5c1a732ab1e813a0c7025 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 21:06:44 -0500 Subject: [PATCH 7/8] fix/ Refine Rule of 55 withdrawal strategy to prioritize filling lower tax brackets and adjust the order of withdrawal sources. --- src/services/calculationEngine.ts | 42 ++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/services/calculationEngine.ts b/src/services/calculationEngine.ts index 64b9063..c271ac6 100644 --- a/src/services/calculationEngine.ts +++ b/src/services/calculationEngine.ts @@ -99,20 +99,44 @@ export const getWithdrawalOrder = ( } } - // 2. Taxable Brokerage (Capital Gains) - Always accessible + // 2. Rule of 55 - If age >= 55 (fills low brackets first, like standard retirement) + if (age >= 55) { + const brackets = TAX_BRACKETS[filingStatus]; + const top12Bracket = brackets.find(b => b.rate === 0.12)?.limit || 50400; + const optimalTradWithdrawal = Math.min( + assets.traditionalIRA, + stdDeductionNeed + top12Bracket + ); + + if (optimalTradWithdrawal > 0) { + order.push({ + source: 'Traditional IRA (Rule of 55)', + limit: optimalTradWithdrawal, + taxType: 'Ordinary', + penalty: false + }); + } + } + + // 3. Taxable Brokerage (Capital Gains) - Always accessible order.push({ source: 'Taxable Brokerage', limit: assets.brokerage, taxType: 'CapitalGains', penalty: false }); - // 3. Roth Contributions (Basis) - Always tax/penalty free + // 4. Roth Contributions (Basis) - Always tax/penalty free order.push({ source: 'Roth IRA (Basis)', limit: rothBasisAvailable, taxType: 'None', penalty: false }); - // 4. Rule of 55 - If age >= 55 and separated from employer + // 5. Additional Traditional IRA (Rule of 55) - If more needed beyond 12% bracket if (age >= 55) { - order.push({ - source: 'Traditional IRA (Rule of 55)', - limit: assets.traditionalIRA, - taxType: 'Ordinary', - penalty: false - }); + const brackets = TAX_BRACKETS[filingStatus]; + const top12Bracket = brackets.find(b => b.rate === 0.12)?.limit || 50400; + const alreadyIncluded = Math.min(assets.traditionalIRA, stdDeductionNeed + top12Bracket); + if (assets.traditionalIRA > alreadyIncluded) { + order.push({ + source: 'Traditional IRA (Rule of 55 - Additional)', + limit: assets.traditionalIRA - alreadyIncluded, + taxType: 'Ordinary', + penalty: false + }); + } } // 4. Traditional IRA (Penalty) - Last resort From 3b2f461f6e514bfc5f54ac18ed842a92714215c1 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Tue, 27 Jan 2026 21:14:29 -0500 Subject: [PATCH 8/8] feat: display conditional 'Reinvested Surplus' card and adapt grid column count accordingly. --- src/components/features/StrategyResults.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/features/StrategyResults.tsx b/src/components/features/StrategyResults.tsx index dd0d156..d3969a4 100644 --- a/src/components/features/StrategyResults.tsx +++ b/src/components/features/StrategyResults.tsx @@ -149,7 +149,7 @@ const StrategyResults: React.FC = ({ result, profile, isDa {/* High Level Summary Cards */} -
+
100 ? 'lg:grid-cols-5' : 'lg:grid-cols-4'} gap-4`}>

Spending (Net)

{formatCurrency(result.nominalSpendingNeeded)}

@@ -168,6 +168,23 @@ const StrategyResults: React.FC = ({ result, profile, isDa

Portfolio + Benefits Needed

+ {/* Calculated Surplus (e.g. from 72t or RMDs) */} + {result.totalWithdrawal - (result.nominalSpendingNeeded + result.estimatedFederalTax) > 100 && ( +
+
+

Reinvested Surplus

+
+ Mandatory rules (like 72t or RMDs) require withdrawing more than you need. This excess is automatically reinvested in your brokerage account. +
+ +
+

+ {formatCurrency(result.totalWithdrawal - (result.nominalSpendingNeeded + result.estimatedFederalTax))} +

+

Excess Mandatory Cash

+
+ )} +

Feasibility