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
1 change: 1 addition & 0 deletions changelog.d/fix-tips-overtime-2029-termination.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Limit the tips and overtime deductions to 2025-2028, as 26 U.S.C. 224(h) and 225(g) and their effective dates require, with new in_effect parameters; the federal deduction lists now name both deductions in every year so those parameters alone set the years. From 2029 Montana and Arizona no longer subtract them and South Carolina's OBBBA add-back no longer includes them.
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
description: Deductions from taxable income if itemizing.
values:
# The tips and overtime deductions (26 U.S.C. 224 and 225) are listed in every
# year. Their own in_effect parameters zero them outside 2025-2028, so a reform
# that switches them on in another year also reaches taxable income.
2018-01-01:
- qualified_business_income_deduction
- wagering_losses_deduction
- itemized_taxable_income_deductions
- tip_income_deduction
- overtime_income_deduction

2025-01-01:
- qualified_business_income_deduction
Expand All @@ -18,6 +23,8 @@ values:
- qualified_business_income_deduction
- wagering_losses_deduction
- itemized_taxable_income_deductions
- tip_income_deduction
- overtime_income_deduction

metadata:
unit: list
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
description: Deductions from taxable income if not itemizing.
values:
# The tips and overtime deductions (26 U.S.C. 224 and 225) are listed in every
# year. Their own in_effect parameters zero them outside 2025-2028, so a reform
# that switches them on in another year also reaches taxable income.
2018-01-01:
- standard_deduction
- qualified_business_income_deduction
- tuition_and_fees_deduction
- tip_income_deduction
- overtime_income_deduction
# The 2020 charitable deduction for non-itemizers is above the line
# (charitable_contributions_ald). From 2021 it is taken from adjusted gross
# income under 26 U.S.C. 170(p) and 63(b)(4).
2020-01-01:
- standard_deduction
- qualified_business_income_deduction
- tip_income_deduction
- overtime_income_deduction
2021-01-01:
- charitable_deduction_for_non_itemizers
- standard_deduction
- qualified_business_income_deduction
- tip_income_deduction
- overtime_income_deduction
2025-01-01:
- charitable_deduction_for_non_itemizers
- standard_deduction
Expand All @@ -26,6 +35,8 @@ values:
- charitable_deduction_for_non_itemizers
- standard_deduction
- qualified_business_income_deduction
- tip_income_deduction
- overtime_income_deduction

metadata:
unit: list
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
description: The IRS allows the overtime income deduction when this is true.
values:
0000-01-01: false
# Pub. L. 119-21, sec. 70202(g): section 225 applies to taxable years
# beginning after December 31, 2024.
2025-01-01: true
# 26 U.S.C. 225(g): no deduction is allowed for any taxable year beginning
# after December 31, 2028.
2029-01-01: false

metadata:
unit: bool
period: year
label: Overtime income deduction in effect
reference:
- title: 26 U.S.C. 225(g)
href: https://www.law.cornell.edu/uscode/text/26/225#g
- title: "One Big Beautiful Bill Act, Pub. L. No. 119-21, secs. 70202(a) (adding 26 U.S.C. 225(g)) and 70202(g) (effective date) #page 103-104"
href: https://www.congress.gov/119/bills/hr1/BILLS-119hr1enr.pdf#page=103
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
description: The IRS allows the tip income deduction when this is true.
values:
0000-01-01: false
# Pub. L. 119-21, sec. 70201(j): section 224 applies to taxable years
# beginning after December 31, 2024.
2025-01-01: true
# 26 U.S.C. 224(h): no deduction is allowed for any taxable year beginning
# after December 31, 2028.
2029-01-01: false

metadata:
unit: bool
period: year
label: Tip income deduction in effect
reference:
- title: 26 U.S.C. 224(h)
href: https://www.law.cornell.edu/uscode/text/26/224#h
- title: "One Big Beautiful Bill Act, Pub. L. No. 119-21, secs. 70201(a) (adding 26 U.S.C. 224(h)) and 70201(j) (effective date) #page 100,102"
href: https://www.congress.gov/119/bills/hr1/BILLS-119hr1enr.pdf#page=100
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ description: Colorado adds back the federal overtime compensation deduction in t
values:
2025-01-01: false
2026-01-01: true
2029-01-01: false

metadata:
unit: bool
Expand All @@ -17,12 +16,11 @@ metadata:
# effective for tax years beginning on or after January 1, 2026. Colorado continues
# to conform to the federal tips deduction (IRC 224), so tips are not added back.
# Colorado conformed for tax year 2025 (Section 6 was not yet effective).
# The federal IRC 225 overtime deduction sunsets after 2028: it is dropped from
# the deductions_if_itemizing and deductions_if_not_itemizing lists at 2029, so
# federal taxable income no longer reflects it. The overtime_income_deduction
# variable is not itself year-gated (it still computes a nonzero amount), so the
# add-back is turned off here from 2029 -- otherwise Colorado would add back a
# deduction that was never taken federally, over-taxing filers.
# C.R.S. 39-22-104(3)(u) adds back "the amount of any overtime compensation
# excluded or deducted from federal gross income" and has no end date. The
# federal IRC 225 deduction ends after 2028 (225(g)), and overtime_income_deduction
# is zero from 2029 (gov.irs.deductions.overtime_income.in_effect), so the
# add-back falls to zero with it and needs no end date here.
# The 2026 start is enforced both here and by the year block in additions.yaml
# (co_overtime_income_addback appears only from the 2026 list); update both if
# the effective dates change.
8 changes: 7 additions & 1 deletion policyengine_us/reforms/crfb/senior_deduction_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@

