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/vt-ut-wi-statutory-fixes.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Subtract only the Vermont student loan interest not already deducted federally, from 2022 and within the income limits; phase out Vermont's child tax credit per child; pay Utah's at-home parent credit once per qualifying child per return; and test the Wisconsin homestead credit's age, earned income, disability and dependency conditions on the claimant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
description: Vermont subtracts student loan interest not deducted federally if this is true.
values:
0000-01-01: false
2022-01-01: true
metadata:
unit: bool
period: year
label: Vermont student loan interest subtraction in effect
reference:
- title: "2022 Act 138, Sec. 5 and Sec. 13 (effective retroactively on January 1, 2022) #page 4-5,15"
href: https://legislature.vermont.gov/Documents/2022/Docs/ACTS/ACT138/ACT138%20As%20Enacted.pdf#page=4
- title: 32 V.S.A. 5811(21)(B)(vi)
href: https://legislature.vermont.gov/statutes/section/32/151/05811
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
description: Vermont allows the student loan interest subtraction only to filers with federal adjusted gross income at or below this amount, by filing status.
metadata:
propagate_metadata_to_children: true
unit: currency-USD
period: year
label: Vermont student loan interest subtraction income limit
breakdown:
- filing_status
reference:
- title: 32 V.S.A. 5811(29)(B)
href: https://legislature.vermont.gov/statutes/section/32/151/05811
- title: "2022 Act 138, Sec. 5 and Sec. 13 (effective retroactively on January 1, 2022) #page 4-5,15"
href: https://legislature.vermont.gov/Documents/2022/Docs/ACTS/ACT138/ACT138%20As%20Enacted.pdf#page=4
- title: 2025 Vermont income tax booklet, Schedule IN-112 line 16c
href: https://taxsim.nber.org/historical_state_tax_forms/VT/2025/Income-Booklet-2025.pdf#page=33
SINGLE:
2022-01-01: 120_000
HEAD_OF_HOUSEHOLD:
2022-01-01: 120_000
SEPARATE:
2022-01-01: 120_000
SURVIVING_SPOUSE:
# IN-112 line 16c: "All other filers, if AGI is greater than $120,000, enter -0-."
2022-01-01: 120_000
JOINT:
2022-01-01: 200_000
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ values:
2021-01-01:
- us_govt_interest
- vt_medical_expense_deduction
- student_loan_interest
- vt_capital_gains_exclusion
- vt_retirement_income_exemption
- salt_refund_income
# 2022 Act 138 added the student loan interest subtraction from 2022
# (32 V.S.A. 5811(21)(B)(vi)); the 2021 Schedule IN-112 has no such line.
2022-01-01:
- us_govt_interest
- vt_medical_expense_deduction
- vt_student_loan_interest_subtraction
- vt_capital_gains_exclusion
- vt_retirement_income_exemption
- salt_refund_income
metadata:
unit: list
label: Vermont subtractions
reference:
- title: 2025 SCHEDULE IN-112 # Line 16a-16c, student loan interest
href: https://taxsim.nber.org/historical_state_tax_forms/VT/2025/Income-Booklet-2025.pdf#page=23
- title: 2024 SCHEDULE IN-112 # PART 1 SUBTRACTIONS FROM FEDERAL ADJUSTED GROSS INCOME
href: https://tax.vermont.gov/sites/tax/files/documents/IN-112-2024.pdf#page=1
- title: 2023 SCHEDULE IN-112 # PART 1 SUBTRACTIONS FROM FEDERAL ADJUSTED GROSS INCOME
Expand Down
221 changes: 221 additions & 0 deletions policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
"""Invariants of four Vermont, Utah and Wisconsin rules.

1. Vermont student loan interest (32 V.S.A. 5811(21)(B)(vi), (29); IN-112
line 16): from 2022, the state subtraction plus the federal deduction equals
the filers' interest paid when AGI is within the limit, and the subtraction
is zero above the limit, in 2021, and never negative.
2. Vermont child tax credit (5830f(b)): the phase-out applies per child, so the
credit for n young children is n times the credit for one at the same AGI.
3. Utah at-home parent credit (59-10-1005(2)): a second spouse who also
qualifies as an at-home parent does not change the credit.
4. Wisconsin homestead eligibility: the same under either head/spouse
labelling, and marking a spouse as claimed never makes a return eligible.

Each check is one vectorized simulation over Hypothesis-drawn households.
"""

