Skip to content
Draft
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
1 change: 1 addition & 0 deletions changelog.d/amti-section-68-and-tax-refunds.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -44,4 +58,6 @@ def formula(tax_unit, period, parameters):
period,
["amt_excluded_deductions", "amt_separate_addition"],
)
- tax_refunds
- section_68_reduction
)
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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,
Expand Down