diff --git a/changelog.d/vt-ut-wi-statutory-fixes.fixed.md b/changelog.d/vt-ut-wi-statutory-fixes.fixed.md new file mode 100644 index 00000000000..964bf73c319 --- /dev/null +++ b/changelog.d/vt-ut-wi-statutory-fixes.fixed.md @@ -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. diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/in_effect.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/in_effect.yaml new file mode 100644 index 00000000000..4bdfb8f38a8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/in_effect.yaml @@ -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 diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml new file mode 100644 index 00000000000..0924e29f038 --- /dev/null +++ b/policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml @@ -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 diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/agi/subtractions.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/agi/subtractions.yaml index 720da356559..9005a8af60c 100644 --- a/policyengine_us/parameters/gov/states/vt/tax/income/agi/subtractions.yaml +++ b/policyengine_us/parameters/gov/states/vt/tax/income/agi/subtractions.yaml @@ -3,7 +3,15 @@ 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 @@ -11,6 +19,8 @@ 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 diff --git a/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py b/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py new file mode 100644 index 00000000000..dfe970fadbb --- /dev/null +++ b/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py @@ -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() diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_at_home_parent_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_at_home_parent_credit.yaml index b582eaadba7..30564d32d06 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_at_home_parent_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_at_home_parent_credit.yaml @@ -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: @@ -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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml new file mode 100644 index 00000000000..7741ba0d445 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml @@ -0,0 +1,157 @@ +- name: 2025 interest fully deducted federally leaves no Vermont student loan interest subtraction + period: 2025 + input: + people: + filer: + age: 24 + employment_income: 10_000 + student_loan_interest: 1_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: VT + output: + # Schedule IN-112 line 16: $1,000 paid (16a) - $1,000 deducted federally + # (16b) = $0 (16c). Federal AGI is already $9,000; subtracting the raw + # interest again would deduct it twice. + student_loan_interest_ald: 1_000 + vt_student_loan_interest_subtraction: 0 + vt_agi: 9_000 + +- name: 2025 interest above the $2,500 federal cap leaves the excess for the Vermont subtraction + period: 2025 + input: + people: + filer: + age: 30 + employment_income: 50_000 + student_loan_interest: 4_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: VT + output: + # IN-112 line 16c: $4,000 - $2,500 = $1,500. + student_loan_interest_ald: 2_500 + vt_student_loan_interest_subtraction: 1_500 + +- name: 2025 single filer with adjusted gross income above $120,000 gets no Vermont student loan interest subtraction + period: 2025 + input: + people: + filer: + age: 30 + employment_income: 125_000 + student_loan_interest: 3_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: VT + output: + # 32 V.S.A. 5811(29)(B)(i): AGI at most $120,000 unless married filing + # jointly. The federal deduction phased out at $100,000 of MAGI. + student_loan_interest_ald: 0 + vt_student_loan_interest_subtraction: 0 + +- name: 2025 joint return under $200,000 subtracts the interest left after the federal phase-out + period: 2025 + input: + people: + head: + age: 30 + employment_income: 195_000 + student_loan_interest: 3_000 + spouse: + age: 30 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: VT + output: + # Federal: $2,500 x (1 - ($195,000 - $170,000) / $30,000) = $416.67. + # AGI $194,583 is within the $200,000 joint limit: $3,000 - $416.67. + student_loan_interest_ald: [416.6667, 0] + vt_student_loan_interest_subtraction: 2_583.3333 + +- name: 2025 joint return over $200,000 gets no Vermont student loan interest subtraction + period: 2025 + input: + people: + head: + age: 30 + employment_income: 205_000 + student_loan_interest: 3_000 + spouse: + age: 30 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: VT + output: + # 5811(29)(B)(ii): AGI at most $200,000 if married filing jointly. + student_loan_interest_ald: [0, 0] + vt_student_loan_interest_subtraction: 0 + +- name: 2021 Vermont has no student loan interest subtraction + period: 2021 + input: + people: + filer: + age: 24 + employment_income: 10_000 + student_loan_interest: 1_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: VT + output: + # 2022 Act 138 added 5811(21)(B)(vi) from 2022; the 2021 Schedule IN-112 + # has no such line. + student_loan_interest_ald: 1_000 + vt_student_loan_interest_subtraction: 0 + vt_subtractions: 0 + vt_agi: 9_000 + +- name: 2021 interest above the federal cap is still not subtracted in Vermont + period: 2021 + input: + people: + filer: + age: 30 + employment_income: 50_000 + student_loan_interest: 4_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: VT + output: + # The subtraction took effect on January 1, 2022 (2022 Act 138, Sec. 13), so + # the $1,500 the federal cap leaves is not subtracted for 2021. + student_loan_interest_ald: 2_500 + vt_student_loan_interest_subtraction: 0 + vt_subtractions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/vt_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/vt_ctc.yaml index 757e825b1e3..f3f4e3c3ccf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/vt_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/vt_ctc.yaml @@ -1,3 +1,26 @@ +- name: Two children halfway phased out at $150,000 each lose $500 + period: 2023 + input: + people: + parent: + age: 35 + child1: + age: 3 + child2: + age: 3 + tax_units: + tax_unit: + members: [parent, child1, child2] + adjusted_gross_income: 150_000 + households: + household: + members: [parent, child1, child2] + state_code: VT + output: + # 32 V.S.A. 5830f(b): "the amount of the credit per child" is reduced by + # $20 per $1,000 over $125,000: 2 x ($1,000 - 25 x $20) = $1,000. + vt_ctc: 1_000 + - name: Five qualifying children. period: 2023 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_eligible.yaml index 4802db8710f..614c0f0b5ac 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_eligible.yaml @@ -1,97 +1,386 @@ - name: wi_homestead_eligible unit test 1 - absolute_error_margin: 0.01 period: 2021 input: - age_head: 18 - age_spouse: 17 - employment_income: 8_000 - head_is_disabled: false - spouse_is_disabled: false - wi_homestead_income: 9_000 - state_code: WI + people: + head: + age: 18 + employment_income: 8000 + spouse: + age: 17 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: true - name: wi_homestead_eligible unit test 2 - absolute_error_margin: 0.01 period: 2021 input: - age_head: 17 - age_spouse: 17 - employment_income: 8_000 - head_is_disabled: false - spouse_is_disabled: false - wi_homestead_income: 9_000 - state_code: WI + people: + head: + age: 17 + employment_income: 8000 + spouse: + age: 17 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: false - name: wi_homestead_eligible unit test 3 - absolute_error_margin: 0.01 period: 2021 input: - age_head: 18 - age_spouse: 17 - employment_income: 0 - head_is_disabled: false - spouse_is_disabled: false - wi_homestead_income: 9_000 - state_code: WI + people: + head: + age: 18 + employment_income: 0 + spouse: + age: 17 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: false - name: wi_homestead_eligible unit test 4 - absolute_error_margin: 0.01 period: 2021 input: - age_head: 62 - age_spouse: 60 - employment_income: 0 - head_is_disabled: false - spouse_is_disabled: false - wi_homestead_income: 9_000 - state_code: WI + people: + head: + age: 62 + employment_income: 0 + spouse: + age: 60 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: true - name: wi_homestead_eligible unit test 5 - absolute_error_margin: 0.01 period: 2022 input: - age_head: 61 - age_spouse: 60 - employment_income: 0 - head_is_disabled: false - spouse_is_disabled: false - wi_homestead_income: 9_000 - state_code: WI + people: + head: + age: 61 + employment_income: 0 + spouse: + age: 60 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: false - name: wi_homestead_eligible unit test 6 - absolute_error_margin: 0.01 period: 2022 input: - age_head: 61 - age_spouse: 60 - employment_income: 0 - head_is_disabled: false - spouse_is_disabled: true - wi_homestead_income: 9_000 - state_code: WI + people: + head: + age: 61 + employment_income: 0 + spouse: + age: 60 + is_disabled: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: true - name: wi_homestead_eligible unit test 7 - absolute_error_margin: 0.01 period: 2021 input: - age_head: 18 - age_spouse: 17 - employment_income: 8_000 - head_is_disabled: false - spouse_is_disabled: false - wi_homestead_income: 25_000 - state_code: WI + people: + head: + age: 18 + employment_income: 8000 + spouse: + age: 17 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 25000 + households: + household: + members: [head, spouse] + state_code: WI output: wi_homestead_eligible: false + +- name: "2025 single filer claimed as a dependent: Wisconsin homestead credit needs a claimant not claimed as a dependent unless 62 or older" + period: 2025 + input: + people: + filer: + age: 30 + employment_income: 1000 + real_estate_taxes: 1000 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: WI + output: + # Wis. Stat. 71.53(2)(d): no claim by a claimant claimed as a dependent + # by another person, unless the claimant is 62 or older. + wi_homestead_eligible: false + wi_homestead_credit: 0 + +- name: "2025 joint return with one spouse claimed as a dependent: Wisconsin homestead credit needs a claimant not claimed as a dependent unless 62 or older (the claimed spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: WI + output: + # The unclaimed spouse may be the claimant (Tax 14.02). Household + # income $1,000 is below the $8,060 phaseout start; property tax + # $1,000 x 80% = $800. + wi_homestead_eligible: true + wi_homestead_credit: 800 + +- name: "2025 joint return with one spouse claimed as a dependent: Wisconsin homestead credit needs a claimant not claimed as a dependent unless 62 or older (the claimed spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: WI + output: + wi_homestead_eligible: true + wi_homestead_credit: 800 + +- name: "2025 joint return with both spouses claimed as dependents: Wisconsin homestead credit needs a claimant not claimed as a dependent unless 62 or older" + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: WI + output: + wi_homestead_eligible: false + wi_homestead_credit: 0 + +- name: "2025 joint return with neither spouse claimed: Wisconsin homestead credit needs a claimant not claimed as a dependent unless 62 or older" + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + employment_income: 500 + real_estate_taxes: 500 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: WI + output: + # Same as above: $1,000 x 80% = $800. + wi_homestead_eligible: true + wi_homestead_credit: 800 + +- name: "2025 joint return with both spouses claimed as dependents: Wisconsin homestead credit allows a claimant aged 62 or older even if claimed" + period: 2025 + input: + people: + person_a: + age: 62 + employment_income: 500 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 62 + employment_income: 500 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: WI + output: + # The age-62 exception keeps both claimed spouses eligible: $800. + wi_homestead_eligible: true + wi_homestead_credit: 800 + +- name: "2025 joint return where only the claimed spouse has earned income: no Wisconsin homestead claimant (the claimed spouse is the head)" + period: 2025 + input: + people: + person_a: + age: 30 + employment_income: 1000 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 30 + real_estate_taxes: 500 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_a, person_b] + filing_status: JOINT + households: + household: + members: [person_a, person_b] + state_code: WI + output: + # Schedule H instructions: the spouse with earned income must be the + # claimant, and a claimant under 62 may not be claimed as a dependent; + # the unclaimed spouse has no earned income and is not 62 or disabled. + wi_homestead_eligible: false + wi_homestead_credit: 0 + +- name: "2025 joint return where only the claimed spouse has earned income: no Wisconsin homestead claimant (the claimed spouse is the spouse)" + period: 2025 + input: + people: + person_b: + age: 30 + real_estate_taxes: 500 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 30 + employment_income: 1000 + real_estate_taxes: 500 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person_b, person_a] + filing_status: JOINT + households: + household: + members: [person_b, person_a] + state_code: WI + output: + wi_homestead_eligible: false + wi_homestead_credit: 0 diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/ut_at_home_parent_credit_potential.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/ut_at_home_parent_credit_potential.py index 6e18a6211b7..80b761683ac 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/ut_at_home_parent_credit_potential.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/ut_at_home_parent_credit_potential.py @@ -10,6 +10,7 @@ class ut_at_home_parent_credit_potential(Variable): defined_for = "ut_at_home_parent_credit_agi_eligible" reference = ( "https://le.utah.gov/xcode/Title59/Chapter10/59-10-S1005.html", + "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1005_2026050620260506.pdf#page=1", "https://www.taxformfinder.org/forms/2021/2021-utah-tc-40-full-packet.pdf#page=23", ) @@ -21,11 +22,17 @@ def formula(tax_unit, period, parameters): qualifying_child = (age < p.max_child_age) & is_dependent count_qualifying_children = tax_unit.sum(qualifying_child) - # Multiply by each qualifying parent; they can claim it separately. - income_eligible_person = add( - tax_unit, - period, - ["ut_at_home_parent_credit_earned_income_eligible_person"], + # 59-10-1005(2): $100 "for each qualifying child if ... the claimant or + # another claimant filing a joint individual income tax return with + # the claimant is an at-home parent", so one credit per child on the + # return however many spouses qualify. + has_at_home_parent = ( + add( + tax_unit, + period, + ["ut_at_home_parent_credit_earned_income_eligible_person"], + ) + > 0 ) - return p.amount * count_qualifying_children * income_eligible_person + return p.amount * count_qualifying_children * has_at_home_parent diff --git a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py new file mode 100644 index 00000000000..b5648dec8d0 --- /dev/null +++ b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class vt_student_loan_interest_subtraction(Variable): + value_type = float + entity = TaxUnit + label = "Vermont student loan interest subtraction" + unit = USD + definition_period = YEAR + reference = ( + "https://legislature.vermont.gov/statutes/section/32/151/05811", + # 2022 Act 138, Sec. 5 (the subtraction) and Sec. 13 (effective date) + # PDF pages 4-5, 15 + "https://legislature.vermont.gov/Documents/2022/Docs/ACTS/ACT138/ACT138%20As%20Enacted.pdf#page=4", + # Schedule IN-112 lines 16a-16c, form and instructions + # PDF pages 23, 33 + "https://taxsim.nber.org/historical_state_tax_forms/VT/2025/Income-Booklet-2025.pdf#page=23", + ) + defined_for = StateCode.VT + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.vt.tax.income.agi.student_loan_interest + # 32 V.S.A. 5811(21)(B)(vi) subtracts the interest a qualified resident + # taxpayer paid on a qualified education loan. Schedule IN-112 line 16 + # subtracts the interest paid (16a) less the amount already deducted + # on federal Schedule 1 (16b), so interest is not deducted twice. The + # subtraction has no dependency test of its own: a filer who is + # claimed elsewhere loses the federal deduction (IRC 221(c)), and + # Vermont then subtracts the whole amount. + person = tax_unit.members + filer = person("is_tax_unit_head_or_spouse", period) + interest_paid = tax_unit.sum(filer * person("student_loan_interest", period)) + federal_deduction = add(tax_unit, period, ["student_loan_interest_ald"]) + remaining_interest = max_(interest_paid - federal_deduction, 0) + # 5811(29)(B): a qualified resident taxpayer's adjusted gross income is + # at most $200,000 if married filing jointly and $120,000 otherwise. + agi = tax_unit("adjusted_gross_income", period) + filing_status = tax_unit("filing_status", period) + income_eligible = agi <= p.income_limit[filing_status] + # 2022 Act 138 took effect on January 1, 2022; Vermont had no such + # subtraction before. + return p.in_effect * income_eligible * remaining_interest diff --git a/policyengine_us/variables/gov/states/vt/tax/income/credits/ctc/vt_ctc.py b/policyengine_us/variables/gov/states/vt/tax/income/credits/ctc/vt_ctc.py index 6d58a34dfe1..00513b318e9 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/credits/ctc/vt_ctc.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/credits/ctc/vt_ctc.py @@ -17,13 +17,12 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.vt.tax.income.credits.ctc eligible = age <= p.age_limit count_eligible = tax_unit.sum(eligible) - # Get maximum credit amount. - max_credit = p.amount * count_eligible # Get adjusted gross income. agi = tax_unit("adjusted_gross_income", period) - # Reduce credit amount over the phaseout range. + # 5830f(b): "the amount of the credit per child" is reduced, but not + # below zero, by $20 for each $1,000 of AGI over $125,000. excess_agi = max_(agi - p.reduction.start, 0) increments = np.ceil(excess_agi / p.reduction.increment) - total_reduction = p.reduction.amount * increments - # Return reduced credit amount. - return max_(max_credit - total_reduction, 0) + reduction_per_child = p.reduction.amount * increments + credit_per_child = max_(p.amount - reduction_per_child, 0) + return count_eligible * credit_per_child diff --git a/policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py b/policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py index c616d3e4e0e..a99a39f3a0d 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py @@ -6,29 +6,34 @@ class wi_homestead_eligible(Variable): entity = TaxUnit label = "Wisconsin homestead credit eligibility status" definition_period = YEAR - reference = "https://docs.legis.wisconsin.gov/misc/lfb/informational_papers/january_2021/0013_homestead_tax_credit_informational_paper_13.pdf#page=7" + reference = ( + "https://docs.legis.wisconsin.gov/misc/lfb/informational_papers/january_2021/0013_homestead_tax_credit_informational_paper_13.pdf#page=7", + # Schedule H instructions, Steps 1 and 2 and questions 1c-1d + # PDF pages 4, 10 + "https://www.revenue.wi.gov/TaxForms2025/2025-ScheduleH-inst.pdf#page=4", + ) defined_for = StateCode.WI def formula(tax_unit, period, parameters): - p = parameters(period).gov.states.wi.tax.income.credits - # minimum age eligibility - head_age = tax_unit("age_head", period) - spouse_age = tax_unit("age_spouse", period) - min_age = p.homestead.eligible.min_age - age_eligible = (head_age >= min_age) | (spouse_age >= min_age) - # earnings eligibility - earnings = add(tax_unit, period, p.homestead.eligible.earnings_sources) - has_positive_earnings = earnings > 0 - min_elderly_age = p.homestead.eligible.min_elderly_age - head_is_elderly = head_age >= min_elderly_age - spouse_is_elderly = spouse_age >= min_elderly_age - is_elderly = head_is_elderly | spouse_is_elderly - head_is_disabled = tax_unit("head_is_disabled", period) - spouse_is_disabled = tax_unit("spouse_is_disabled", period) - is_disabled = head_is_disabled | spouse_is_disabled - earnings_eligible = has_positive_earnings | is_elderly | is_disabled + p = parameters(period).gov.states.wi.tax.income.credits.homestead.eligible + person = tax_unit.members + filer = person("is_tax_unit_head_or_spouse", period) + age = person("age", period) + elderly = age >= p.min_elderly_age + # The claimant must be 18 or older and must be 62 or older, disabled + # or have earned income; the Schedule H instructions add, for a + # married couple, "The spouse that meets this definition must be the + # claimant." + earnings = add(person, period, p.earnings_sources) + disabled = person("is_disabled", period) + qualifies = (age >= p.min_age) & (elderly | disabled | (earnings > 0)) + # Wis. Stat. 71.53(2)(d) and Schedule H Step 2a: no claim by a + # claimant who was or will be claimed as a dependent on another + # person's federal return, unless the claimant is 62 or older. + claimed = person("claimed_as_dependent_on_another_return", period) + eligible_claimant = filer & qualifies & (~claimed | elderly) # income eligibility homestead_income = tax_unit("wi_homestead_income", period) - income_eligible = homestead_income < p.homestead.eligible.max_income + income_eligible = homestead_income < p.max_income # overall eligibility - return age_eligible & earnings_eligible & income_eligible + return tax_unit.any(eligible_claimant) & income_eligible