def create_senior_deduction_extension() -> Reform:
def modify_parameters(parameters):
# Update deductions_if_itemizing to include additional_senior_deduction after 2028
# Update deductions_if_itemizing to include additional_senior_deduction after 2028.
# The tips and overtime deductions stay listed, as in the baseline lists;
# their own in_effect parameters zero them after 2028.
parameters.gov.irs.deductions.deductions_if_itemizing.update(
start=instant("2029-01-01"),
value=[
"qualified_business_income_deduction",
"wagering_losses_deduction",
"itemized_taxable_income_deductions",
"tip_income_deduction",
"overtime_income_deduction",
"additional_senior_deduction",
],
)
Expand All @@ -22,6 +26,8 @@ def modify_parameters(parameters):
value=[
"standard_deduction",
"qualified_business_income_deduction",
"tip_income_deduction",
"overtime_income_deduction",
"additional_senior_deduction",
"charitable_deduction_for_non_itemizers",
],
Expand Down
171 changes: 171 additions & 0 deletions policyengine_us/tests/core/test_tip_and_overtime_deduction_years.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""Years in which the tips and overtime deductions apply (26 U.S.C. 224, 225).

Sections 224 and 225 apply to taxable years beginning after December 31, 2024
(Pub. L. 119-21, secs. 70201(j) and 70202(g)), and 224(h) and 225(g) say "No
deduction shall be allowed under this section for any taxable year beginning
after December 31, 2028."

The first test builds two simulations of many tax units (current law, and
both deductions switched on in every year); the second builds one. Inputs are
the same in every year. The tests check:

