diff --git a/changelog.d/amti-section-68-and-tax-refunds.fixed.md b/changelog.d/amti-section-68-and-tax-refunds.fixed.md new file mode 100644 index 00000000000..fb309349410 --- /dev/null +++ b/changelog.d/amti-section-68-and-tax-refunds.fixed.md @@ -0,0 +1 @@ +Leave state and local tax refunds and the section 68 itemized deduction reduction out of federal alternative minimum taxable income, and leave Colorado's state income tax addback and refund subtraction out of Colorado alternative minimum taxable income. diff --git a/policyengine_us/tests/core/test_amt_income_refund_and_section_68_invariants.py b/policyengine_us/tests/core/test_amt_income_refund_and_section_68_invariants.py new file mode 100644 index 00000000000..c157ef466eb --- /dev/null +++ b/policyengine_us/tests/core/test_amt_income_refund_and_section_68_invariants.py @@ -0,0 +1,84 @@ +"""Alternative minimum taxable income leaves out tax refunds and section 68. + +26 U.S.C. 56(b)(1)(C): no recovery of state or local taxes is in alternative +minimum taxable income (Form 6251 line 2b). 56(b)(1)(E): section 68 does not +apply. With the itemization choice fixed and no deductions that depend on AGI +(no medical expenses, charity well under its AGI limits), adding a state tax +refund to a filer's income must leave AMTI unchanged in every year: the refund +enters AGI and is subtracted back, and any change it causes in the section 68 +reduction or the capped SALT deduction cancels because AMTI reverses the +first and adds back the second. + +Each Hypothesis example evaluates a batch of tax units and their twins +without the refund in one vectorized simulation, in a state with no income tax. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 + + +@st.composite +def tax_unit(draw): + earnings = draw(st.integers(20_000, 3_000_000)) + return { + "joint": draw(st.booleans()), + "itemizes": draw(st.booleans()), + "employment_income": earnings, + "salt_refund_income": draw(st.integers(1, 20_000)), + "real_estate_taxes": draw(st.integers(0, 40_000)), + "home_mortgage_interest": draw(st.integers(0, 40_000)), + # Under 10% of earnings keeps charity below every AGI limit. + "charitable_cash_donations": draw(st.integers(0, earnings // 10)), + } + + +def _amt_income(units, year): + situation = {"people": {}, "tax_units": {}, "households": {}} + for twin in (0, 1): + for i, unit in enumerate(units): + head = f"head_{twin}_{i}" + members = [head] + situation["people"][head] = { + "age": {year: 45}, + "employment_income": {year: unit["employment_income"]}, + "salt_refund_income": { + year: unit["salt_refund_income"] if twin == 0 else 0 + }, + "real_estate_taxes": {year: unit["real_estate_taxes"]}, + "home_mortgage_interest": {year: unit["home_mortgage_interest"]}, + "charitable_cash_donations": {year: unit["charitable_cash_donations"]}, + } + if unit["joint"]: + spouse = f"spouse_{twin}_{i}" + situation["people"][spouse] = {"age": {year: 45}} + members.append(spouse) + situation["tax_units"][f"unit_{twin}_{i}"] = { + "members": members, + "tax_unit_itemizes": {year: unit["itemizes"]}, + } + situation["households"][f"household_{twin}_{i}"] = { + "members": members, + "state_code": {year: "TX"}, + } + simulation = Simulation(situation=situation) + amti = np.asarray(simulation.calculate("amt_income", year), dtype=float) + return amti[: len(units)], amti[len(units) :] + + +@pytest.mark.parametrize("year", [2017, 2025, 2026]) +@settings( + max_examples=4, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(units=st.lists(tax_unit(), min_size=8, max_size=8)) +def test_state_tax_refund_does_not_change_amt_income(year, units): + with_refund, without_refund = _amt_income(units, year) + np.testing.assert_allclose(with_refund, without_refund, atol=TOLERANCE) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.yaml index 6543111b8d2..15ed9bbe782 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.yaml @@ -161,3 +161,88 @@ # The senior deduction is not in line 14 and is not added back, so # line 4 is AGI for a filer who takes the standard deduction. amt_income: 100_000 + +# 26 U.S.C. 56(b)(1)(C); 2025 Form 6251 line 2b: refunds of state and local +# taxes included in income are subtracted. +- name: Single itemizer subtracts a state income tax refund, 2025 + absolute_error_margin: 0.01 + period: 2025 + input: + tax_unit_itemizes: true + taxable_income: 100_000 + salt_deduction: 8_000 + salt_refund_income: 2_000 + filing_status: SINGLE + output: + # 100,000 + 8,000 - 2,000 + amt_income: 106_000 + +# 26 U.S.C. 56(b)(1)(E): section 68 does not apply, so the itemized deduction +# reduction in taxable income comes back out. +- name: Single itemizer reverses the section 68 reduction, 2026 + absolute_error_margin: 0.01 + period: 2026 + input: + tax_unit_itemizes: true + taxable_income: 1_000_000 + salt_deduction: 10_000 + itemized_taxable_income_deductions_reduction: 5_000 + filing_status: SINGLE + output: + # 1,000,000 + 10,000 - 5,000 + amt_income: 1_005_000 + +- name: Non-itemizer has no section 68 reduction to reverse, 2026 + absolute_error_margin: 0.01 + period: 2026 + input: + tax_unit_itemizes: false + taxable_income: 50_000 + standard_deduction: 1_000 + itemized_taxable_income_deductions_reduction: 5_000 + filing_status: SINGLE + output: + amt_income: 51_000 + +# 2017 Form 6251 line 6 enters line 9 of the Schedule A Itemized Deductions +# Worksheet (the section 68 reduction) as a negative amount. +- name: Single itemizer reverses the 2017 overall limitation on itemized deductions + absolute_error_margin: 0.01 + period: 2017 + input: + tax_unit_itemizes: true + taxable_income: 300_000 + exemptions: 4_050 + salt_deduction: 20_000 + misc_deduction: 0 + itemized_taxable_income_deductions_reduction: 4_155 + filing_status: SINGLE + output: + # 300,000 + 4,050 + 20,000 - 4,155 + amt_income: 319_895 + +- name: A dependent's state tax refund is not subtracted from the filer's AMTI, 2025 + absolute_error_margin: 0.01 + period: 2025 + input: + people: + person1: + age: 45 + salt_refund_income: 1_000 + person2: + age: 17 + is_tax_unit_dependent: true + salt_refund_income: 500 + tax_units: + tax_unit: + members: [person1, person2] + tax_unit_itemizes: true + taxable_income: 100_000 + salt_deduction: 8_000 + filing_status: HEAD_OF_HOUSEHOLD + households: + household: + members: [person1, person2] + output: + # 100,000 + 8,000 - 1,000 + amt_income: 107_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.yaml index 35a377bc3f2..809ac870211 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.yaml @@ -31,3 +31,19 @@ output: # max(0, 0 + 0 - 50,000) = 0 co_alternative_minimum_taxable_income: 0 + +# DR 0104AMT (2025): line 2 sums DR 0104 lines 3 through 7 and 9, leaving out +# the state income tax addback on line 2; line 4 sums the DR 0104AD +# subtractions excluding the state income tax refund on line 1. +- name: CO AMTI leaves out the state income tax addback and the refund subtraction + period: 2025 + input: + amt_income_less_exemptions: 200_000 + co_additions: 10_000 + co_state_addback: 6_000 + co_subtractions: 5_000 + salt_refund_income: 1_500 + state_code: CO + output: + # 200,000 + (10,000 - 6,000) - (5,000 - 1,500) + co_alternative_minimum_taxable_income: 200_500 diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.py index 5c488f3054b..6882c72bf01 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income.py @@ -13,6 +13,9 @@ class amt_income(Variable): "https://www.irs.gov/pub/irs-prior/f6251--2024.pdf#page=1", "https://www.irs.gov/pub/irs-prior/f6251--2025.pdf#page=1", "https://www.irs.gov/pub/irs-prior/i6251--2025.pdf#page=2", + # 2017 Form 6251 lines 6 and 7, and instructions (PDF pages 2-3). + "https://www.irs.gov/pub/irs-prior/f6251--2017.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=2", ) def formula(tax_unit, period, parameters): @@ -36,6 +39,17 @@ def formula(tax_unit, period, parameters): deductions_add_back = ( add(tax_unit, period, p.deductions_add_back) if p.deductions_add_back else 0 ) + # 2025 line 2b (2017 line 7): refunds of state and local taxes in + # income are not AMTI (26 U.S.C. 56(b)(1)(C)). Dependents' refunds are + # on their own returns, as irs_gross_income treats them. + tax_refunds = tax_unit_non_dep_add(tax_unit, period, ["salt_refund_income"]) + # Section 68 does not apply to AMTI (26 U.S.C. 56(b)(1)(E)), so the + # itemized deduction reduction in line 1 comes back out (2017 line 6). + section_68_reduction = where( + tax_unit("tax_unit_itemizes", period), + tax_unit("itemized_taxable_income_deductions_reduction", period), + 0, + ) return ( line_1 + deductions_add_back @@ -44,4 +58,6 @@ def formula(tax_unit, period, parameters): period, ["amt_excluded_deductions", "amt_separate_addition"], ) + - tax_refunds + - section_68_reduction ) diff --git a/policyengine_us/variables/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.py b/policyengine_us/variables/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.py index 99a985d0093..83094b51cad 100644 --- a/policyengine_us/variables/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.py +++ b/policyengine_us/variables/gov/states/co/tax/income/amt/co_alternative_minimum_taxable_income.py @@ -11,6 +11,7 @@ class co_alternative_minimum_taxable_income(Variable): reference = [ "https://tax.colorado.gov/DR0104AMT", "https://law.justia.com/codes/colorado/2022/title-39/article-22/part-1/section-39-22-105/", + "https://tax.colorado.gov/sites/tax/files/documents/DR0104AMT_2025.pdf#page=1", ] def formula(tax_unit, period, parameters): @@ -19,10 +20,18 @@ def formula(tax_unit, period, parameters): federal_amt_income_less_exemptions = tax_unit( "amt_income_less_exemptions", period ) - # Line 2: Colorado additions (from DR 0104, lines 3-8) - co_additions = tax_unit("co_additions", period) - # Line 4: Colorado subtractions (excluding state income tax refund) - co_subtractions = tax_unit("co_subtractions", period) + # Line 2: Colorado additions from DR 0104, lines 3 through 7 and 9. The + # state income tax addback (DR 0104 line 2) is left out because federal + # alternative minimum taxable income never deducted those taxes. + co_additions = tax_unit("co_additions", period) - tax_unit( + "co_state_addback", period + ) + # Line 4: Colorado subtractions excluding the state income tax refund + # (DR 0104AD line 1), which federal alternative minimum taxable income + # already leaves out. + co_subtractions = tax_unit("co_subtractions", period) - add( + tax_unit, period, ["salt_refund_income"] + ) # Line 5: Colorado AMTI return max_( 0,