From 626796eb8a3772f18c0ce9849e9f9a206b6a22b6 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Thu, 5 Mar 2026 19:46:02 -0500 Subject: [PATCH] NITT tax --- src/components/features/StrategyResults.tsx | 6 ++- src/components/features/TaxReference.tsx | 2 +- src/constants/index.ts | 9 +++++ src/services/calculationEngine.ts | 41 +++++++++++++++++++-- src/types/core.ts | 1 + 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/components/features/StrategyResults.tsx b/src/components/features/StrategyResults.tsx index de7dc50..698c27c 100644 --- a/src/components/features/StrategyResults.tsx +++ b/src/components/features/StrategyResults.tsx @@ -257,7 +257,7 @@ const StrategyResults: React.FC = ({ result, profile, isDa

Tax Calculation Breakdown

-
+
Standard Deduction

{formatCurrency(result.standardDeduction)}

@@ -274,6 +274,10 @@ const StrategyResults: React.FC = ({ result, profile, isDa Mandatory RMD

{formatCurrency(result.rmdAmount)}

+
+ NIIT (3.8%) +

{formatCurrency(result.niitAmount)}

+
diff --git a/src/components/features/TaxReference.tsx b/src/components/features/TaxReference.tsx index 98778de..8554eba 100644 --- a/src/components/features/TaxReference.tsx +++ b/src/components/features/TaxReference.tsx @@ -665,7 +665,7 @@ const TaxReference: React.FC = () => {

Limitations

  • • State taxes are not included
  • -
  • • Net Investment Income Tax (NIIT) not modeled
  • +
  • • Net Investment Income Tax (NIIT) modeled at 3.8% on investment income above MAGI thresholds
  • • Tax brackets are estimates and may change
  • • Does not account for itemized deductions
  • • Social Security COLA adjustments assumed 0%
  • diff --git a/src/constants/index.ts b/src/constants/index.ts index a297d6f..b8a4b7f 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -179,3 +179,12 @@ export const SINGLE_LIFE_EXPECTANCY_TABLE: Record = { 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, +}; diff --git a/src/services/calculationEngine.ts b/src/services/calculationEngine.ts index adb27fa..419da0f 100644 --- a/src/services/calculationEngine.ts +++ b/src/services/calculationEngine.ts @@ -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'; /** @@ -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. */ @@ -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) @@ -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; @@ -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, @@ -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 @@ -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; diff --git a/src/types/core.ts b/src/types/core.ts index 07f06d2..c58cc97 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -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