From a4bb6988365867fb10cdb07a792223528fe9b1b6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:21:45 -0400 Subject: [PATCH 01/12] Subtract only the Vermont student loan interest not deducted federally 32 V.S.A. 5811(21)(B)(vi) (2022 Act 138, from 2022) subtracts the interest a qualified resident taxpayer paid on a qualified education loan, for AGI up to $200,000 joint or $120,000 otherwise (5811(29)(B)). Schedule IN-112 line 16 subtracts the interest paid (16a) less the amount "already deducted on federal Form 1040, Schedule 1" (16b). The model subtracted the raw interest from 2021, deducting it a second time for anyone who took the federal deduction, with no income limit; the 2021 IN-112 has no such line. Co-Authored-By: Claude Opus 5.5 --- .../student_loan_interest/income_limit.yaml | 26 ++++ .../vt/tax/income/agi/subtractions.yaml | 12 +- .../vt_student_loan_interest_subtraction.yaml | 134 ++++++++++++++++++ .../vt_student_loan_interest_subtraction.py | 38 +++++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml create mode 100644 policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py 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..5798498b482 --- /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. 4 (effective January 1, 2022) + 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/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..af06e9ec32c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml @@ -0,0 +1,134 @@ +- 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_subtractions: 0 + vt_agi: 9_000 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..a8faf6b6881 --- /dev/null +++ b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py @@ -0,0 +1,38 @@ +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", + "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] + return income_eligible * remaining_interest From 2f79c676a05e1ca7d7c65c8da3cfc8005e4e63c8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:21:45 -0400 Subject: [PATCH 02/12] Phase out Vermont's child tax credit per child 32 V.S.A. 5830f(b) reduces "the amount of the credit per child" by $20 for each $1,000 of AGI over $125,000; the model reduced the total once, so two young children at $150,000 got $1,500 instead of $1,000. Co-Authored-By: Claude Opus 5.5 --- .../states/vt/tax/income/credits/vt_ctc.yaml | 23 +++++++++++++++++++ .../vt/tax/income/credits/ctc/vt_ctc.py | 11 ++++----- 2 files changed, 28 insertions(+), 6 deletions(-) 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/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 From c9d46fc09359244e5a0097a2d4bea94281bc8447 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:21:45 -0400 Subject: [PATCH 03/12] Pay Utah's at-home parent credit once per qualifying child per return Utah Code 59-10-1005(2) allows $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". The model multiplied the credit by the number of spouses earning $3,000 or less; the existing two-parent case asserted $400 for two children and now expects $200. Co-Authored-By: Claude Opus 5.5 --- .../credits/ut_at_home_parent_credit.yaml | 35 +++++++++++++++++-- .../ut_at_home_parent_credit_potential.py | 19 ++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) 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/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 From fc588f9730300b095fee306d8f8ab2695fa4d228 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:21:45 -0400 Subject: [PATCH 04/12] Test the Wisconsin homestead credit's conditions on the claimant The Schedule H instructions put each condition on the claimant: 18 or older; 62 or older, disabled or with earned income ("The spouse that meets this definition must be the claimant"); and not claimed as a dependent on another person's federal return unless 62 or older (Wis. Stat. 71.53(2)(d); Step 2a). Spouses choose the claimant. The model tested age and earnings across the household and never read the dependency bar. The existing cases now use person inputs. Co-Authored-By: Claude Opus 5.5 --- changelog.d/vt-ut-wi-statutory-fixes.fixed.md | 1 + .../wi/tax/income/wi_homestead_eligible.yaml | 401 +++++++++++++++--- .../homestead/wi_homestead_eligible.py | 45 +- 3 files changed, 371 insertions(+), 76 deletions(-) create mode 100644 changelog.d/vt-ut-wi-statutory-fixes.fixed.md 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/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/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 From 42cb60bb72844b723dd188aacbdf293ba5e0e243 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:23:19 -0400 Subject: [PATCH 05/12] Add property tests for the Vermont, Utah and Wisconsin invariants Co-Authored-By: Claude Opus 5.5 --- .../test_vt_ut_wi_statutory_invariants.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py 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() From 7ae084b4619d6a12e792e662304a5a16d63bc27d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 19:04:24 -0400 Subject: [PATCH 06/12] Gate the Vermont student loan subtraction on its 2022 effective date The subtraction list already omits the variable in 2021, but the variable itself returned a positive amount when computed for 2021. An in_effect parameter (false before 2022) makes it zero there. The Act 138 references now cite Sec. 5 (the subtraction, pages 4-5) and Sec. 13 (effective retroactively on January 1, 2022, page 15); the earlier reference named Sec. 4. Co-Authored-By: Claude Opus 5.5 --- .../agi/student_loan_interest/in_effect.yaml | 13 +++++++++++ .../student_loan_interest/income_limit.yaml | 2 +- .../vt_student_loan_interest_subtraction.yaml | 23 +++++++++++++++++++ .../vt_student_loan_interest_subtraction.py | 6 ++++- 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/in_effect.yaml 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 index 5798498b482..0924e29f038 100644 --- 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 @@ -9,7 +9,7 @@ metadata: reference: - title: 32 V.S.A. 5811(29)(B) href: https://legislature.vermont.gov/statutes/section/32/151/05811 - - title: 2022 Act 138, Sec. 4 (effective January 1, 2022) + - 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 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 index af06e9ec32c..7741ba0d445 100644 --- 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 @@ -130,5 +130,28 @@ # 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/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 index a8faf6b6881..b5648dec8d0 100644 --- 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 @@ -9,6 +9,8 @@ class vt_student_loan_interest_subtraction(Variable): 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 @@ -35,4 +37,6 @@ def formula(tax_unit, period, parameters): agi = tax_unit("adjusted_gross_income", period) filing_status = tax_unit("filing_status", period) income_eligible = agi <= p.income_limit[filing_status] - return income_eligible * remaining_interest + # 2022 Act 138 took effect on January 1, 2022; Vermont had no such + # subtraction before. + return p.in_effect * income_eligible * remaining_interest From e553dc0237ac5f924c606cc577222c3dfbd096a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 20:15:12 +0000 Subject: [PATCH 07/12] Move the Vermont child tax credit fix to its own PR A maintainer asked for the per-child phase-out under 32 V.S.A. 5830f(b) as a separate PR that fixes #9996; #10644 now carries it with its own tests. This reverts 2f79c676 and drops the matching property and changelog text, so this PR holds only the Vermont student loan, Utah and Wisconsin fixes. Co-Authored-By: Claude Opus 5.5 --- changelog.d/vt-ut-wi-statutory-fixes.fixed.md | 2 +- .../test_vt_ut_wi_statutory_invariants.py | 28 ++----------------- .../states/vt/tax/income/credits/vt_ctc.yaml | 23 --------------- .../vt/tax/income/credits/ctc/vt_ctc.py | 11 ++++---- 4 files changed, 10 insertions(+), 54 deletions(-) diff --git a/changelog.d/vt-ut-wi-statutory-fixes.fixed.md b/changelog.d/vt-ut-wi-statutory-fixes.fixed.md index 964bf73c319..2e6c772c246 100644 --- a/changelog.d/vt-ut-wi-statutory-fixes.fixed.md +++ b/changelog.d/vt-ut-wi-statutory-fixes.fixed.md @@ -1 +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. +Subtract only the Vermont student loan interest not already deducted federally, from 2022 and within the income limits; 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/tests/core/test_vt_ut_wi_statutory_invariants.py b/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py index dfe970fadbb..b1c73cc70b4 100644 --- a/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py +++ b/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py @@ -1,14 +1,12 @@ -"""Invariants of four Vermont, Utah and Wisconsin rules. +"""Invariants of three 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 +2. 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 +3. 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. @@ -118,26 +116,6 @@ def test_vt_student_loan_subtraction_completes_the_federal_deduction(year, units 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]), 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 f3f4e3c3ccf..757e825b1e3 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,26 +1,3 @@ -- 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/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 00513b318e9..6d58a34dfe1 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,12 +17,13 @@ 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) - # 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. + # Reduce credit amount over the phaseout range. excess_agi = max_(agi - p.reduction.start, 0) increments = np.ceil(excess_agi / p.reduction.increment) - reduction_per_child = p.reduction.amount * increments - credit_per_child = max_(p.amount - reduction_per_child, 0) - return count_eligible * credit_per_child + total_reduction = p.reduction.amount * increments + # Return reduced credit amount. + return max_(max_credit - total_reduction, 0) From 53d2ab7926af0786f69a0144a8301416d35ce335 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:50:14 +0000 Subject: [PATCH 08/12] Give a Vermont surviving spouse no student loan interest subtraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 32 V.S.A. § 5811(29)(B) sets limits only for single, head of household, married filing separately and joint filers, and the chapter names surviving spouses wherever it includes them, so a surviving spouse is not a qualified resident taxpayer. IN-112 line 16c's "All other filers" $120,000 has no gap to fill. The limit for that status becomes -inf; the conflict is recorded as the candidate us-vt:student-loan-interest-surviving-spouse-limit, and the parameter's comment is counted in KNOWN_UNMARKED until the marker lands. Also cite the effective date as the § 5811 history note gives it (2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022) and state (29)(B)'s filing statuses exactly in the variable's comment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f --- .../agi/student_loan_interest/in_effect.yaml | 2 +- .../student_loan_interest/income_limit.yaml | 10 +++--- .../code_health/test_source_conflicts.py | 1 + .../vt_student_loan_interest_subtraction.py | 13 ++++--- source_conflicts/candidates.yaml | 34 +++++++++++++++++++ 5 files changed, 50 insertions(+), 10 deletions(-) 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 index 4bdfb8f38a8..e1d64c28835 100644 --- 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 @@ -7,7 +7,7 @@ metadata: 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" + - title: 2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022 (the history note to § 5811) 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 index 0924e29f038..f80f2902f1b 100644 --- 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 @@ -1,4 +1,4 @@ -description: Vermont allows the student loan interest subtraction only to filers with federal adjusted gross income at or below this amount, by filing status. +description: Vermont allows the student loan interest subtraction only to filers with federal adjusted gross income at or below this amount, by filing status; no surviving spouse qualifies. metadata: propagate_metadata_to_children: true unit: currency-USD @@ -9,7 +9,7 @@ metadata: 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" + - title: 2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022 (the history note to § 5811) 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 @@ -20,7 +20,9 @@ HEAD_OF_HOUSEHOLD: 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 + # The 2025 IN-112 line 16c instructions say "All other filers" get $120,000, + # but § 5811(29)(B) names no limit for a qualifying surviving spouse, so a + # surviving spouse never qualifies. + 2022-01-01: -.inf JOINT: 2022-01-01: 200_000 diff --git a/policyengine_us/tests/code_health/test_source_conflicts.py b/policyengine_us/tests/code_health/test_source_conflicts.py index 4a9b7f803db..21b67382c61 100644 --- a/policyengine_us/tests/code_health/test_source_conflicts.py +++ b/policyengine_us/tests/code_health/test_source_conflicts.py @@ -138,6 +138,7 @@ "policyengine_us/parameters/gov/states/or/ohcs/liheap/payment/income_band.yaml": 1, "policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml": 1, "policyengine_us/parameters/gov/states/ut/dwf/ccap/copay/income_limits.yaml": 1, + "policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml": 1, "policyengine_us/parameters/gov/states/wi/tax/income/credits/homestead/income/disqualified_loss_sources.yaml": 1, "policyengine_us/parameters/gov/territories/pr/tax/income/credits/earned_income/eligibility/min.yaml": 1, "policyengine_us/parameters/gov/territories/pr/tax/income/credits/earned_income/eligibility/separate_filer.yaml": 1, 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 index b5648dec8d0..9677b8eec7a 100644 --- 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 @@ -9,8 +9,8 @@ class vt_student_loan_interest_subtraction(Variable): 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 + # 2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022 (the history + # note to § 5811) "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 @@ -33,10 +33,13 @@ def formula(tax_unit, period, parameters): 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. + # at most $120,000 if single, head of household or married filing + # separately, and $200,000 if married filing jointly. It sets no limit + # for a qualifying surviving spouse, who doesn't qualify; + # income_limit.yaml records that. 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. + # The history note to § 5811 dates 2021, No. 138 (Adj. Sess.), §§ 5 + # and 6 to Jan. 1, 2022; the subtraction applies from that date. return p.in_effect * income_eligible * remaining_interest diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 6b1dbcbedc4..59094733f85 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -9964,6 +9964,40 @@ candidates: state: unreviewed note: PE-US follows Vermont's deduction; evidence concerns federal fiscal 2016-2017 and 2024 only, with intervening years unasserted. +- id: us-vt:student-loan-interest-surviving-spouse-limit + program: income-tax + years: + from: 2022 + type: tax + question: Does a qualifying surviving spouse get Vermont's student loan interest subtraction, when 32 V.S.A. § + 5811(29)(B) sets an income limit only for single, head of household, married filing separately and joint filers? + sources: + - kind: statute + citation: 32 V.S.A. § 5811(21)(B)(vi), (29)(B) + url: https://legislature.vermont.gov/statutes/section/32/151/05811 + - kind: form + citation: 2025 Vermont Schedule IN-112, line 16c + url: https://taxsim.nber.org/historical_state_tax_forms/VT/2025/Income-Booklet-2025.pdf#page=23 + - kind: statute + citation: 32 V.S.A. § 5822(a) + url: https://legislature.vermont.gov/statutes/section/32/151/05822 + readings: + statute: (29)(B) defines a qualified resident taxpayer by a $120,000 limit for single, head of household and + married filing separately filers and a $200,000 limit for joint filers. It names no amount for a qualifying + surviving spouse, so a surviving spouse doesn't meet the definition and gets no subtraction. + other: Line 16c applies $120,000 to "All other filers", a qualifying surviving spouse included. + example: A 2025 surviving spouse with $110,000 of federal AGI and $3,000 of student loan interest, none deducted + federally, subtracts $3,000 under line 16c and nothing under § 5811(29)(B). + pe_us: + follows: statute + code: + - policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10104 + state: ruled + note: PolicyEngine follows the statute. The chapter names surviving spouses where it includes them (§ 5811(21)(C)(i) + and (ii), § 5822(a)(1) and (3)), and (29)(B) does not. The Department's form gives them the $120,000 limit. Code + not yet on main. - id: us-wa:liheap-minimum-benefit-cost-cap program: liheap years: From 6c61d40880efb6284dd312094da5aa94a5ff9581 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:50:14 +0000 Subject: [PATCH 09/12] Cite the Wisconsin statutes behind the homestead claimant tests The age, earned income and earned income definition tests come from Wis. Stat. 71.53(2)(c), 71.54(1)(gm) and 71.52(1g); the reference tuple and the comment cited only the LFB paper and the Schedule H instructions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f --- .../income/credits/homestead/wi_homestead_eligible.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 a99a39f3a0d..b211290f951 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 @@ -11,6 +11,9 @@ class wi_homestead_eligible(Variable): # 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", + "https://docs.legis.wisconsin.gov/statutes/statutes/71/viii/53/2/c", + "https://docs.legis.wisconsin.gov/statutes/statutes/71/viii/54", + "https://docs.legis.wisconsin.gov/statutes/statutes/71/viii/52/1g", ) defined_for = StateCode.WI @@ -20,10 +23,10 @@ def formula(tax_unit, period, parameters): 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." + # Wis. Stat. 71.53(2)(c), 71.54(1)(gm): 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)) From 37fd90982d9710d936cdab6f551438886e00454c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:55:55 +0000 Subject: [PATCH 10/12] Pin Vermont's limits, the surviving spouse and Wisconsin's claimant age MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the cases no test covered: AGI exactly at the $120,000 and $200,000 limits ("equal to or less than"), two surviving spouses who get no subtraction under 32 V.S.A. § 5811(29)(B) (the form would give $3,000 and $1,500), a dependent's interest, and a Wisconsin couple where neither spouse alone is both 18 or older and an earner. Also carries #10191's eligibility Cases 8 to 15 unchanged, so that PR's merge of main can take main's side of the eligibility file whole, and cites the effective date as the § 5811 history note gives it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f --- .../vt/tax/income/agi/subtractions.yaml | 2 +- .../vt_student_loan_interest_subtraction.yaml | 135 +++++++++- .../wi/tax/income/wi_homestead_eligible.yaml | 233 ++++++++++++++++++ 3 files changed, 367 insertions(+), 3 deletions(-) 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 d077a7b8562..128b5219ed0 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 @@ -6,7 +6,7 @@ values: - vt_capital_gains_exclusion - vt_retirement_income_exemption - salt_refund_income - # 2022 Act 138 added the student loan interest subtraction from 2022 + # 2021, No. 138 (Adj. Sess.) 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 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 index 7741ba0d445..b97f519b05c 100644 --- 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 @@ -127,7 +127,7 @@ members: [filer] state_code: VT output: - # 2022 Act 138 added 5811(21)(B)(vi) from 2022; the 2021 Schedule IN-112 + # 2021, No. 138 (Adj. Sess.) 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 @@ -150,8 +150,139 @@ members: [filer] state_code: VT output: - # The subtraction took effect on January 1, 2022 (2022 Act 138, Sec. 13), so + # The § 5811 history note dates the subtraction to January 1, 2022 (2021, + # No. 138 (Adj. Sess.), §§ 5, 6), 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 + +- name: 2025 single filer at exactly $120,000 of adjusted gross income keeps the Vermont student loan interest subtraction + period: 2025 + input: + people: + filer: + age: 30 + employment_income: 120_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 "equal to or less than" $120,000. The + # federal deduction is fully phased out above $100,000 of MAGI, so 16a $3,000 + # - 16b $0 = $3,000. + adjusted_gross_income: 120_000 + student_loan_interest_ald: 0 + vt_student_loan_interest_subtraction: 3_000 + +- name: 2022 joint return at exactly $200,000 of adjusted gross income keeps the Vermont student loan interest subtraction + period: 2022 + input: + people: + head: + age: 30 + employment_income: 200_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 "equal to or less than" $200,000 if married filing + # jointly. The 2022 federal deduction ends at $175,000 of joint MAGI. + adjusted_gross_income: 200_000 + student_loan_interest_ald: [0, 0] + vt_student_loan_interest_subtraction: 3_000 + +- name: 2025 surviving spouse with $110,000 of wages gets no Vermont student loan interest subtraction + period: 2025 + input: + people: + head: + age: 40 + employment_income: 110_000 + student_loan_interest: 3_000 + child: + age: 10 + tax_units: + tax_unit: + members: [head, child] + filing_status: SURVIVING_SPOUSE + households: + household: + members: [head, child] + state_code: VT + output: + # Federal: 2025 MAGI $110,000 is past the non-joint phase-out ($85,000 to + # $100,000), so the deduction is $0 and AGI is $110,000. + # Vermont: 5811(29)(B) sets limits for single, head of household, married + # filing separately and joint filers only, so a surviving spouse is not a + # qualified resident taxpayer: $0. (IN-112 line 16c would give $3,000 - $0 + # = $3,000.) + adjusted_gross_income: 110_000 + student_loan_interest_ald: [0, 0] + vt_student_loan_interest_subtraction: 0 + +- name: 2025 surviving spouse with $50,000 of wages gets no Vermont subtraction for interest above the federal cap + period: 2025 + input: + people: + head: + age: 40 + employment_income: 50_000 + student_loan_interest: 4_000 + child: + age: 10 + tax_units: + tax_unit: + members: [head, child] + filing_status: SURVIVING_SPOUSE + households: + household: + members: [head, child] + state_code: VT + output: + # Federal: min($4,000, $2,500 cap) = $2,500, MAGI $50,000 is below the + # $85,000 phase-out start, so AGI is $50,000 - $2,500 = $47,500. + # Vermont: 5811(29)(B) names no limit for a surviving spouse, so $0. + # (IN-112 line 16c would give $4,000 - $2,500 = $1,500.) + adjusted_gross_income: 47_500 + student_loan_interest_ald: [2_500, 0] + vt_student_loan_interest_subtraction: 0 + +- name: 2025 interest paid by a dependent is not part of the filer's Vermont student loan interest subtraction + period: 2025 + input: + people: + head: + age: 50 + employment_income: 60_000 + student: + age: 22 + is_full_time_student: true + is_tax_unit_dependent: true + student_loan_interest: 1_000 + tax_units: + tax_unit: + members: [head, student] + households: + household: + members: [head, student] + state_code: VT + output: + # 5811(21)(B)(vi): interest "paid by a qualified resident taxpayer"; the + # dependent is not the taxpayer on this return. + student_loan_interest_ald: [0, 0] + vt_student_loan_interest_subtraction: 0 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 614c0f0b5ac..2eba0c6ceaa 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 @@ -384,3 +384,236 @@ output: wi_homestead_eligible: false wi_homestead_credit: 0 + +- name: "2025 joint return where the only earner is 17 and the 18-year-old spouse has no earned income: no Wisconsin homestead claimant" + period: 2025 + input: + people: + head: + age: 18 + spouse: + age: 17 + employment_income: 5_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + wi_homestead_income: 9_000 + households: + household: + members: [head, spouse] + state_code: WI + output: + # Wis. Stat. 71.53(2)(c) bars a claimant under 18, and 71.54(1)(gm) bars a + # claimant under 62 who is not disabled and has no earned income. Neither + # spouse meets both alone. + wi_homestead_eligible: false + +# The claimant must have their own earned income; either spouse may be the +# claimant (2024 Schedule H instructions, page 10). Unemployment compensation +# is not earned income (page 7), and a dependent's wages do not qualify them. + +- name: Case 8, a dependent's wages do not meet the claimant's earned income test. + period: 2024 + input: + people: + person1: + age: 40 + unemployment_compensation: 6_000 + person2: + age: 17 + is_tax_unit_dependent: true + employment_income: 4_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_eligible: false + +- name: Case 9, the claimant's own wages meet the earned income test. + period: 2024 + input: + people: + person1: + age: 40 + employment_income: 4_000 + person2: + age: 17 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_eligible: true + +# Either spouse can be the claimant, who must be 18 or older and 62 or older, +# disabled or have their own earned income (2024 Schedule H instructions, +# pages 4 and 10; Wis. Stat. 71.53(2)(c), 71.54(1)(dm)-(gm)). + +- name: Case 10, one spouse's wages qualify even when the other has a larger self-employment loss. + period: 2024 + input: + people: + person1: + age: 40 + employment_income: 5_000 + person2: + age: 40 + self_employment_income: -8_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_eligible: true + +- name: Case 11, the earner must also be 18 or older. + period: 2024 + input: + people: + person1: + age: 17 + employment_income: 6_000 + is_tax_unit_head: true + person2: + age: 19 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_eligible: false + +# Only one spouse needs earned income, and that spouse must be the claimant +# (2024 Schedule H instructions, page 10). Either tax-unit role can qualify. + +- name: Case 12, only the spouse earns and can be the claimant. + period: 2024 + input: + people: + person1: + age: 40 + is_tax_unit_head: true + ssi: 0 + person2: + age: 38 + employment_income: 6_000 + is_tax_unit_spouse: true + ssi: 0 + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + tanf: 0 + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_income: 6_000 + wi_homestead_eligible: true + +- name: Case 13, the same earning spouse can be labeled as the head. + period: 2024 + input: + people: + person1: + age: 40 + is_tax_unit_spouse: true + ssi: 0 + person2: + age: 38 + employment_income: 6_000 + is_tax_unit_head: true + ssi: 0 + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + tanf: 0 + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_income: 6_000 + wi_homestead_eligible: true + +# The person meeting the age-62 condition can be the claimant (2024 +# Schedule H instructions, page 4), whichever tax-unit role they hold. + +- name: Case 14, only the spouse is 65 and can be the claimant. + period: 2024 + input: + people: + person1: + age: 50 + is_tax_unit_head: true + ssi: 0 + person2: + age: 65 + social_security_retirement: 9_000 + is_tax_unit_spouse: true + ssi: 0 + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + tanf: 0 + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_income: 9_000 + wi_homestead_eligible: true + +- name: Case 15, the same older spouse can be labeled as the head. + period: 2024 + input: + people: + person1: + age: 50 + is_tax_unit_spouse: true + ssi: 0 + person2: + age: 65 + social_security_retirement: 9_000 + is_tax_unit_head: true + ssi: 0 + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + tanf: 0 + households: + household: + members: [person1, person2] + state_code: WI + output: + wi_homestead_income: 9_000 + wi_homestead_eligible: true From 8c307c64dbddeb4a9373cfeb249c2767c6f4c483 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:00:08 +0000 Subject: [PATCH 11/12] Draw Vermont surviving spouses in the student loan property test The test hard-coded the limit as $200,000 joint and $120,000 otherwise and never drew a surviving spouse, so a surviving-spouse limit of $120,000 or $200,000 passed it. It now draws single, joint and surviving spouse returns and asserts a zero subtraction for every surviving spouse. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f --- .../test_vt_ut_wi_statutory_invariants.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) 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 index b1c73cc70b4..d5b81c35223 100644 --- a/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py +++ b/policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py @@ -3,7 +3,8 @@ 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. + is zero above the limit, in 2021, for every surviving spouse + (5811(29)(B) sets no limit for that status), and never negative. 2. 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. 3. Wisconsin homestead eligibility: the same under either head/spouse @@ -26,9 +27,10 @@ ) -def _situation(units, year, state): +def _situation(units, year, state, filing_statuses=None): """units: list of lists of person dicts; the first person is the head, - the second (if flagged) the spouse, the rest dependents.""" + the second (if flagged) the spouse, the rest dependents. filing_statuses, + if given, sets each tax unit's filing status.""" people, groups = ( {}, { @@ -59,6 +61,8 @@ def _situation(units, year, state): 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} + if filing_statuses is not None: + groups["tax_units"][f"t_{i}"]["filing_status"] = {year: filing_statuses[i]} groups["households"][f"h_{i}"] = { "members": members, "state_code": {year: state}, @@ -72,7 +76,7 @@ def _calc(sim, name, year): @st.composite def borrowers(draw): - joint = draw(st.booleans()) + status = draw(st.sampled_from(["SINGLE", "JOINT", "SURVIVING_SPOUSE"])) head = { "age": 30, "is_tax_unit_head": True, @@ -80,7 +84,9 @@ def borrowers(draw): "student_loan_interest": float(draw(st.integers(0, 6_000))), } unit = [head] - if joint: + if status == "SURVIVING_SPOUSE": + unit.append({"age": 10}) + if status == "JOINT": unit.append( { "age": 30, @@ -89,7 +95,7 @@ def borrowers(draw): "student_loan_interest": float(draw(st.integers(0, 3_000))), } ) - return unit + return status, unit @SETTINGS @@ -97,17 +103,24 @@ def borrowers(draw): 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]) +def test_vt_student_loan_subtraction_completes_the_federal_deduction(year, draws): + statuses = np.array([status for status, _ in draws]) + units = [unit for _, unit in draws] + sim = Simulation(situation=_situation(units, year, "VT", list(statuses))) + paid = np.array([sum(p.get("student_loan_interest", 0) 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) + surviving = statuses == "SURVIVING_SPOUSE" + # 5811(29)(B): $200,000 if joint, $120,000 if single; no surviving spouse + # qualifies. + limit = np.where( + statuses == "JOINT", 200_000, np.where(surviving, -np.inf, 120_000) + ) assert (sub >= 0).all() + assert not sub[surviving].any() if year < 2022: assert not sub.any() return From a35d9295a34a95ddd6b828eea6eaee8abb80c46d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:13:08 +0000 Subject: [PATCH 12/12] Note the Vermont surviving spouse in the changelog fragment Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f --- changelog.d/vt-ut-wi-statutory-fixes.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/vt-ut-wi-statutory-fixes.fixed.md b/changelog.d/vt-ut-wi-statutory-fixes.fixed.md index 2e6c772c246..b57511f80e3 100644 --- a/changelog.d/vt-ut-wi-statutory-fixes.fixed.md +++ b/changelog.d/vt-ut-wi-statutory-fixes.fixed.md @@ -1 +1 @@ -Subtract only the Vermont student loan interest not already deducted federally, from 2022 and within the income limits; 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. +Subtract only the Vermont student loan interest not already deducted federally, from 2022 and within the income limits, which give a surviving spouse none; 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.