diff --git a/changelog.d/fix-niit-magi-section-911-addback.fixed.md b/changelog.d/fix-niit-magi-section-911-addback.fixed.md new file mode 100644 index 00000000000..2d88afae01d --- /dev/null +++ b/changelog.d/fix-niit-magi-section-911-addback.fixed.md @@ -0,0 +1 @@ +Add the foreign earned income excluded under 26 U.S.C. 911(a)(1) to modified adjusted gross income for the net investment income tax, as 26 U.S.C. 1411(d) requires, through a new niit_magi_section_911_addition variable. It defaults to foreign_earned_income_exclusion; a filer with housing amounts or itemized deductions disallowed under section 911(d)(6) can enter the Form 8960 worksheet amount directly. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml index c8850912ea2..d4983683baa 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml @@ -339,3 +339,152 @@ niit_magi: 255_000 net_investment_income: 15_000 net_investment_income_tax: 190 + +- name: Section 911 exclusion lifts a single filer's MAGI over the threshold + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 100_000 + households: + household: + members: [person1] + state_code: TX + output: + # MAGI 200,000 + 100,000 = 300,000; excess over 200,000 is 100,000. + # 3.8% x min(50,000, 100,000) = 1,900. + niit_magi: 300_000 + net_investment_income: 50_000 + net_investment_income_tax: 1_900 + +- name: Section 911 exclusion that leaves MAGI below the threshold + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 50_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 90_000 + households: + household: + members: [person1] + state_code: TX + output: + # MAGI 100,000 + 90,000 = 190,000, below the 200,000 threshold. + niit_magi: 190_000 + net_investment_income_tax: 0 + +- name: Section 911 exclusion where the MAGI excess limits the tax base + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 120_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 60_000 + households: + household: + members: [person1] + state_code: TX + output: + # MAGI 170,000 + 60,000 = 230,000; 3.8% x min(50,000, 30,000) = 1,140. + niit_magi: 230_000 + net_investment_income_tax: 1_140 + +- name: Section 911 exclusion lifts a joint return's MAGI over the threshold + period: 2025 + input: + people: + head: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + qualified_dividend_income: 40_000 + spouse: + age: 45 + is_tax_unit_spouse: true + employment_income: 50_000 + tax_units: + tax_unit: + members: [head, spouse] + foreign_earned_income_exclusion: 60_000 + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + # AGI 240,000 is below the 250,000 joint threshold; MAGI 300,000 is + # 50,000 above it. 3.8% x min(40,000, 50,000) = 1,520. + filing_status: JOINT + adjusted_gross_income: 240_000 + niit_magi: 300_000 + net_investment_income_tax: 1_520 + +- name: Section 911 exclusion on a separate return uses the 125,000 threshold + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 100_000 + taxable_interest_income: 20_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SEPARATE + foreign_earned_income_exclusion: 30_000 + households: + household: + members: [person1] + state_code: TX + output: + # MAGI 120,000 + 30,000 = 150,000; 3.8% x min(20,000, 25,000) = 760. + niit_magi: 150_000 + net_investment_income_tax: 760 + +- name: An entered section 911 addition without housing amounts sets the tax + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 100_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + # Form 2555 line 45 is 100,000, of which 30,000 is the housing + # exclusion. Section 1411(d) adds back the other 70,000. + foreign_earned_income_exclusion: 100_000 + niit_magi_section_911_addition: 70_000 + households: + household: + members: [person1] + state_code: TX + output: + # MAGI 150,000 + 70,000 = 220,000; 3.8% x min(50,000, 20,000) = 760. + # The default (the full 100,000) would give 1,900. + niit_magi: 220_000 + net_investment_income_tax: 760 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/niit_magi.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/niit_magi.yaml index cf878ddd3ff..c34521f70aa 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/niit_magi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/niit_magi.yaml @@ -70,3 +70,122 @@ output: adjusted_gross_income: 160_000 niit_magi: 160_000 + +- name: The section 911(a)(1) exclusion raises MAGI by the excluded amount + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 100_000 + households: + household: + members: [person1] + state_code: TX + output: + # 26 U.S.C. 1411(d); Form 8960 Line 13 MAGI Worksheet: line 1 AGI + # 200,000 plus line 2c 100,000. + adjusted_gross_income: 200_000 + niit_magi_section_911_addition: 100_000 + niit_magi: 300_000 + +- name: The section 911 addition and positive code H both raise MAGI + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + estate_income: 10_000 + estate_income_net_investment_income_adjustment: 4_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 20_000 + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 160_000 + niit_magi: 160_000 + 4_000 + 20_000 + +- name: An entered worksheet amount replaces the default, leaving out housing amounts + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + # Form 2555 line 45 is 100,000: a 70,000 foreign earned income + # exclusion (line 42) and a 30,000 housing exclusion (line 36). + # Section 1411(d) adds back only the line 42 amount. + foreign_earned_income_exclusion: 100_000 + niit_magi_section_911_addition: 70_000 + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 200_000 + niit_magi: 270_000 + +- name: A negative entered addition leaves MAGI at AGI + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + tax_units: + tax_unit: + members: [person1] + niit_magi_section_911_addition: -5_000 + households: + household: + members: [person1] + state_code: TX + output: + # Section 1411(d) adds "the excess (if any)", never a negative amount. + niit_magi: 150_000 + +- name: An entered worksheet amount restores itemized deductions netted out of the exclusion + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + employment_income: 150_000 + qualified_dividend_income: 50_000 + tax_units: + tax_unit: + members: [person1] + # Foreign Earned Income Tax Worksheet line 2c: a 100,000 foreign + # earned income exclusion (Form 2555 lines 42 and 45; no housing + # amounts and nothing on line 44) less 5,000 of itemized deductions + # disallowed because they relate to it (line 2b). Section 1411(d)(2) + # subtracts only deductions taken into account in computing AGI, so + # the addition is 100,000. + foreign_earned_income_exclusion: 95_000 + niit_magi_section_911_addition: 100_000 + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 200_000 + niit_magi: 300_000 diff --git a/policyengine_us/tests/test_niit_section_911_magi.py b/policyengine_us/tests/test_niit_section_911_magi.py new file mode 100644 index 00000000000..a7b57b9ed65 --- /dev/null +++ b/policyengine_us/tests/test_niit_section_911_magi.py @@ -0,0 +1,393 @@ +"""Property and differential tests for the section 911 addition to NIIT MAGI. + +26 U.S.C. 1411(d): "the term 'modified adjusted gross income' means adjusted +gross income increased by the excess of— (1) the amount excluded from gross +income under section 911(a)(1), over (2) the amount of any deductions (taken +into account in computing adjusted gross income) or exclusions disallowed +under section 911(d)(6) with respect to the amounts described in paragraph +(1)." The Form 8960 Line 13 MAGI Worksheet enters that excess on line 2c. + +`niit_magi_section_911_addition` is that excess. It defaults to +`foreign_earned_income_exclusion` and can be entered directly, for a filer +with housing amounts (which section 1411(d) leaves out) or with itemized +deductions netted out of the exclusion (which it does not subtract). + +Properties that hold for every household: + +1. Nothing added changes nothing, bit for bit. With no exclusion, or an + entered addition of zero or less, AGI, `niit_magi`, + `net_investment_income_tax` and income tax equal those of the formula + before the section 911 addition (AGI plus the estate and trust code H + change), run as a reform on the same households. A household without an + exclusion also gets the same results whatever the other households in + the batch exclude. +2. MAGI is AGI plus the positive code H change plus the addition, and the + addition is the exclusion, or the entered amount, floored at zero. +3. MAGI and the tax never fall as the excluded amount rises. +4. The tax is 3.8% of the lesser of net investment income and the excess of + MAGI over the threshold of section 1411(b) ($250,000 joint or surviving + spouse, $125,000 separate, $200,000 otherwise, not indexed), computed + here independently. + +Households live in Texas. +""" + +from functools import lru_cache + +import numpy as np +import pytest + +from policyengine_us import CountryTaxBenefitSystem, Simulation + +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +RATE = 0.038 +THRESHOLD = { + "SINGLE": 200_000, + "HEAD_OF_HOUSEHOLD": 200_000, + "JOINT": 250_000, + "SEPARATE": 125_000, + "SURVIVING_SPOUSE": 250_000, +} +STATUSES = list(THRESHOLD) +YEARS = [2018, 2022, 2025, 2026] + +OUTPUTS = [ + "filing_status", + "adjusted_gross_income", + "foreign_earned_income_exclusion", + "niit_magi_section_911_addition", + "niit_magi", + "net_investment_income", + "net_investment_income_tax", + "income_tax_before_refundable_credits", + "income_tax", +] +# Every output other than the exclusion and the addition themselves, which +# the households set. +UNCHANGED_WITHOUT_EXCLUSION = [ + "adjusted_gross_income", + "niit_magi", + "net_investment_income", + "net_investment_income_tax", + "income_tax_before_refundable_credits", + "income_tax", +] + + +def build_situation(households, year): + people, tax_units, marital_units = {}, {}, {} + groups = {"households": {}, "spm_units": {}, "families": {}} + for i, h in enumerate(households): + head = f"head_{i}" + people[head] = { + "age": {year: 45}, + "employment_income": {year: h["wages"]}, + "qualified_dividend_income": {year: h["qualified_dividends"]}, + "taxable_interest_income": {year: h["interest"]}, + "long_term_capital_gains": {year: h["long_term_gains"]}, + "estate_income": {year: h["estate_income"]}, + "estate_income_net_investment_income_adjustment": {year: h["code_h"]}, + } + members = [head] + marital_units[f"marital_unit_{i}"] = {"members": [head]} + if h["status"] == "JOINT": + spouse = f"spouse_{i}" + people[spouse] = { + "age": {year: 45}, + "employment_income": {year: h["spouse_wages"]}, + } + members.append(spouse) + marital_units[f"marital_unit_{i}"]["members"].append(spouse) + if h["status"] == "HEAD_OF_HOUSEHOLD": + child = f"child_{i}" + people[child] = {"age": {year: 17}} + members.append(child) + marital_units[f"marital_unit_{i}_child"] = {"members": [child]} + tax_unit = { + "members": members, + # Set for every tax unit: an input given to only some of them + # leaves the rest at the default (single). + "filing_status": {year: h["status"]}, + "foreign_earned_income_exclusion": {year: h["exclusion"]}, + } + if h["entered_addition"] is not None: + tax_unit["niit_magi_section_911_addition"] = {year: h["entered_addition"]} + tax_units[f"tax_unit_{i}"] = tax_unit + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "TX"}, + } + groups["spm_units"][f"spm_unit_{i}"] = {"members": members} + groups["families"][f"family_{i}"] = {"members": members} + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + **groups, + } + + +def before_the_section_911_addition(): + """A reform restoring `niit_magi` as it was before this change.""" + from policyengine_core.reforms import Reform + + from policyengine_us.model_api import ( + USD, + YEAR, + TaxUnit, + Variable, + ) + + class niit_magi(Variable): + value_type = float + entity = TaxUnit + label = "NIIT MAGI before the section 911 addition" + unit = USD + definition_period = YEAR + + def formula(tax_unit, period, parameters): + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + estate_magi_adjustment = tax_unit.sum( + not_dependent * person("estate_income_niit_magi_adjustment", period) + ) + return tax_unit("adjusted_gross_income", period) + estate_magi_adjustment + + class reform(Reform): + def apply(self): + self.update_variable(niit_magi) + + return reform + + +@lru_cache(maxsize=None) +def system_before_the_section_911_addition(): + """Built once: constructing a reformed system takes seconds.""" + return CountryTaxBenefitSystem(reform=before_the_section_911_addition()) + + +def calculate_batch(households, year, before): + simulation = Simulation( + situation=build_situation(households, year), + **( + {"tax_benefit_system": system_before_the_section_911_addition()} + if before + else {} + ), + ) + results = {} + for v in OUTPUTS: + values = simulation.calculate(v, year) + results[v] = ( + values.decode_to_str() if v == "filing_status" else np.asarray(values) + ) + return results + + +def calculate(households, year, before=False): + """Households with and without an entered addition, as separate batches. + + An input given to only some tax units in a batch sets the rest to the + default (zero), not to the value the formula would compute, so mixing + them would hide the default. + """ + entered = np.array([h["entered_addition"] is not None for h in households]) + results = {} + for group in [entered, ~entered]: + if not group.any(): + continue + batch = calculate_batch( + [h for h, g in zip(households, group) if g], year, before + ) + for v, values in batch.items(): + if v not in results: + results[v] = np.empty(len(households), dtype=values.dtype) + if v == "filing_status": + results[v] = np.empty(len(households), dtype=object) + results[v][group] = values + return results + + +def with_exclusion(households, exclusion): + """The same households with the exclusion replaced or transformed.""" + change = exclusion if callable(exclusion) else lambda _: exclusion + return [{**h, "exclusion": change(h["exclusion"])} for h in households] + + +def tolerance(*amounts): + """One cent, plus the rounding of single-precision arithmetic. + + The model stores amounts as 32-bit floats, which carry about seven + significant digits. + """ + return 0.01 + 5e-7 * np.max(np.abs(amounts), axis=0) + + +def expected_addition(household): + entered = household["entered_addition"] + amount = household["exclusion"] if entered is None else entered + return max(0, amount) + + +def assert_invariants(households, year): + law = calculate(households, year) + before = calculate(households, year, before=True) + unstacked = calculate(with_exclusion(households, 0), year) + more_excluded = calculate(with_exclusion(households, lambda e: 1.5 * e + 500), year) + assert list(law["filing_status"]) == [h["status"] for h in households] + agi = law["adjusted_gross_income"] + addition = np.array([expected_addition(h) for h in households]) + nothing_added = addition == 0 + + # 1. Nothing excluded changes nothing, bit for bit: against the formula + # before the section 911 addition, and against the same households in a + # batch where no one excludes anything. + for v in UNCHANGED_WITHOUT_EXCLUSION: + assert np.array_equal(law[v][nothing_added], before[v][nothing_added]), v + no_exclusion = np.array( + [h["exclusion"] == 0 and h["entered_addition"] is None for h in households] + ) + for v in OUTPUTS[1:]: + assert np.array_equal(law[v][no_exclusion], unstacked[v][no_exclusion]), v + assert not law["niit_magi_section_911_addition"][no_exclusion].any() + + # 2. MAGI is AGI plus the positive code H change plus the addition. + code_h = np.array([max(0, h["code_h"]) for h in households]) + magi = agi + code_h + addition + slack = tolerance(magi, law["niit_magi"]) + assert (np.abs(law["niit_magi"] - magi) <= slack).all() + assert (np.abs(law["niit_magi"] - before["niit_magi"] - addition) <= slack).all() + entered = np.array([h["entered_addition"] is not None for h in households]) + assert ( + np.abs(law["niit_magi_section_911_addition"][~entered] - addition[~entered]) + <= slack[~entered] + ).all() + + # 3. MAGI and the tax never fall as the excluded amount rises. Rounding + # is monotone, so this holds exactly. + for v in ["niit_magi", "net_investment_income_tax"]: + assert (more_excluded[v] >= law[v]).all(), v + assert (law[v] >= unstacked[v]).all(), v + + # 4. The tax from the statute, computed independently of the model's + # formula and parameters. + threshold = np.array([THRESHOLD[h["status"]] for h in households]) + net_investment_income = law["net_investment_income"] + niit = RATE * np.minimum( + np.maximum(0, net_investment_income), np.maximum(0, magi - threshold) + ) + assert (np.abs(law["net_investment_income_tax"] - niit) <= slack).all() + assert (law["net_investment_income_tax"] >= 0).all() + assert ( + law["net_investment_income_tax"] + <= RATE * np.maximum(0, net_investment_income) + slack + ).all() + return law, before + + +def household(status, wages, exclusion, **amounts): + return { + "status": status, + "wages": wages, + "spouse_wages": amounts.get("spouse_wages", 0), + "qualified_dividends": amounts.get("qualified_dividends", 0), + "interest": amounts.get("interest", 0), + "long_term_gains": amounts.get("long_term_gains", 0), + "estate_income": amounts.get("estate_income", 0), + "code_h": amounts.get("code_h", 0), + "exclusion": exclusion, + "entered_addition": amounts.get("entered_addition"), + } + + +GRID = [ + household( + status, + wages, + exclusion, + spouse_wages=40_000 if status == "JOINT" else 0, + qualified_dividends=dividends, + long_term_gains=gains, + estate_income=estate_income, + code_h=code_h, + entered_addition=entered, + ) + for status in STATUSES + for wages in (0, 120_000, 600_000) + for dividends, gains in ((0, 0), (50_000, 0), (10_000, 120_000)) + for estate_income, code_h in ((0, 0), (12_000, 5_000)) + for exclusion in (0, 1, 60_000, 130_000, 260_000) + for entered in (None, 40_000) +] + [ + # An entered negative amount, and an entered zero beside a positive + # exclusion. + household( + "SINGLE", 150_000, 0, qualified_dividends=50_000, entered_addition=-5_000 + ), + household( + "SINGLE", 150_000, 100_000, qualified_dividends=50_000, entered_addition=0 + ), +] + + +@pytest.mark.parametrize("year", YEARS) +def test_grid_invariants(year): + law, before = assert_invariants(GRID, year) + # The grid reaches filers whose tax the addition raises and filers it + # does not. + raised = law["net_investment_income_tax"] > before["net_investment_income_tax"] + assert raised.any() + assert (~raised & (law["niit_magi_section_911_addition"] > 0)).any() + + +def test_review_example(): + # 2025, single, $150,000 of wages, $50,000 of qualified dividends and a + # $100,000 section 911(a)(1) exclusion. MAGI is $300,000, $100,000 above + # the threshold, so the tax is 3.8% of $50,000. + example = household("SINGLE", 150_000, 100_000, qualified_dividends=50_000) + law, before = assert_invariants([example], 2025) + assert law["adjusted_gross_income"][0] == 200_000 + assert law["niit_magi"][0] == 300_000 + assert law["net_investment_income_tax"][0] == pytest.approx(1_900, abs=0.01) + # Before the change, MAGI stayed at AGI and there was no tax. + assert before["niit_magi"][0] == 200_000 + assert before["net_investment_income_tax"][0] == 0 + + +household_strategy = st.fixed_dictionaries( + { + "status": st.sampled_from(STATUSES), + "wages": st.integers(0, 900_000), + "spouse_wages": st.one_of(st.just(0), st.integers(1, 400_000)), + "qualified_dividends": st.one_of(st.just(0), st.integers(1, 300_000)), + "interest": st.one_of(st.just(0), st.integers(1, 100_000)), + "long_term_gains": st.one_of(st.just(0), st.integers(-50_000, 1_000_000)), + "estate_income": st.one_of(st.just(0), st.integers(1, 100_000)), + "code_h": st.one_of(st.just(0), st.integers(-50_000, 50_000)), + "exclusion": st.one_of(st.just(0), st.integers(1, 300_000)), + "entered_addition": st.one_of( + st.none(), st.just(0), st.integers(-20_000, 300_000) + ), + } +) + +SETTINGS = dict( + max_examples=10, + deadline=None, + suppress_health_check=[ + hypothesis.HealthCheck.too_slow, + hypothesis.HealthCheck.data_too_large, + ], +) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given( + st.lists(household_strategy, min_size=1, max_size=25), + st.sampled_from(YEARS), +) +def test_random_households_keep_the_invariants(households, year): + assert_invariants(households, year) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/foreign_earned_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/foreign_earned_income.py index 44dbb48992b..601559b2e36 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/foreign_earned_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/foreign_earned_income.py @@ -6,7 +6,7 @@ class foreign_earned_income_exclusion(Variable): entity = TaxUnit label = "Foreign earned income exclusion" unit = USD - documentation = "Income earned and any housing expense in foreign countries that is excluded from adjusted gross income under 26 U.S. Code § 911. Other income inputs are the amounts left after the exclusion. Section 911(f) adds this amount back to set the tax rates on that income: line 2c of the Foreign Earned Income Tax Worksheet (Form 2555 lines 45 and 50, less deductions disallowed because they relate to the excluded income)." + documentation = "Income earned and any housing expense in foreign countries that is excluded from adjusted gross income under 26 U.S. Code § 911. Other income inputs are the amounts left after the exclusion. Section 911(f) adds this amount back to set the tax rates on that income: line 2c of the Foreign Earned Income Tax Worksheet (Form 2555 lines 45 and 50, less deductions disallowed because they relate to the excluded income). The net investment income tax adds back only the section 911(a)(1) part; see niit_magi_section_911_addition." definition_period = YEAR reference = [ "https://www.law.cornell.edu/uscode/text/26/911", diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi.py b/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi.py index bf52b431407..57c8dce26f0 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi.py @@ -9,16 +9,20 @@ class niit_magi(Variable): documentation = ( "Form 8960 line 13 modified adjusted gross income: adjusted gross " "income plus the MAGI change that comes with Schedule K-1 (Form 1041) " - "box 14 code H amounts. A dependent's amounts stay on the dependent's " - "own return. The foreign earned income exclusion add-back and the " - "CFC and PFIC adjustments of Treas. Reg. 1.1411-10(e) are not modeled." + "box 14 code H amounts, plus the foreign earned income excluded under " + "26 U.S.C. 911(a)(1) net of the deductions disallowed for it (26 U.S.C. " + "1411(d); see niit_magi_section_911_addition). A dependent's amounts " + "stay on the dependent's own return. The CFC and PFIC adjustments of " + "Treas. Reg. 1.1411-10(e) are not modeled." ) definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/1411#d", + "https://www.law.cornell.edu/cfr/text/26/1.1411-2", "https://www.irs.gov/pub/irs-prior/i8960--2024.pdf#page=11", "https://www.irs.gov/pub/irs-prior/i8960--2024.pdf#page=19", "https://www.irs.gov/pub/irs-prior/i8960--2024.pdf#page=20", + "https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=23", ) def formula(tax_unit, period, parameters): @@ -27,4 +31,14 @@ def formula(tax_unit, period, parameters): estate_magi_adjustment = tax_unit.sum( not_dependent * person("estate_income_niit_magi_adjustment", period) ) - return tax_unit("adjusted_gross_income", period) + estate_magi_adjustment + # Section 1411(d) adds "the excess (if any)" of the section + # 911(a)(1) exclusion over the amounts section 911(d)(6) disallows + # for it, so the addition is never negative. + section_911_addition = max_( + 0, tax_unit("niit_magi_section_911_addition", period) + ) + return ( + tax_unit("adjusted_gross_income", period) + + estate_magi_adjustment + + section_911_addition + ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi_section_911_addition.py b/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi_section_911_addition.py new file mode 100644 index 00000000000..1e66576ff87 --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/niit_magi_section_911_addition.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * + + +class niit_magi_section_911_addition(Variable): + value_type = float + entity = TaxUnit + label = "Section 911 addition to modified adjusted gross income for the net investment income tax" + unit = USD + documentation = ( + "The amount 26 U.S.C. 1411(d) adds to adjusted gross income: the " + "foreign earned income excluded under section 911(a)(1), less the " + "deductions and exclusions disallowed under section 911(d)(6) for it. " + "On the Form 8960 Line 13 MAGI Worksheet this is line 2c: Form 2555 " + "line 42 less the Form 2555 line 44 deductions allocable to it. " + "Section 1411(d) does not add back the housing exclusion of section " + "911(a)(2) (Form 2555 line 36) or the housing deduction (line 50). " + "The default is foreign_earned_income_exclusion, line 2c of the Form " + "1040 Foreign Earned Income Tax Worksheet: Form 2555 lines 45 and 50, " + "less the itemized deductions and exclusions disallowed because they " + "relate to the excluded income (worksheet line 2b). For a filer with " + "no housing exclusion, no housing deduction and nothing on worksheet " + "line 2b, the default equals the section 1411(d) amount. " + "Housing amounts make it too high. Itemized deductions on line 2b " + "make it too low, since section 1411(d)(2) subtracts only deductions " + "taken into account in computing adjusted gross income. For any other " + "filer, enter the Form 8960 worksheet amount directly." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1411#d", + "https://www.law.cornell.edu/uscode/text/26/911#a", + "https://www.law.cornell.edu/cfr/text/26/1.1411-2", + "https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=22", + "https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=23", + "https://www.irs.gov/pub/irs-prior/f2555--2025.pdf#page=3", + "https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf#page=37", + ) + + def formula(tax_unit, period, parameters): + return max_(0, tax_unit("foreign_earned_income_exclusion", period))