1. Outside 2025-2028 both deductions are zero for every return.
2. The year switch is the only thing that changes across years. With both
deductions switched on in every year, each return gets in every year what
it gets in 2025. Inside 2025-2028 that is also the current-law amount. This
holds because the caps, thresholds and phase-out parameters each carry one
value, dated 2025; if any is later indexed or amended, compare against
that year's value instead.
3. South Carolina's add-back, Montana's Schedule 1-A subtraction and
Arizona's subtractions pass the federal deductions through exactly. Giving
a return its tips and overtime moves each by the federal tips and overtime
deductions in 2026-2028, and not at all from 2029 (Hypothesis).
"""

import numpy as np
from hypothesis import Phase, example, given, settings
from hypothesis import strategies as st

from policyengine_us import Simulation
from policyengine_us.system import system

YEARS = range(2015, 2036)
FIRST_YEAR, LAST_YEAR = 2025, 2028
STATUSES = ("SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE")
OUTPUTS = ("tip_income_deduction", "overtime_income_deduction")
SWITCHES = (
"gov.irs.deductions.tip_income.in_effect",
"gov.irs.deductions.overtime_income.in_effect",
)
# Each state's channel for the federal deductions, as a tax unit amount.
STATE_OUTPUTS = {
"SC": "sc_obbba_addback",
"MT": "mt_federal_obbba_subtraction",
"AZ": "az_subtractions",
}
STATE_YEARS = (2026, 2027, 2028, 2029, 2030, 2035)


def _situation(units, years, state_of=None):
"""One tax unit per (status, magi, tips, overtime); joint units get a
spouse with the same tips and overtime, others one person."""
people, tax_units, marital_units, households = {}, {}, {}, {}
for i, (status, magi, tips, overtime) in enumerate(units):
members = [f"head{i}"] + ([f"spouse{i}"] if status == "JOINT" else [])
for name in members:
people[name] = {
"age": {year: 40 for year in years},
"ssn_card_type": {year: "CITIZEN" for year in years},
"treasury_tipped_occupation_code": {year: 102 for year in years},
"tip_income": {year: float(tips) for year in years},
"fsla_overtime_premium": {year: float(overtime) for year in years},
}
tax_units[f"t{i}"] = {
"members": members,
"filing_status": {year: status for year in years},
"adjusted_gross_income": {year: float(magi) for year in years},
# Fixed, so that adding tips cannot change South Carolina's
# standard deduction add-back through the itemizing choice.
"tax_unit_itemizes": {year: False for year in years},
}
marital_units[f"m{i}"] = {"members": members}
state = state_of[i] if state_of else "TX"
households[f"h{i}"] = {
"members": members,
"state_code": {year: state for year in years},
}
return {
"people": people,
"tax_units": tax_units,
"marital_units": marital_units,
"households": households,
}


def _draw(seed, n=120):
rng = np.random.default_rng(seed)
units = []
for _ in range(n):
status = STATUSES[int(rng.integers(len(STATUSES)))]
magi = float(rng.choice([0, 60_000, 150_000, int(rng.integers(0, 500_001))]))
tips = int(rng.choice([0, 25_000, int(rng.integers(0, 40_001))]))
overtime = int(rng.choice([0, 12_500, int(rng.integers(0, 30_001))]))
units.append((status, magi, tips, overtime))
return units


def test_deductions_apply_only_from_2025_through_2028():
# One draw, so the switched-on reform builds one model.
units = _draw(seed=0, n=240)
situation = _situation(units, YEARS)
baseline = Simulation(tax_benefit_system=system, situation=situation)
always = Simulation(
situation=situation,
reform={switch: {"2015-01-01.2100-12-31": True} for switch in SWITCHES},
)
for name in OUTPUTS:
first_year = baseline.calculate(name, FIRST_YEAR)
# Guard: the draw must hold returns with a positive deduction.
assert np.any(first_year > 0), name
for year in YEARS:
current_law = baseline.calculate(name, year)
switched_on = always.calculate(name, year)
# 2. The switch alone changes the amount across years.
np.testing.assert_array_equal(switched_on, first_year, (name, year))
if FIRST_YEAR <= year <= LAST_YEAR:
np.testing.assert_array_equal(current_law, first_year, (name, year))
else:
# 1. Nothing outside 2025-2028.
assert np.all(current_law == 0), (name, year)


amounts = st.one_of(
st.sampled_from([0, 12_500, 25_000]), st.integers(min_value=0, max_value=40_000)
)
cases = st.tuples(
st.sampled_from(STATUSES),
st.one_of(
st.sampled_from([60_000, 150_000, 300_000]),
st.integers(min_value=0, max_value=500_000),
),
amounts,
amounts,
)


# Shrinking a failure takes minutes on a simulation this size; the failing
# example is still reported.
@settings(
max_examples=5,
deadline=None,
derandomize=True,
phases=(Phase.explicit, Phase.generate),
)
@given(drawn=st.lists(cases, min_size=8, max_size=8))
# Generation starts from the minimal draw (no tips or overtime), so one
# explicit draw makes sure positive deductions reach every state.
@example(drawn=[(status, 60_000, 10_000, 5_000) for status in STATUSES] * 2)
def test_states_pass_the_federal_deductions_through_only_while_they_apply(drawn):
# Each drawn return appears in each state twice: with its tips and
# overtime, and with none.
units, state_of = [], []
for status, magi, tips, overtime in drawn:
for state in STATE_OUTPUTS:
units += [(status, magi, tips, overtime), (status, magi, 0, 0)]
state_of += [state, state]
simulation = Simulation(
tax_benefit_system=system,
situation=_situation(units, STATE_YEARS, state_of),
)
state_of = np.array(state_of)
for year in STATE_YEARS:
federal = sum(simulation.calculate(name, year) for name in OUTPUTS)
for state, name in STATE_OUTPUTS.items():
amount = simulation.calculate(name, year, map_to="tax_unit")
rows = np.flatnonzero(state_of == state)
with_pay, without_pay = rows[0::2], rows[1::2]
moved = amount[with_pay] - amount[without_pay]
# 3. Exactly the federal deductions while they apply, else nothing.
expected = federal[with_pay] if year <= LAST_YEAR else 0
np.testing.assert_allclose(moved, expected, atol=0.01, err_msg=(name, year))
assert np.all(federal[without_pay] == 0), (name, year)
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,29 @@
- name: 2030, after the tip, overtime, senior and vehicle loan deductions end
period: 2030
input:
adjusted_gross_income: 500_000
filing_status: SINGLE
qualified_business_income_deduction: 1_000
tip_income_deduction: 2_000
overtime_income_deduction: 3_000
additional_senior_deduction: 4_000
auto_loan_interest_deduction: 5_000
people:
person1:
age: 40
tip_income: 2_000
treasury_tipped_occupation_code: 102
fsla_overtime_premium: 3_000
tax_units:
tax_unit:
members: [person1]
adjusted_gross_income: 100_000
filing_status: SINGLE
qualified_business_income_deduction: 1_000
additional_senior_deduction: 4_000
auto_loan_interest_deduction: 5_000
households:
household:
members: [person1]
output:
# Sections 224(h), 225(g), 151(d)(5)(C)(i) and 163(h)(4)(A), as added by
# P.L. 119-21, allow these deductions only for taxable years beginning
# before 2029, so only the QBI deduction is subtracted.
taxable_income_before_itemized_deductions: 499_000
# before 2029, so only the QBI deduction is subtracted. The tips and
# overtime deductions are zero from 2029 (their in_effect parameters);
# the senior and vehicle loan deductions are off the 2029 deduction list.
tip_income_deduction: 0
overtime_income_deduction: 0
taxable_income_before_itemized_deductions: 99_000
Loading
Loading