import numpy as np
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st

from policyengine_us import Simulation

SETTINGS = settings(
max_examples=3,
deadline=None,
derandomize=True,
suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
)


def _situation(units, year, state):
"""units: list of lists of person dicts; the first person is the head,
the second (if flagged) the spouse, the rest dependents."""
people, groups = (
{},
{
k: {}
for k in (
"tax_units",
"spm_units",
"families",
"marital_units",
"households",
)
},
)
for i, unit in enumerate(units):
members = []
for j, attrs in enumerate(unit):
name = f"p_{i}_{j}"
people[name] = {k: {year: v} for k, v in attrs.items()}
members.append(name)
filers = [
m
for m, a in zip(members, unit)
if a.get("is_tax_unit_head") or a.get("is_tax_unit_spouse")
]
groups["marital_units"][f"m_{i}"] = {"members": filers or members[:1]}
for j, m in enumerate(members):
if m not in (filers or members[:1]):
groups["marital_units"][f"m_{i}_{j}"] = {"members": [m]}
for key, prefix in (("tax_units", "t"), ("spm_units", "s"), ("families", "f")):
groups[key][f"{prefix}_{i}"] = {"members": members}
groups["households"][f"h_{i}"] = {
"members": members,
"state_code": {year: state},
}
return {"people": people, **groups}


def _calc(sim, name, year):
return np.asarray(sim.calculate(name, year), dtype=float)


@st.composite
def borrowers(draw):
joint = draw(st.booleans())
head = {
"age": 30,
"is_tax_unit_head": True,
"employment_income": float(draw(st.integers(0, 250_000))),
"student_loan_interest": float(draw(st.integers(0, 6_000))),
}
unit = [head]
if joint:
unit.append(
{
"age": 30,
"is_tax_unit_spouse": True,
"employment_income": float(draw(st.integers(0, 60_000))),
"student_loan_interest": float(draw(st.integers(0, 3_000))),
}
)
return unit


@SETTINGS
@given(
st.sampled_from([2021, 2022, 2024, 2026]),
st.lists(borrowers(), min_size=8, max_size=20),
)
def test_vt_student_loan_subtraction_completes_the_federal_deduction(year, units):
sim = Simulation(situation=_situation(units, year, "VT"))
paid = np.array([sum(p["student_loan_interest"] for p in u) for u in units])
federal = np.asarray(
sim.calculate("student_loan_interest_ald", year, map_to="tax_unit")
)
sub = _calc(sim, "vt_student_loan_interest_subtraction", year)
agi = _calc(sim, "adjusted_gross_income", year)
joint = np.array([len(u) == 2 for u in units])
limit = np.where(joint, 200_000, 120_000)
assert (sub >= 0).all()
if year < 2022:
assert not sub.any()
return
within = agi <= limit
np.testing.assert_allclose(sub[within] + federal[within], paid[within], atol=0.01)
assert not sub[~within].any()


@SETTINGS
@given(
st.sampled_from([2023, 2025, 2026]),
st.lists(st.integers(0, 260_000), min_size=6, max_size=15),
)
def test_vt_ctc_phase_out_is_per_child(year, agis):
units = []
for agi in agis:
for n in (1, 3):
units.append(
[{"age": 35, "is_tax_unit_head": True}] + [{"age": 2} for _ in range(n)]
)
situation = _situation(units, year, "VT")
for i, agi in enumerate(np.repeat(agis, 2)):
situation["tax_units"][f"t_{i}"]["adjusted_gross_income"] = {year: float(agi)}
sim = Simulation(situation=situation)
ctc = _calc(sim, "vt_ctc", year).reshape(-1, 2)
np.testing.assert_allclose(ctc[:, 1], 3 * ctc[:, 0], atol=0.01)


