Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/features/StrategyResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ const StrategyResults: React.FC<StrategyResultsProps> = ({ result, profile, isDa
<ShieldAlert className="w-4 h-4 text-amber-500" />
<h4 className="text-sm font-bold">Tax Calculation Breakdown</h4>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 text-xs">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-4 text-xs">
<div className="p-2 bg-slate-50 dark:bg-slate-800 rounded">
<span className="text-slate-500">Standard Deduction</span>
<p className="font-bold text-slate-900 dark:text-white">{formatCurrency(result.standardDeduction)}</p>
Expand All @@ -274,6 +274,10 @@ const StrategyResults: React.FC<StrategyResultsProps> = ({ result, profile, isDa
<span className="text-slate-500">Mandatory RMD</span>
<p className="font-bold text-slate-900 dark:text-white">{formatCurrency(result.rmdAmount)}</p>
</div>
<div className="p-2 bg-slate-50 dark:bg-slate-800 rounded">
<span className="text-slate-500 flex items-center gap-1">NIIT (3.8%) <Tooltip content="Net Investment Income Tax: 3.8% surtax on investment income (dividends, capital gains) when MAGI exceeds $200K (Single) / $250K (MFJ). IRA distributions increase MAGI but are not subject to NIIT themselves." /></span>
<p className="font-bold text-slate-900 dark:text-white">{formatCurrency(result.niitAmount)}</p>
</div>
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion src/components/features/TaxReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ const TaxReference: React.FC = () => {
<h4 className="font-semibold text-slate-800 dark:text-white mb-2">Limitations</h4>
<ul className="text-sm text-slate-600 dark:text-slate-400 space-y-1">
<li>• State taxes are not included</li>
<li>• Net Investment Income Tax (NIIT) not modeled</li>
<li>• Net Investment Income Tax (NIIT) modeled at 3.8% on investment income above MAGI thresholds</li>
<li>• Tax brackets are estimates and may change</li>
<li>• Does not account for itemized deductions</li>
<li>• Social Security COLA adjustments assumed 0%</li>
Expand Down
9 changes: 9 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,12 @@ export const SINGLE_LIFE_EXPECTANCY_TABLE: Record<number, number> = {
70: 18.8, 71: 18.0, 72: 17.2, 73: 16.4, 74: 15.6,
75: 14.8,
};

// Net Investment Income Tax (NIIT) - IRC §1411
// 3.8% on the lesser of: net investment income OR MAGI exceeding threshold
// Thresholds are NOT indexed for inflation (static since 2013)
export const NIIT_RATE = 0.038;
export const NIIT_THRESHOLDS = {
[FilingStatus.Single]: 200000,
[FilingStatus.MarriedJoint]: 250000,
};
41 changes: 37 additions & 4 deletions src/services/calculationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { UserProfile, StrategyResult, WithdrawalSource, FilingStatus, LongevityR
import {
STANDARD_DEDUCTION, AGE_DEDUCTION, TAX_BRACKETS, CAP_GAINS_BRACKETS, UNIFORM_LIFETIME_TABLE, RMD_START_AGE,
SENIOR_DEDUCTION, SENIOR_DEDUCTION_PHASEOUT, SS_TAX_THRESHOLDS, IRMAA_THRESHOLDS, IRMAA_SAFETY_BUFFER,
FED_MIDTERM_RATE_120, SINGLE_LIFE_EXPECTANCY_TABLE // Imported
FED_MIDTERM_RATE_120, SINGLE_LIFE_EXPECTANCY_TABLE, // Imported
NIIT_RATE, NIIT_THRESHOLDS
} from '../constants';

/**
Expand All @@ -26,6 +27,24 @@ const calculateTaxableSocialSecurity = (ssAmount: number, otherIncome: number, f
);
};

/**
* Calculates Net Investment Income Tax (NIIT) - IRC §1411
* 3.8% on the LESSER of:
* (a) Net investment income (dividends, capital gains from brokerage)
* (b) MAGI exceeding threshold ($200K Single, $250K MFJ)
* IRA distributions count toward MAGI but NOT net investment income.
*/
const calculateNIIT = (
netInvestmentIncome: number,
magi: number,
filingStatus: FilingStatus
): number => {
const threshold = NIIT_THRESHOLDS[filingStatus];
const magiExcess = Math.max(0, magi - threshold);
if (magiExcess <= 0 || netInvestmentIncome <= 0) return 0;
return NIIT_RATE * Math.min(netInvestmentIncome, magiExcess);
};

/**
* Calculates Federal Tax using the 'Two-Layer Cake' approach.
*/
Expand Down Expand Up @@ -288,6 +307,7 @@ export const calculateStrategy = (profile: UserProfile): StrategyResult => {
let grossCash = totalAnnualSS + income.pension + brokerageDividends;
let ordIncomeForTax = income.pension + ordinaryDividends;
let capGainsForTax = qualifiedDividends;
let netInvestmentIncome = brokerageDividends; // Dividends are always NII; brokerage gains added below
let currentPenalty = 0;

// 0. RMDs (Always First)
Expand Down Expand Up @@ -356,7 +376,10 @@ export const calculateStrategy = (profile: UserProfile): StrategyResult => {
// Add to Cash / Tax / Penalty
grossCash += pull;
if (step.taxType === 'Ordinary') ordIncomeForTax += taxableAmt;
if (step.taxType === 'CapitalGains') capGainsForTax += taxableAmt;
if (step.taxType === 'CapitalGains') {
capGainsForTax += taxableAmt;
netInvestmentIncome += taxableAmt; // Brokerage gains are NII
}

if (step.penalty) {
currentPenalty += pull * 0.10;
Expand All @@ -370,7 +393,12 @@ export const calculateStrategy = (profile: UserProfile): StrategyResult => {
liquidityGapWarning = gap > 1 || currentPenalty > 0;

const finalTaxableSS = calculateTaxableSocialSecurity(totalAnnualSS, ordIncomeForTax, filingStatus);
const iterationTax = calculateFederalTax(ordIncomeForTax + finalTaxableSS, capGainsForTax, filingStatus, standardDeduction);
const iterationIncomeTax = calculateFederalTax(ordIncomeForTax + finalTaxableSS, capGainsForTax, filingStatus, standardDeduction);

// NIIT: MAGI = all income before standard deduction
const magi = ordIncomeForTax + finalTaxableSS + capGainsForTax;
const niitTax = calculateNIIT(netInvestmentIncome, magi, filingStatus);
const iterationTax = iterationIncomeTax + niitTax;

lastResult = {
totalWithdrawal: grossCash,
Expand All @@ -384,6 +412,7 @@ export const calculateStrategy = (profile: UserProfile): StrategyResult => {
taxableSocialSecurity: finalTaxableSS,
currentYearSocialSecurity: totalAnnualSS,
provisionalIncome: ordIncomeForTax + (0.5 * totalAnnualSS),
niitAmount: niitTax,
standardDeduction,
notes: currentPenalty > 0 ? [`Includes $${currentPenalty.toFixed(0)} early withdrawal penalty.`] : [],
nominalSpendingNeeded
Expand Down Expand Up @@ -601,7 +630,11 @@ export const calculateLongevity = (profile: UserProfile, strategy: StrategyResul
const stdDeduction = STANDARD_DEDUCTION[profile.filingStatus] +
calculateAgeDeduction(age, profile.filingStatus, spouseAgeThisYear);

const estimatedTax = calculateFederalTax(totalOrdinaryForTax + taxableSS, totalCapGainsForTax, profile.filingStatus, stdDeduction);
const incomeTax = calculateFederalTax(totalOrdinaryForTax + taxableSS, totalCapGainsForTax, profile.filingStatus, stdDeduction);
const longevityNII = currentDividends + brokerageGain;
const longevityMAGI = totalOrdinaryForTax + taxableSS + totalCapGainsForTax;
const niitTax = calculateNIIT(longevityNII, longevityMAGI, profile.filingStatus);
const estimatedTax = incomeTax + niitTax;
const totalCashFlow = fromBrokerage + fromTrad + fromRoth + fromHSA + totalFixedIncome;
const effectiveTaxRate = totalCashFlow > 0 ? estimatedTax / totalCashFlow : 0;

Expand Down
1 change: 1 addition & 0 deletions src/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface StrategyResult {
taxableSocialSecurity: number;
currentYearSocialSecurity: number;
provisionalIncome: number;
niitAmount: number; // Net Investment Income Tax (3.8% surtax)
standardDeduction: number;
notes: string[];
nominalSpendingNeeded: number; // Adjusted for inflation if needed
Expand Down