From 791fd13e41ba0fd20cf89948ca62ff379275e167 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 21:10:02 -0400 Subject: [PATCH 1/3] Use the $100,000 car loan interest threshold for surviving spouses 26 U.S.C. 163(h)(4)(C)(ii)(I) reduces the deduction for modified AGI over $100,000 ($200,000 in the case of a joint return), and Schedule 1-A line 26 reads "$100,000 ($200,000 if married filing jointly)". A surviving spouse does not file a joint return, so the parameter's $200,000 was wrong. Adds statute and form references, and YAML boundary cases for a surviving spouse at $100,000, $100,001, $101,000, $101,001 and $150,000, plus the threshold for head of household, separate and joint filers. Co-Authored-By: Claude Opus 5.5 --- ...terest-surviving-spouse-threshold.fixed.md | 1 + .../auto_loan_interest/phase_out/start.yaml | 11 ++- .../auto_loan_interest_deduction.yaml | 89 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fix-auto-loan-interest-surviving-spouse-threshold.fixed.md diff --git a/changelog.d/fix-auto-loan-interest-surviving-spouse-threshold.fixed.md b/changelog.d/fix-auto-loan-interest-surviving-spouse-threshold.fixed.md new file mode 100644 index 00000000000..acf98b0b0df --- /dev/null +++ b/changelog.d/fix-auto-loan-interest-surviving-spouse-threshold.fixed.md @@ -0,0 +1 @@ +Use the $100,000 car loan interest deduction phase-out threshold for surviving spouses, as 26 U.S.C. 163(h)(4)(C)(ii)(I) and Schedule 1-A line 26 provide; only a joint return gets $200,000. diff --git a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml index a61801f08d3..6a56f45df09 100644 --- a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml @@ -1,4 +1,4 @@ -description: The IRS reduces the auto loan interest deduction for filers with adjusted gross income above this threshold, based on filing status. +description: The IRS reduces the auto loan interest deduction for filers with modified adjusted gross income above this threshold, based on filing status. metadata: propagate_metadata_to_children: true unit: currency-USD @@ -7,8 +7,10 @@ metadata: breakdown: - filing_status reference: - - title: H.R.1 - One Big Beautiful Bill Act - href: https://www.congress.gov/bill/119th-congress/house-bill/1/text + - title: 26 U.S.C. § 163(h)(4)(C)(ii)(I) + href: https://www.law.cornell.edu/uscode/text/26/163#h_4_C_ii_I + - title: 2025 Schedule 1-A (Form 1040), line 26 + href: https://www.irs.gov/pub/irs-pdf/f1040s1a.pdf#page=2 SINGLE: 2025-01-01: 100_000 @@ -18,5 +20,6 @@ HEAD_OF_HOUSEHOLD: 2025-01-01: 100_000 JOINT: 2025-01-01: 200_000 +# A surviving spouse does not file a joint return, so the $100,000 amount applies. SURVIVING_SPOUSE: - 2025-01-01: 200_000 + 2025-01-01: 100_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml index ff5e3321024..c28c9784852 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml @@ -48,3 +48,92 @@ # 26 U.S.C. 163(h)(4)(C)(ii)(II): modified AGI 105,000; five $1,000 # steps over 100,000 at $200 each. auto_loan_interest_deduction: 4_000 + +# 26 U.S.C. 163(h)(4)(C)(ii)(I): $200 for each $1,000 (or portion thereof) of +# modified AGI over $100,000 ($200,000 on a joint return). Schedule 1-A +# (2025), line 26: "$100,000 ($200,000 if married filing jointly)". A +# surviving spouse does not file a joint return, so the $100,000 amount +# applies; line 28 rounds a partial $1,000 up to a whole one. +- name: Surviving spouse at the $100,000 threshold keeps the full deduction + period: 2025 + input: + adjusted_gross_income: 100_000 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 5_000 + +- name: Surviving spouse one dollar over $100,000 loses one $200 step + period: 2025 + input: + adjusted_gross_income: 100_001 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + # A $1 excess is a portion of $1,000, so one step. + auto_loan_interest_deduction: 4_800 + +- name: Surviving spouse $1,000 over $100,000 loses one $200 step + period: 2025 + input: + adjusted_gross_income: 101_000 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 4_800 + +- name: Surviving spouse $1,001 over $100,000 loses two $200 steps + period: 2025 + input: + adjusted_gross_income: 101_001 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 4_600 + +- name: Surviving spouse at $150,000 is fully phased out + period: 2025 + input: + adjusted_gross_income: 150_000 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 12_000 + output: + # Capped at $10,000, less 50 steps of $200. + auto_loan_interest_deduction: 0 + +- name: Head of household one dollar over $100,000 loses one $200 step + period: 2025 + input: + adjusted_gross_income: 100_001 + filing_status: HEAD_OF_HOUSEHOLD + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 4_800 + +- name: Married filing separately may claim, from the $100,000 threshold + period: 2025 + input: + adjusted_gross_income: 100_001 + filing_status: SEPARATE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + # Section 163(h)(4) has no joint-return requirement. + auto_loan_interest_deduction: 4_800 + +- name: Joint return at the $200,000 threshold keeps the full deduction + period: 2025 + input: + adjusted_gross_income: 200_000 + filing_status: JOINT + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 5_000 + +- name: Joint return one dollar over $200,000 loses one $200 step + period: 2025 + input: + adjusted_gross_income: 200_001 + filing_status: JOINT + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 4_800 From 764d24f9c21ccf7a9f7a14d196fdb461ad8027ba Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 21:14:08 -0400 Subject: [PATCH 2/3] Add property tests for the car loan interest deduction Checks the model against a restatement of Schedule 1-A lines 24-30 under every filing status, the bounds and $200 steps, the rounded-up $1,000 increments at every step, that only a joint return has the $200,000 threshold, and monotonicity in income and interest. Co-Authored-By: Claude Opus 5.5 --- ...auto_loan_interest_deduction_properties.py | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py diff --git a/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py b/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py new file mode 100644 index 00000000000..e243b6b7454 --- /dev/null +++ b/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py @@ -0,0 +1,265 @@ +"""Invariants of the car loan interest deduction (26 U.S.C. 163(h)(4)). + +Section 163(h)(4)(C)(i) limits the qualified passenger vehicle loan interest +taken into account to $10,000. Section 163(h)(4)(C)(ii)(I) then reduces the +deduction "(but not below zero) by $200 for each $1,000 (or portion thereof) +by which the modified adjusted gross income of the taxpayer for the taxable +year exceeds $100,000 ($200,000 in the case of a joint return)". Schedule 1-A +(2025), lines 24-30, restate this: line 26 is "$100,000 ($200,000 if married +filing jointly)", and line 28 divides the excess by $1,000 and, if the result +isn't a whole number, increases it to the next higher whole number. The 2025 +Form 1040 instructions give "All other filing statuses—$100,000", so a +surviving spouse, who does not file a joint return, uses $100,000. Section +163(h)(4) has no joint-return requirement, so separate filers may claim. + +Each test call, and each Hypothesis example, builds one simulation of many tax +units, each in its own household. The tests check: + +1. Seeded draws, evaluated under all five filing statuses, match an + independent restatement of Schedule 1-A lines 24-30. +2. The deduction lies between 0 and the capped interest, and a positive + deduction is the capped interest less a whole multiple of $200. These + follow from 1, and are checked on the model's output directly so that an + error in the restatement's arithmetic cannot hide them. +3. At every $1,000 step from each threshold to full phase-out, for each filing + status: k whole steps over the threshold leave the cap less $200 per step + (not below zero), and $1, $500 or $999 more takes another $200, because a + portion of $1,000 counts as a whole one. One dollar below the threshold the + full cap is left. +4. A single filer, a head of household, a separate filer and a surviving + spouse get the same deduction at every income, and a joint return at + modified AGI m gets what a single filer gets at m - $100,000. +5. Raising modified AGI never raises the deduction, and leaves it unchanged + while the count of $1,000 increments (a portion rounded up) over the + threshold is unchanged; raising the interest never lowers it (Hypothesis). + +Modified AGI here is adjusted gross income with no section 911, 931 or 933 +exclusions; the YAML tests cover the section 933 add-back. Incomes are whole +or quarter dollars, which float32 holds exactly at these magnitudes. +""" + +import numpy as np +import pytest +from hypothesis import Phase, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import system + +YEAR = 2025 +STATUSES = ("SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE") +NON_JOINT = tuple(status for status in STATUSES if status != "JOINT") + +# Schedule 1-A (2025), restated from the form rather than read from the +# parameter files. Line 24: "the smaller of the amount on line 23 or $10,000". +# Line 26: "$100,000 ($200,000 if married filing jointly)". Lines 28-29: each +# $1,000 of excess, a fraction rounded up, times $200. +CAP = 10_000 +THRESHOLD = {"JOINT": 200_000} +DEFAULT_THRESHOLD = 100_000 +INCREMENT = 1_000 +REDUCTION = 200 +# Steps that take the full cap to zero. +STEPS = CAP // REDUCTION + + +def _threshold(status): + return THRESHOLD.get(status, DEFAULT_THRESHOLD) + + +def _increments(magi, status): + """Lines 27-28: whole $1,000s over the threshold, a portion rounded up.""" + # Quarter dollars keep the arithmetic exact. + excess = round(4 * magi) - 4 * _threshold(status) + return max(-(-excess // (4 * INCREMENT)), 0) + + +def _schedule_1a(interest, magi, status): + """Lines 24-30 of Schedule 1-A, restated.""" + line_24 = min(interest, CAP) + line_29 = _increments(magi, status) * REDUCTION + return max(line_24 - line_29, 0) + + +def _simulate(units): + """One tax unit per (status, magi, interest); joint units get a spouse.""" + period = str(YEAR) + people, tax_units, marital_units, households = {}, {}, {}, {} + for i, (status, magi, interest) in enumerate(units): + members = [f"head{i}"] + if status == "JOINT": + members.append(f"spouse{i}") + for name in members: + people[name] = {"age": {period: 40}} + tax_units[f"t{i}"] = { + "members": members, + "filing_status": {period: status}, + "adjusted_gross_income": {period: float(magi)}, + } + marital_units[f"m{i}"] = {"members": members} + households[f"h{i}"] = { + "members": members, + "state_code": {period: "TX"}, + "qualified_passenger_vehicle_loan_interest": {period: float(interest)}, + } + simulation = Simulation( + tax_benefit_system=system, + situation={ + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households, + }, + ) + magi = simulation.calculate("agi_plus_section_911_931_933_exclusions", period) + np.testing.assert_array_equal(magi, [unit[1] for unit in units]) + return simulation.calculate("auto_loan_interest_deduction", period) + + +def _draw(seed, n=120): + """Random (magi, interest) pairs concentrated around each threshold's steps.""" + rng = np.random.default_rng(seed) + draws = [] + for _ in range(n): + kind = rng.integers(3) + threshold = int(rng.choice([DEFAULT_THRESHOLD, THRESHOLD["JOINT"]])) + if kind == 0: + magi = float(rng.integers(0, 400_001)) + elif kind == 1: + step = threshold + INCREMENT * int(rng.integers(0, STEPS + 2)) + magi = float(step + rng.choice([-1, 0, 1, 999, 1_000])) + else: + # Quarter dollars from $2,000 below to $52,000 above. + magi = threshold + int(rng.integers(-8_000, 208_001)) / 4 + interest = float(rng.choice([0, 5_000, CAP, int(rng.integers(0, 15_001))])) + draws.append((magi, interest)) + return draws + + +@pytest.mark.parametrize("seed", [0, 1]) +def test_deduction_matches_schedule_1a_under_every_filing_status(seed): + units = [ + (status, magi, interest) + for magi, interest in _draw(seed) + for status in STATUSES + ] + actual = _simulate(units) + # 1. Schedule 1-A lines 24-30. + expected = [ + _schedule_1a(interest, magi, status) for status, magi, interest in units + ] + np.testing.assert_array_equal(actual, expected) + + # 2. Bounds, and whole $200 reductions from the capped interest. + for unit, value in zip(units, actual): + capped = min(unit[2], CAP) + assert 0 <= value <= capped, (unit, value) + if value > 0: + assert (capped - value) % REDUCTION == 0, (unit, value) + + +def test_each_thousand_dollars_or_portion_takes_two_hundred(): + units, index = [], {} + for status in STATUSES: + threshold = _threshold(status) + index[(status, "below")] = len(units) + units.append((status, threshold - 1, CAP)) + for k in range(STEPS + 2): + for offset in (0, 1, 500, 999): + index[(status, k, offset)] = len(units) + units.append((status, threshold + INCREMENT * k + offset, CAP)) + value = _simulate(units) + + for status in STATUSES: + assert value[index[(status, "below")]] == CAP, status + for k in range(STEPS + 2): + # 3. k whole steps over the threshold take $200 each ... + at_step = value[index[(status, k, 0)]] + assert at_step == max(CAP - REDUCTION * k, 0), (status, k) + # ... and any portion of the next $1,000 takes another $200. + for offset in (1, 500, 999): + assert value[index[(status, k, offset)]] == max( + CAP - REDUCTION * (k + 1), 0 + ), (status, k, offset) + + +def test_only_a_joint_return_has_the_higher_threshold(): + # Incomes of at least $100,000, so a single filer at m - $100,000 has a + # nonnegative AGI. + draws = [ + (magi if magi >= DEFAULT_THRESHOLD else magi + DEFAULT_THRESHOLD, interest) + for magi, interest in _draw(2) + ] + units = [] + for magi, interest in draws: + units.extend((status, magi, interest) for status in STATUSES) + units.append(("SINGLE", magi - DEFAULT_THRESHOLD, interest)) + value = _simulate(units).reshape(len(draws), len(STATUSES) + 1) + by_status = {status: value[:, i] for i, status in enumerate(STATUSES)} + + # 4. The four statuses without a joint return share every amount ... + for status in NON_JOINT: + np.testing.assert_array_equal(by_status[status], by_status["SINGLE"], status) + # ... and a joint return is a single filer $100,000 lower. + np.testing.assert_array_equal(by_status["JOINT"], value[:, -1]) + + +thresholds = st.sampled_from([DEFAULT_THRESHOLD, THRESHOLD["JOINT"]]) +magis = st.one_of( + # On or next to a $1,000 step of either threshold. + st.tuples( + thresholds, + st.integers(min_value=0, max_value=STEPS + 1), + st.sampled_from([-1, 0, 1, 500, 999]), + ).map(lambda t: t[0] + INCREMENT * t[1] + t[2]), + # Anywhere in or near either phase-out range, in quarter dollars. + st.tuples(thresholds, st.integers(min_value=-8_000, max_value=208_000)).map( + lambda t: t[0] + t[1] / 4 + ), + st.integers(min_value=0, max_value=400_000), +) +raises = st.one_of( + st.integers(min_value=0, max_value=999), + st.integers(min_value=0, max_value=4_000).map(lambda quarters: quarters / 4), + st.integers(min_value=0, max_value=60_000), +) +interests = st.one_of( + st.sampled_from([0, 5_000, CAP]), st.integers(min_value=0, max_value=15_000) +) +more_interest = st.integers(min_value=0, max_value=5_000) + + +# Each example is one simulation of 300 tax units, so a few examples keep the +# run short; shrinking is skipped and a failing example is still reported. +@settings( + max_examples=7, + deadline=None, + derandomize=True, + phases=(Phase.explicit, Phase.generate), +) +@given( + cases=st.lists( + st.tuples(magis, raises, interests, more_interest), + min_size=20, + max_size=20, + ) +) +def test_more_income_never_raises_and_more_interest_never_lowers_it(cases): + units, same_step = [], [] + for magi, raise_by, interest, extra in cases: + for status in STATUSES: + units.append((status, magi, interest)) + units.append((status, magi + raise_by, interest)) + units.append((status, magi, interest + extra)) + same_step.append( + _increments(magi, status) == _increments(magi + raise_by, status) + ) + value = _simulate(units) + base, richer, more = value[0::3], value[1::3], value[2::3] + same_step = np.array(same_step) + # 5. Non-increasing in modified AGI, and piecewise constant: equal while + # the rounded-up increments are equal ... + assert np.all(richer <= base) + np.testing.assert_array_equal(richer[same_step], base[same_step]) + # ... and non-decreasing in interest. + assert np.all(more >= base) From 354d6e6cb3b70d1fac9d0b92fcc472d98b0ab937 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 06:26:45 -0400 Subject: [PATCH 3/3] Address review: references, comments and boundary cases - start.yaml also cites 26 CFR 1.163-16(h)(2) (T.D. 10054) and the 2025 Form 1040 instructions, which name "All other filing statuses". - cap, step and increment cite the statute and their Schedule 1-A lines, and their descriptions lose two typos and say modified AGI. - The formula's comments cite Schedule 1-A lines 26-29, not an older worksheet's lines, and its references add the statute and the form. - YAML adds a single filer at $100,001, a surviving spouse whose reduction passes the interest, and 1.163-16(h)(3) Example 2 for single and surviving-spouse filers. - The step walk runs with interest below, at and above the cap. Co-Authored-By: Claude Opus 5.5 --- .../deductions/auto_loan_interest/cap.yaml | 6 ++- .../phase_out/increment.yaml | 8 +-- .../auto_loan_interest/phase_out/start.yaml | 4 ++ .../auto_loan_interest/phase_out/step.yaml | 9 ++-- ...auto_loan_interest_deduction_properties.py | 49 +++++++++++-------- .../auto_loan_interest_deduction.yaml | 41 ++++++++++++++++ .../auto_loan_interest_deduction.py | 10 ++-- 7 files changed, 95 insertions(+), 32 deletions(-) diff --git a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/cap.yaml b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/cap.yaml index 845557f182c..4f46f446c7f 100644 --- a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/cap.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/cap.yaml @@ -8,5 +8,7 @@ metadata: period: year label: Auto loan interest deduction cap reference: - - title: H.R.1 - One Big Beautiful Bill Act - href: https://www.congress.gov/bill/119th-congress/house-bill/1/text + - title: 26 U.S.C. § 163(h)(4)(C)(i) + href: https://www.law.cornell.edu/uscode/text/26/163#h_4_C_i + - title: 2025 Schedule 1-A (Form 1040), line 24 + href: https://www.irs.gov/pub/irs-pdf/f1040s1a.pdf#page=2 diff --git a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/increment.yaml b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/increment.yaml index 4822aef8e46..13de33ee275 100644 --- a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/increment.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/increment.yaml @@ -1,11 +1,13 @@ -description: The IRS deduces the auto loan interest deduction in these increments of adjusted gross income exceeding the threshold. +description: The IRS reduces the auto loan interest deduction for each increment of this size, or portion of one, by which modified adjusted gross income exceeds the threshold. metadata: unit: currency-USD period: year label: Auto loan interest deduction reduction increment reference: - - title: H.R.1 - One Big Beautiful Bill Act - href: https://www.congress.gov/bill/119th-congress/house-bill/1/text + - title: 26 U.S.C. § 163(h)(4)(C)(ii)(I) + href: https://www.law.cornell.edu/uscode/text/26/163#h_4_C_ii_I + - title: 2025 Schedule 1-A (Form 1040), line 28 + href: https://www.irs.gov/pub/irs-pdf/f1040s1a.pdf#page=2 values: 2025-01-01: 1_000 diff --git a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml index 6a56f45df09..32214ec489b 100644 --- a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/start.yaml @@ -9,8 +9,12 @@ metadata: reference: - title: 26 U.S.C. § 163(h)(4)(C)(ii)(I) href: https://www.law.cornell.edu/uscode/text/26/163#h_4_C_ii_I + - title: 26 CFR 1.163-16(h)(2) (T.D. 10054, 91 FR 57214) + href: https://www.govinfo.gov/content/pkg/FR-2026-09-08/pdf/2026-18219.pdf#page=28 - title: 2025 Schedule 1-A (Form 1040), line 26 href: https://www.irs.gov/pub/irs-pdf/f1040s1a.pdf#page=2 + - title: 2025 Form 1040 instructions, No Tax on Car Loan Interest + href: https://www.irs.gov/pub/irs-pdf/i1040gi.pdf#page=109 SINGLE: 2025-01-01: 100_000 diff --git a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/step.yaml b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/step.yaml index 88ac42a66f7..4ae52f98f79 100644 --- a/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/step.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/auto_loan_interest/phase_out/step.yaml @@ -1,12 +1,13 @@ -description: The IRS reduces the auto loan interest deduction at the following rate for each increment in of adjusted gross income above the threshold. +description: The IRS reduces the auto loan interest deduction by this amount for each increment of modified adjusted gross income above the threshold. metadata: unit: currency-USD period: year label: Auto loan interest deduction reduction step reference: - - title: H.R.1 - One Big Beautiful Bill Act - href: https://www.congress.gov/bill/119th-congress/house-bill/1/text + - title: 26 U.S.C. § 163(h)(4)(C)(ii)(I) + href: https://www.law.cornell.edu/uscode/text/26/163#h_4_C_ii_I + - title: 2025 Schedule 1-A (Form 1040), line 29 + href: https://www.irs.gov/pub/irs-pdf/f1040s1a.pdf#page=2 values: 2025-01-01: 200 - diff --git a/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py b/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py index e243b6b7454..2a856842674 100644 --- a/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py +++ b/policyengine_us/tests/core/test_auto_loan_interest_deduction_properties.py @@ -22,10 +22,11 @@ follow from 1, and are checked on the model's output directly so that an error in the restatement's arithmetic cannot hide them. 3. At every $1,000 step from each threshold to full phase-out, for each filing - status: k whole steps over the threshold leave the cap less $200 per step - (not below zero), and $1, $500 or $999 more takes another $200, because a - portion of $1,000 counts as a whole one. One dollar below the threshold the - full cap is left. + status and for interest below, at and above the cap: k whole steps over the + threshold leave the capped interest less $200 per step (not below zero), + and $1, $500 or $999 more takes another $200, because a portion of $1,000 + counts as a whole one. One dollar below the threshold the capped interest + is left. 4. A single filer, a head of household, a separate filer and a surviving spouse get the same deduction at every income, and a joint return at modified AGI m gets what a single filer gets at m - $100,000. @@ -159,28 +160,36 @@ def test_deduction_matches_schedule_1a_under_every_filing_status(seed): def test_each_thousand_dollars_or_portion_takes_two_hundred(): + # Interest below, at and above the cap, so the walk also sees the cap and + # that it applies before the reduction. + interests = (5_000, CAP, 12_000) units, index = [], {} for status in STATUSES: threshold = _threshold(status) - index[(status, "below")] = len(units) - units.append((status, threshold - 1, CAP)) - for k in range(STEPS + 2): - for offset in (0, 1, 500, 999): - index[(status, k, offset)] = len(units) - units.append((status, threshold + INCREMENT * k + offset, CAP)) + for interest in interests: + index[(status, interest, "below")] = len(units) + units.append((status, threshold - 1, interest)) + for k in range(STEPS + 2): + for offset in (0, 1, 500, 999): + index[(status, interest, k, offset)] = len(units) + magi = threshold + INCREMENT * k + offset + units.append((status, magi, interest)) value = _simulate(units) for status in STATUSES: - assert value[index[(status, "below")]] == CAP, status - for k in range(STEPS + 2): - # 3. k whole steps over the threshold take $200 each ... - at_step = value[index[(status, k, 0)]] - assert at_step == max(CAP - REDUCTION * k, 0), (status, k) - # ... and any portion of the next $1,000 takes another $200. - for offset in (1, 500, 999): - assert value[index[(status, k, offset)]] == max( - CAP - REDUCTION * (k + 1), 0 - ), (status, k, offset) + for interest in interests: + capped = min(interest, CAP) + key = (status, interest) + assert value[index[(*key, "below")]] == capped, key + for k in range(STEPS + 2): + # 3. k whole steps over the threshold take $200 each ... + at_step = value[index[(*key, k, 0)]] + assert at_step == max(capped - REDUCTION * k, 0), (*key, k) + # ... and any portion of the next $1,000 takes another $200. + for offset in (1, 500, 999): + assert value[index[(*key, k, offset)]] == max( + capped - REDUCTION * (k + 1), 0 + ), (*key, k, offset) def test_only_a_joint_return_has_the_higher_threshold(): diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml index c28c9784852..0ed3bb4d843 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/auto_loan_interest_deduction.yaml @@ -101,6 +101,47 @@ # Capped at $10,000, less 50 steps of $200. auto_loan_interest_deduction: 0 +- name: Surviving spouse reduction past the interest stops at zero + period: 2025 + input: + adjusted_gross_income: 160_000 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + # 60 steps of $200 is $12,000, more than the $5,000 of interest. + auto_loan_interest_deduction: 0 + +- name: Single filer one dollar over $100,000 loses one $200 step + period: 2025 + input: + adjusted_gross_income: 100_001 + filing_status: SINGLE + qualified_passenger_vehicle_loan_interest: 5_000 + output: + auto_loan_interest_deduction: 4_800 + +# 26 CFR 1.163-16(h)(3), Example 2 (T.D. 10054): a single filer with $7,000 +# of interest and modified AGI of $124,200 is $24,200 over, which "is then +# rounded up to 25" steps, so the $7,000 is reduced by $5,000. +- name: Regulation example 2, single filer + period: 2025 + input: + adjusted_gross_income: 124_200 + filing_status: SINGLE + qualified_passenger_vehicle_loan_interest: 7_000 + output: + auto_loan_interest_deduction: 2_000 + +- name: Regulation example 2 facts for a surviving spouse + period: 2025 + input: + adjusted_gross_income: 124_200 + filing_status: SURVIVING_SPOUSE + qualified_passenger_vehicle_loan_interest: 7_000 + output: + # The same $100,000 threshold applies. + auto_loan_interest_deduction: 2_000 + - name: Head of household one dollar over $100,000 loses one $200 step period: 2025 input: diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/auto_loan_interest/auto_loan_interest_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/auto_loan_interest/auto_loan_interest_deduction.py index c12ab856515..85d306125ac 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/auto_loan_interest/auto_loan_interest_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/auto_loan_interest/auto_loan_interest_deduction.py @@ -8,6 +8,8 @@ class auto_loan_interest_deduction(Variable): unit = USD definition_period = YEAR reference = ( + "https://www.law.cornell.edu/uscode/text/26/163#h_4", + "https://www.irs.gov/pub/irs-pdf/f1040s1a.pdf#page=2", "https://www.congress.gov/bill/119th-congress/house-bill/1/text", "https://www.irs.gov/taxtopics/tc505", "https://www.irs.gov/pub/irs-pdf/p6126.pdf", @@ -24,16 +26,18 @@ def formula(tax_unit, period, parameters): # Get filing status. filing_status = tax_unit("filing_status", period) - # Get the phaseout start amount based on filing status (line 4). + # Get the phaseout start amount based on filing status (Schedule 1-A, + # line 26). phaseout_start = p.phase_out.start[filing_status] # 26 U.S.C. 163(h)(4)(C)(ii)(II): modified adjusted gross income adds # back income excluded under sections 911, 931 and 933 (Schedule 1-A, # line 3). magi = tax_unit("agi_plus_section_911_931_933_exclusions", period) - # Get the excess amount, if any, in thousands of dollars (rounded up) [lines 5 and 6]. + # Get the excess amount, if any, in thousands of dollars (rounded up) + # [lines 27 and 28]. excess = max_(magi - phaseout_start, 0) increments = np.ceil(excess / p.phase_out.increment) - # Calculate the excess part phase out amount (line 7). + # Calculate the excess part phase out amount (line 29). phase_out_amount = increments * p.phase_out.step return max_(capped_interest - phase_out_amount, 0)