@SETTINGS
@given(
st.sampled_from([2022, 2024, 2025, 2026]),
st.lists(
st.tuples(st.integers(0, 3_000), st.integers(0, 3_000), st.integers(1, 3)),
min_size=6,
max_size=15,
),
)
def test_ut_at_home_parent_credit_once_per_return(year, draws):
units = []
for head_earnings, spouse_earnings, newborns in draws:
kids = [{"age": 0} for _ in range(newborns)]
# The spouse qualifies in the first copy (earnings <= $3,000) and not
# in the second ($10,000), with total income kept under $50,000.
for spouse_pay in (float(spouse_earnings), 10_000.0):
units.append(
[
{
"age": 30,
"is_tax_unit_head": True,
"employment_income": float(head_earnings),
},
{
"age": 30,
"is_tax_unit_spouse": True,
"employment_income": spouse_pay,
},
]
+ kids
)
sim = Simulation(situation=_situation(units, year, "UT"))
credit = _calc(sim, "ut_at_home_parent_credit_potential", year).reshape(-1, 2)
np.testing.assert_allclose(credit[:, 0], credit[:, 1], atol=0.01)


@st.composite
def homestead_couples(draw):
def adult():
return {
"age": draw(st.integers(16, 80)),
"is_disabled": draw(st.booleans()),
"employment_income": float(draw(st.sampled_from([0, 0, 500, 3_000]))),
"real_estate_taxes": float(draw(st.integers(0, 1_500))),
}

return adult(), adult()


@SETTINGS
@given(
st.sampled_from([2021, 2023, 2025]),
st.lists(homestead_couples(), min_size=6, max_size=12),
)
def test_wi_homestead_eligibility_is_label_free_and_monotone(year, couples):
# Per couple: labels (a head, b head) x claims (none, a, b, both).
units = []
for a, b in couples:
for claims in [(False, False), (True, False), (False, True), (True, True)]:
for a_is_head in (True, False):
pa = {**a, "claimed_as_dependent_on_another_return": claims[0]}
pb = {**b, "claimed_as_dependent_on_another_return": claims[1]}
first, second = (pa, pb) if a_is_head else (pb, pa)
units.append(
[
{**first, "is_tax_unit_head": True},
{**second, "is_tax_unit_spouse": True},
]
)
sim = Simulation(situation=_situation(units, year, "WI"))
eligible = _calc(sim, "wi_homestead_eligible", year).reshape(-1, 4, 2)
np.testing.assert_array_equal(eligible[:, :, 0], eligible[:, :, 1])
e = eligible[:, :, 0]
none, a_claimed, b_claimed, both = e.T
for more, fewer in (
(a_claimed, none),
(b_claimed, none),
(both, a_claimed),
(both, b_claimed),
):
assert not (more > fewer).any()
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
state_code: UT
output:
ut_at_home_parent_credit: 0
- name: 2 eligible parents with 1 child
- name: 1 eligible parent with 2 children
period: 2022
input:
people:
Expand Down Expand Up @@ -123,4 +123,35 @@
- person4
state_code: UT
output:
ut_at_home_parent_credit: 400
# Utah Code 59-10-1005(2): $100 for each qualifying child when the claimant
# or a joint-filing spouse is an at-home parent, once per return.
ut_at_home_parent_credit: 200

- name: 2025 two at-home parents with one newborn get one credit for the child
period: 2025
input:
people:
head:
age: 30
employment_income: 2_000
spouse:
age: 30
employment_income: 1_000
is_tax_unit_spouse: true
newborn:
age: 0
tax_units:
tax_unit:
members: [head, spouse, newborn]
filing_status: JOINT
ut_income_tax_before_non_refundable_credits: 1000000
households:
household:
members: [head, spouse, newborn]
state_code: UT
output:
# Utah Code 59-10-1005(2): $100 for each qualifying child if the claimant
# or a joint-filing spouse is an at-home parent; both spouses earning
# $3,000 or less does not double it.
ut_at_home_parent_credit_potential: 100
ut_at_home_parent_credit: 100
Loading