From e7c4f28b1913a4e18316d19d2c1fd68da5c18f4d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 11:42:27 -0400 Subject: [PATCH 1/7] Count only deductible mortgage interest in Montana; report the whole Virginia EITC Montana's itemized deductions (2018-2023 under MCA 15-30-2131(1)(a), which takes the IRC section 161 items as amended per MCA 15-30-2101) allow the home mortgage interest "allowed by federal law" (Form 2 instructions, line 9), so interest on acquisition debt above the IRC 163(h)(3) caps is not deductible. The four Montana itemized deduction variables used `mortgage_interest`, which adds `non_deductible_mortgage_interest`; they now use `deductible_mortgage_interest`. `va_eitc` returned only the head's VAGI share of the credit for SEPARATE units, so a two-adult SEPARATE unit's reported credit depended on which spouse was labelled head, and a dependent's Virginia subtractions scaled a lone filer's credit. Spouses filing separately together claim the whole credit (Form 760 instructions: EIC shares by earned income, the low-income credit on one return), and the tax already applies the whole credit, so `va_eitc` now reports it for every filing status. `va_eitc_person` and `va_agi_share` fed only that branch and are removed. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-state-head-fallbacks.fixed.md | 1 + ..._refundable_credit_downstream_consumers.py | 1 - ...uctions_for_federal_itemization_indiv.yaml | 38 +++++++++- ...uctions_for_federal_itemization_joint.yaml | 71 ++++++++++++++++++- .../mt_itemized_deductions_indiv.yaml | 38 +++++++++- .../mt_itemized_deductions_joint.yaml | 71 ++++++++++++++++++- .../va/tax/income/credits/eitc/va_eitc.yaml | 66 +++++++++++++++-- .../income/credits/eitc/va_eitc_person.yaml | 63 ---------------- .../states/va/tax/income/va_agi_share.yaml | 40 ----------- ...eductions_for_federal_itemization_indiv.py | 11 ++- ...eductions_for_federal_itemization_joint.py | 11 ++- .../general/mt_itemized_deductions_indiv.py | 11 ++- .../general/mt_itemized_deductions_joint.py | 11 ++- .../va/tax/income/credits/eitc/va_eitc.py | 24 ++++--- .../tax/income/credits/eitc/va_eitc_person.py | 19 ----- .../gov/states/va/tax/income/va_agi_share.py | 25 ------- 16 files changed, 327 insertions(+), 174 deletions(-) create mode 100644 changelog.d/fix-state-head-fallbacks.fixed.md delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc_person.yaml delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_share.yaml delete mode 100644 policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc_person.py delete mode 100644 policyengine_us/variables/gov/states/va/tax/income/va_agi_share.py diff --git a/changelog.d/fix-state-head-fallbacks.fixed.md b/changelog.d/fix-state-head-fallbacks.fixed.md new file mode 100644 index 00000000000..047e13c35b9 --- /dev/null +++ b/changelog.d/fix-state-head-fallbacks.fixed.md @@ -0,0 +1 @@ +Montana itemized deductions count only federally deductible mortgage interest, so interest on acquisition debt above the IRC 163(h)(3) caps is no longer deducted. The Virginia EITC reports the whole credit the tax applies for spouses filing separately, not only the share of the spouse listed as head. diff --git a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py index 6b769d9b02e..d4f8dce8cd5 100644 --- a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py +++ b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py @@ -78,7 +78,6 @@ "va_non_refundable_eitc": { "variables/gov/states/va/tax/income/credits/eitc/refundability_calculation/va_income_tax_if_claiming_non_refundable_eitc.py", "variables/gov/states/va/tax/income/credits/eitc/va_eitc.py", - "variables/gov/states/va/tax/income/credits/eitc/va_eitc_person.py", }, } diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml index 4435e64a801..d50865834e9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml @@ -30,7 +30,7 @@ period: 2021 input: charitable_deduction: 1_000 - mortgage_interest: 34 + deductible_mortgage_interest: 34 mt_salt_deduction: 1_035 mt_medical_expense_deduction_indiv: 3_000 casualty_loss_deduction: 745 @@ -74,3 +74,39 @@ state_code: MT output: mt_itemized_deductions_for_federal_itemization_indiv: [1_000, 700, 0] + +- name: Spouses filing separately split the federally capped mortgage interest + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 30_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + spouse: + is_tax_unit_spouse: true + home_mortgage_interest: 10_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2019 + charitable_deduction: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # $30,000 of the $40,000 interest is allowed by federal law (the + # $750,000 cap on $1,000,000 of post-2017 debt). Each spouse reported a + # share of the interest and deducts that share of the allowed amount. + deductible_mortgage_interest: [22_500, 7_500] + mt_itemized_deductions_for_federal_itemization_indiv: [22_500, 7_500] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml index 8a1114f4ef5..d3dbd04270a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml @@ -18,7 +18,7 @@ input: filing_status: HEAD_OF_HOUSEHOLD charitable_deduction: 0 - mortgage_interest: 1_234 + deductible_mortgage_interest: 1_234 mt_salt_deduction: 10_000 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 0 @@ -33,7 +33,7 @@ input: filing_status: SINGLE charitable_deduction: 1_000 - mortgage_interest: 34 + deductible_mortgage_interest: 34 mt_salt_deduction: 1_035 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 745 @@ -74,3 +74,70 @@ mt_federal_income_tax_deduction_for_federal_itemization: 600 output: mt_itemized_deductions_for_federal_itemization_joint: [2_212, 0, 0] + +- name: Mortgage interest on post-2017 debt above the federal cap is not deductible + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 40_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_spouse: true + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2019 + charitable_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # $750,000 of the $1,000,000 acquisition debt incurred after Dec. 15, + # 2017 qualifies, so $30,000 of the $40,000 interest is allowed by + # federal law and in Montana (2022 Form 2 instructions, line 9). + deductible_mortgage_interest: [30_000, 0] + mt_itemized_deductions_for_federal_itemization_joint: [30_000, 0] + +- name: Mortgage interest on pre-2018 debt keeps the $1,000,000 cap + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 40_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_spouse: true + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2016 + charitable_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # Debt incurred before Dec. 16, 2017 keeps the $1,000,000 cap, so all + # $40,000 is allowed. + deductible_mortgage_interest: [40_000, 0] + mt_itemized_deductions_for_federal_itemization_joint: [40_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml index 2bc390f8ad3..0b83bfadd6a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml @@ -30,7 +30,7 @@ period: 2021 input: charitable_deduction: 1_000 - mortgage_interest: 34 + deductible_mortgage_interest: 34 mt_salt_deduction: 1_035 mt_medical_expense_deduction_indiv: 3_000 casualty_loss_deduction: 745 @@ -74,3 +74,39 @@ state_code: MT output: mt_itemized_deductions_indiv: [1_000, 700, 0] + +- name: Spouses filing separately split the federally capped mortgage interest + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 30_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + spouse: + is_tax_unit_spouse: true + home_mortgage_interest: 10_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2019 + charitable_deduction: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # $30,000 of the $40,000 interest is allowed by federal law (the + # $750,000 cap on $1,000,000 of post-2017 debt). Each spouse reported a + # share of the interest and deducts that share of the allowed amount. + deductible_mortgage_interest: [22_500, 7_500] + mt_itemized_deductions_indiv: [22_500, 7_500] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml index 6b25795e07a..f82bb4e07d1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml @@ -18,7 +18,7 @@ input: filing_status: HEAD_OF_HOUSEHOLD charitable_deduction: 0 - mortgage_interest: 1_234 + deductible_mortgage_interest: 1_234 mt_salt_deduction: 10_000 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 0 @@ -33,7 +33,7 @@ input: filing_status: SINGLE charitable_deduction: 1_000 - mortgage_interest: 34 + deductible_mortgage_interest: 34 mt_salt_deduction: 1_035 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 745 @@ -74,3 +74,70 @@ mt_federal_income_tax_deduction_unit: 600 output: mt_itemized_deductions_joint: [2_212, 0, 0] + +- name: Mortgage interest on post-2017 debt above the federal cap is not deductible + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 40_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_spouse: true + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2019 + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # $750,000 of the $1,000,000 acquisition debt incurred after Dec. 15, + # 2017 qualifies, so $30,000 of the $40,000 interest is allowed by + # federal law and in Montana (2022 Form 2 instructions, line 9). + deductible_mortgage_interest: [30_000, 0] + mt_itemized_deductions_joint: [30_000, 0] + +- name: Mortgage interest on pre-2018 debt keeps the $1,000,000 cap + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 40_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_spouse: true + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2016 + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # Debt incurred before Dec. 16, 2017 keeps the $1,000,000 cap, so all + # $40,000 is allowed. + deductible_mortgage_interest: [40_000, 0] + mt_itemized_deductions_joint: [40_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml index f058dd918c1..422d3ec9101 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml @@ -39,16 +39,16 @@ output: va_eitc: 100 -- name: Prorated eitc for the head of the tax unit +- name: Spouses filing separately in one tax unit report the whole credit the tax applies period: 2021 input: people: person1: - va_eitc_person: 1_000 is_tax_unit_head: true + employment_income: 9_000 person2: - va_eitc_person: 500 - is_tax_unit_head: false + is_tax_unit_spouse: true + employment_income: 3_000 tax_units: tax_unit: members: [person1, person2] @@ -61,4 +61,60 @@ members: [person1, person2] state_code: VA output: - va_eitc: 1_000 + # The spouses split the Virginia EIC by their shares of the earned income + # used for the federal EITC and claim those shares on their two separate + # returns, and only one of them may claim the Credit for Low-Income + # Individuals (2021 Form 760 instructions, p. 32), so together they claim + # the whole credit. It does not depend on which spouse is listed first. + va_eitc: 100 + +- name: The same couple with the head and spouse labels exchanged + period: 2021 + input: + people: + person1: + is_tax_unit_spouse: true + employment_income: 9_000 + person2: + is_tax_unit_head: true + employment_income: 3_000 + tax_units: + tax_unit: + members: [person1, person2] + va_refundable_eitc: 0 + va_non_refundable_eitc: 100 + va_claims_refundable_eitc: false + filing_status: SEPARATE + households: + household: + members: [person1, person2] + state_code: VA + output: + va_eitc: 100 + +- name: A separate filer's credit is not reduced by a dependent's Virginia subtractions + period: 2024 + input: + people: + parent: + is_tax_unit_head: true + age: 40 + employment_income: 12_000 + child: + is_tax_unit_dependent: true + age: 19 + is_full_time_student: true + unemployment_compensation: 4_000 + tax_units: + tax_unit: + members: [parent, child] + va_refundable_eitc: 600 + va_non_refundable_eitc: 15 + va_claims_refundable_eitc: true + filing_status: SEPARATE + households: + household: + members: [parent, child] + state_code: VA + output: + va_eitc: 600 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc_person.yaml deleted file mode 100644 index 60883139865..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc_person.yaml +++ /dev/null @@ -1,63 +0,0 @@ -- name: Claims refundable eitc. - period: 2021 - input: - va_refundable_eitc: 800 - va_non_refundable_eitc: 400 - va_claims_refundable_eitc: true - va_agi_share: 0.8 - state_code: VA - output: - va_eitc_person: 640 - -- name: Claims refundable eitc. - period: 2021 - input: - people: - p1: - va_agi_share: 0.8 - p2: - va_agi_share: 0.2 - tax_units: - tax_unit: - members: [p1, p2] - va_refundable_eitc: 800 - va_non_refundable_eitc: 400 - va_claims_refundable_eitc: true - households: - household: - members: [p1, p2] - state_code: VA - output: - va_eitc_person: [640, 160] - -- name: Claims non-refundable eitc. - period: 2021 - input: - va_refundable_eitc: 50 - va_non_refundable_eitc: 100 - va_claims_refundable_eitc: false - va_agi_share: 0.8 - state_code: VA - output: - va_eitc_person: 80 - -- name: Claims refundable eitc. - period: 2021 - input: - people: - p1: - va_agi_share: 0.8 - p2: - va_agi_share: 0.2 - tax_units: - tax_unit: - members: [p1, p2] - va_refundable_eitc: 50 - va_non_refundable_eitc: 100 - va_claims_refundable_eitc: false - households: - household: - members: [p1, p2] - state_code: VA - output: - va_eitc_person: [80, 20] diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_share.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_share.yaml deleted file mode 100644 index e64b54f52e6..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_share.yaml +++ /dev/null @@ -1,40 +0,0 @@ -- name: Positive income for both the head and the spouse in the household. - period: 2021 - input: - people: - person1: - is_tax_unit_head: true - age: 40 - va_agi_person: 200 - person2: - is_tax_unit_spouse: true - age: 40 - va_agi_person: 800 - households: - household: - members: [person1, person2] - state_code: VA - output: - va_agi_share: [0.2, 0.8] - -- name: If no adjusted gross income, assign to head. - period: 2021 - input: - people: - person1: - is_tax_unit_head: true - person2: - is_tax_unit_spouse: true - households: - household: - members: [person1, person2] - state_code: VA - output: - va_agi_share: [1, 0] - -- name: If no net income and single, assign entirely to them as head. - period: 2021 - input: - state_code: VA - output: - va_agi_share: 1 diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py index 5d1cb0c5153..1c1a1e3c8e4 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py @@ -11,6 +11,12 @@ class mt_itemized_deductions_for_federal_itemization_indiv(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0010/0150-0300-0210-0010.html", + # MCA 15-30-2101: "Internal Revenue Code" means the IRC as amended + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", + # 2023 Form 2 instructions, itemized deductions line 9: home mortgage + # interest "allowed by federal law", limited to the first $750,000 of + # acquisition debt incurred after Dec. 15, 2017 ) defined_for = "mt_married_filing_separately_on_same_return_eligible" @@ -22,7 +28,10 @@ def formula(person, period, parameters): head_or_spouse = person("is_tax_unit_head_or_spouse", period) # The interest deduction is the sum of mortagage and investment interest expenses investment_interest = person("investment_interest_expense", period) - mortgage_interest = person("mortgage_interest", period) + # Montana allows the mortgage interest federal law allows, so interest + # on acquisition debt above the IRC 163(h)(3) caps is not deductible + # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). + mortgage_interest = person("deductible_mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( person, diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py index 3469bb0b4b3..c56b1d7272e 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py @@ -11,13 +11,22 @@ class mt_itemized_deductions_for_federal_itemization_joint(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0010/0150-0300-0210-0010.html", + # MCA 15-30-2101: "Internal Revenue Code" means the IRC as amended + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", + # 2023 Form 2 instructions, itemized deductions line 9: home mortgage + # interest "allowed by federal law", limited to the first $750,000 of + # acquisition debt incurred after Dec. 15, 2017 ) defined_for = StateCode.MT def formula(person, period, parameters): charitable_deduction = person.tax_unit("charitable_deduction", period) investment_interest = person("investment_interest_expense", period) - mortgage_interest = person("mortgage_interest", period) + # Montana allows the mortgage interest federal law allows, so interest + # on acquisition debt above the IRC 163(h)(3) caps is not deductible + # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). + mortgage_interest = person("deductible_mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( person.tax_unit, diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py index 7b31319e265..2ce15dcaaac 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py @@ -11,6 +11,12 @@ class mt_itemized_deductions_indiv(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0010/0150-0300-0210-0010.html", + # MCA 15-30-2101: "Internal Revenue Code" means the IRC as amended + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", + # 2023 Form 2 instructions, itemized deductions line 9: home mortgage + # interest "allowed by federal law", limited to the first $750,000 of + # acquisition debt incurred after Dec. 15, 2017 ) defined_for = "mt_married_filing_separately_on_same_return_eligible" @@ -24,7 +30,10 @@ def formula(person, period, parameters): head_or_spouse = person("is_tax_unit_head_or_spouse", period) # The interest deduction is the sum of mortagage and investment interest expenses investment_interest = person("investment_interest_expense", period) - mortgage_interest = person("mortgage_interest", period) + # Montana allows the mortgage interest federal law allows, so interest + # on acquisition debt above the IRC 163(h)(3) caps is not deductible + # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). + mortgage_interest = person("deductible_mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( person, diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py index a3328b0290d..c13d9dbd54e 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py @@ -11,13 +11,22 @@ class mt_itemized_deductions_joint(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0010/0150-0300-0210-0010.html", + # MCA 15-30-2101: "Internal Revenue Code" means the IRC as amended + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", + # 2023 Form 2 instructions, itemized deductions line 9: home mortgage + # interest "allowed by federal law", limited to the first $750,000 of + # acquisition debt incurred after Dec. 15, 2017 ) defined_for = StateCode.MT def formula(person, period, parameters): charitable_deduction = person.tax_unit("charitable_deduction", period) investment_interest = person("investment_interest_expense", period) - mortgage_interest = person("mortgage_interest", period) + # Montana allows the mortgage interest federal law allows, so interest + # on acquisition debt above the IRC 163(h)(3) caps is not deductible + # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). + mortgage_interest = person("deductible_mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( person.tax_unit, diff --git a/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py b/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py index 0447a964de3..12ed37aef99 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py +++ b/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py @@ -6,21 +6,23 @@ class va_eitc(Variable): entity = TaxUnit label = "Virginia Earned Income Tax Credit" unit = USD - documentation = "Refundable or non-refundable Virginia EITC" + documentation = ( + "Refundable or non-refundable Virginia EITC. Spouses filing separately " + "split the Virginia EIC by their shares of the earned income used for " + "the federal EITC, and only one of them may claim the Credit for " + "Low-Income Individuals, so their returns together claim the whole " + "credit. This is the amount the tax unit's income tax applies." + ) definition_period = YEAR - reference = "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=32" + reference = ( + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=32", + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2025-760-instructions.pdf#page=31", + "https://law.lis.virginia.gov/vacode/title58.1/chapter3/section58.1-339.8/", + ) defined_for = StateCode.VA def formula(tax_unit, period, parameters): refundable_eitc = tax_unit("va_refundable_eitc", period) non_refundable_eitc = tax_unit("va_non_refundable_eitc", period) claims_refundable = tax_unit("va_claims_refundable_eitc", period) - amount = where(claims_refundable, refundable_eitc, non_refundable_eitc) - filing_status = tax_unit("filing_status", period) - # In the case of separated individuals, the EITC amount is prorated - is_separate = filing_status == filing_status.possible_values.SEPARATE - person = tax_unit.members - eitc_person = person("va_eitc_person", period) - is_head = person("is_tax_unit_head", period) - head_eitc = tax_unit.sum(eitc_person * is_head) - return where(is_separate, head_eitc, amount) + return where(claims_refundable, refundable_eitc, non_refundable_eitc) diff --git a/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc_person.py b/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc_person.py deleted file mode 100644 index 703dc91f6e9..00000000000 --- a/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc_person.py +++ /dev/null @@ -1,19 +0,0 @@ -from policyengine_us.model_api import * - - -class va_eitc_person(Variable): - value_type = float - entity = Person - label = "Virginia Earned Income Tax Credit per individual when married filing seperately" - unit = USD - definition_period = YEAR - reference = "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=32" - defined_for = StateCode.VA - - def formula(person, period, parameters): - refundable_eitc = person.tax_unit("va_refundable_eitc", period) - non_refundable_eitc = person.tax_unit("va_non_refundable_eitc", period) - claims_refundable = person.tax_unit("va_claims_refundable_eitc", period) - va_agi_share = person("va_agi_share", period) - eitc_claimed = where(claims_refundable, refundable_eitc, non_refundable_eitc) - return eitc_claimed * va_agi_share diff --git a/policyengine_us/variables/gov/states/va/tax/income/va_agi_share.py b/policyengine_us/variables/gov/states/va/tax/income/va_agi_share.py deleted file mode 100644 index aef97e3632e..00000000000 --- a/policyengine_us/variables/gov/states/va/tax/income/va_agi_share.py +++ /dev/null @@ -1,25 +0,0 @@ -from policyengine_us.model_api import * - - -class va_agi_share(Variable): - value_type = float - entity = Person - label = "Virginia share of state adjusted gross income of each person" - unit = USD - definition_period = YEAR - reference = "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2021-760-instructions.pdf#page=20" - defined_for = StateCode.VA - - def formula(person, period, parameters): - agi = person("va_agi_person", period) - total_agi = person.tax_unit.sum(agi) - # avoid divide-by-zero warnings - fraction = np.zeros_like(total_agi) - mask = total_agi != 0 - fraction[mask] = agi[mask] / total_agi[mask] - # if no net income, then assign entirely to head. - return where( - total_agi == 0, - person("is_tax_unit_head", period), - fraction, - ) From 351ae2638ac7db6ca360eb37a52616862cec35da Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 16:36:40 -0400 Subject: [PATCH 2/7] Name the separate-filer Virginia EITC case for what it checks On main a dependent's Virginia subtractions raise the filer's VAGI share above 1, so the case guards against any dependence, not only a reduction. Co-Authored-By: Claude Opus 5.5 --- .../baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml index 422d3ec9101..d016cdf73a2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml @@ -92,7 +92,7 @@ output: va_eitc: 100 -- name: A separate filer's credit is not reduced by a dependent's Virginia subtractions +- name: A separate filer's credit does not depend on a dependent's Virginia subtractions period: 2024 input: people: From 9336312608b3a439d02bf77c7b22fce338bf1438 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 16:42:02 -0400 Subject: [PATCH 3/7] Check Montana's mortgage interest against the federal cap for every balance A grid of couples (balance, origination year, interest, the spouse's share) restates the 26 U.S.C. 163(h)(3) caps independently of the parameter files and checks the four Montana itemized deduction variables against them, on the joint path every year and on the separate-on-the-same-form path through 2023. Fails on main for the 12 of 60 joint units where a cap binds (largest difference $28,000); passes here. Locally (macOS arm64, not CI): about 1 s of test time, 15.7 s wall including import, 1.14 GB peak RSS. Co-Authored-By: Claude Opus 5.5 --- ...tgage_interest_federal_limit_properties.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py diff --git a/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py b/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py new file mode 100644 index 00000000000..de54e344a32 --- /dev/null +++ b/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py @@ -0,0 +1,141 @@ +"""Montana's itemized mortgage interest is the amount federal law allows. + +Former MCA 15-30-2131(1)(a) allowed the items in 26 U.S.C. 161 (which +includes section 163 interest), and MCA 15-30-2101 reads "Internal Revenue +Code" as the Code as amended. The Form 2 instructions for itemized deductions +line 9 (2018-2023) say to enter the home mortgage interest "allowed by federal +law" and that interest on acquisition debt incurred after Dec. 15, 2017 "is +limited to the first $750,000". From 2024 Montana starts from the federal +itemized deductions. So interest on acquisition debt above the 26 U.S.C. +163(h)(3) caps is never a Montana deduction. + +A grid of couples with one acquisition mortgage (balance, origination year, +interest) shares one simulation per year. The other Montana itemized +components are set to zero so each variable is its mortgage interest line, +and the federal limit is restated here independently of the parameter files: +$1,000,000 of debt incurred before the TCJA date ($750,000 after), with +interest prorated by the share of the balance under the cap. PolicyEngine +approximates the Dec. 15, 2017 date by origination year 2017, and treats a +mortgage with no reported balance as fully deductible; the restatement uses +the same input conventions. + +1. Joint returns (every year): the Montana joint itemized deductions, and the + variant used for the federal itemization choice, equal the allowed amount. +2. Spouses filing separately on the same form (through 2023): each spouse's + deduction is the allowed amount times the share of the interest that + spouse reported, and the two sum to the allowed amount. +""" + +from itertools import product + +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEARS = range(2021, 2027) +BALANCES = (0, 300_000, 750_000, 1_000_000, 2_500_000) +ORIGINATION_YEARS = (2005, 2017, 2018, 2024) +INTERESTS = (0, 15_000, 40_000) +SPOUSE_SHARES = (0.0, 0.25) # share of the interest the spouse reported +UNITS = tuple(product(BALANCES, ORIGINATION_YEARS, INTERESTS, SPOUSE_SHARES)) + +ZERO_PERSON_COMPONENTS = ( + "mt_misc_deductions", + "mt_medical_expense_deduction_joint", + "mt_medical_expense_deduction_indiv", + "mt_salt_deduction", + "mt_federal_income_tax_deduction_indiv", + "mt_federal_income_tax_deduction_for_federal_itemization_indiv", +) +ZERO_TAX_UNIT_COMPONENTS = ( + "charitable_deduction", + "mt_federal_income_tax_deduction_unit", + "mt_federal_income_tax_deduction_for_federal_itemization", +) + + +def allowed_interest(interest, balance, origination_year): + """Home mortgage interest allowed under 26 U.S.C. 163(h)(3).""" + if balance == 0: + return interest + cap = 1_000_000 if origination_year <= 2017 else 750_000 + return interest * min(1, cap / balance) + + +def _situation(year): + period = str(year) + people, tax_units, households, marital_units = {}, {}, {}, {} + for i, (balance, origination, interest, spouse_share) in enumerate(UNITS): + head, spouse = f"u{i}_head", f"u{i}_spouse" + for name, role, share in ( + (head, "is_tax_unit_head", 1 - spouse_share), + (spouse, "is_tax_unit_spouse", spouse_share), + ): + people[name] = { + "age": {period: 50}, + role: {period: True}, + "home_mortgage_interest": {period: interest * share}, + **{v: {period: 0} for v in ZERO_PERSON_COMPONENTS}, + } + members = [head, spouse] + tax_units[f"u{i}"] = { + "members": members, + "first_home_mortgage_balance": {period: balance}, + "first_home_mortgage_origination_year": {period: origination}, + **{v: {period: 0} for v in ZERO_TAX_UNIT_COMPONENTS}, + } + households[f"u{i}"] = {"members": members, "state_code": {period: "MT"}} + marital_units[f"u{i}"] = {"members": members} + return { + "people": people, + "tax_units": tax_units, + "households": households, + "marital_units": marital_units, + } + + +@pytest.mark.parametrize("year", YEARS) +def test_montana_mortgage_interest_is_the_federally_allowed_amount(year): + sim = Simulation(situation=_situation(year)) + + def per_person(variable): + return np.asarray(sim.calculate(variable, year), dtype=float).reshape(-1, 2) + + allowed = np.array( + [ + allowed_interest(interest, balance, origination) + for balance, origination, interest, _ in UNITS + ] + ) + spouse_share = np.array([unit[3] for unit in UNITS]) + + # 1. Joint returns. The head-only units (spouse share 0) keep this check + # independent of how the joint formula splits interest between spouses. + head_only = spouse_share == 0 + for variable in ( + "mt_itemized_deductions_joint", + "mt_itemized_deductions_for_federal_itemization_joint", + ): + np.testing.assert_allclose( + per_person(variable).sum(axis=1)[head_only], + allowed[head_only], + atol=0.01, + err_msg=f"{variable} in {year}", + ) + + # 2. Spouses filing separately on the same form. + p = sim.tax_benefit_system.parameters(year).gov.states.mt.tax.income + if not p.married_filing_separately_on_same_return_allowed: + return + expected = np.stack([allowed * (1 - spouse_share), allowed * spouse_share], 1) + for variable in ( + "mt_itemized_deductions_indiv", + "mt_itemized_deductions_for_federal_itemization_indiv", + ): + np.testing.assert_allclose( + per_person(variable), + expected, + atol=0.01, + err_msg=f"{variable} in {year}", + ) From 55bfdab25a7d7134c9982672918e04d8158c6eaa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 09:29:52 -0400 Subject: [PATCH 4/7] Give each Maryland spouse their own losses when the couple has no gross income Worksheet 13D line 1 takes "the portion of federal adjusted gross income ... attributable to each spouse". Gross income never goes below zero here, so with no gross income the couple's AGI is their losses and above-the-line deductions. md_two_income_subtraction gave all of it to the head. When one spouse had the whole loss that was the legal answer, because the subtraction takes the smaller spouse. But when both spouses had losses it was not: two spouses with -$20,000 each and $35,000 each of Maryland additions get $1,200 by the worksheet and $0 on main. Each spouse's portion now follows their own losses (the person-level sources of loss_ald) and person-level deductions, with deductions recorded only for the tax unit split equally. Also from the review of #10004: Montana's separate-on-the-same-form cases state the joint federal filing status and the cap assumption, a federal separate return's $375,000 cap gets its own case, va_eitc's documentation describes the applied credit, and the removed Virginia variables are listed in a changelog fragment. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-state-head-fallbacks.fixed.md | 2 +- .../fix-state-head-fallbacks.removed.md | 1 + .../md_two_income_subtraction.yaml | 107 ++++++++++++++++++ ...uctions_for_federal_itemization_indiv.yaml | 13 ++- ...uctions_for_federal_itemization_joint.yaml | 29 +++++ .../mt_itemized_deductions_indiv.yaml | 13 ++- .../mt_itemized_deductions_joint.yaml | 29 +++++ .../va/tax/income/credits/eitc/va_eitc.yaml | 5 +- .../subtractions/md_two_income_subtraction.py | 41 ++++++- .../va/tax/income/credits/eitc/va_eitc.py | 12 +- 10 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 changelog.d/fix-state-head-fallbacks.removed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml diff --git a/changelog.d/fix-state-head-fallbacks.fixed.md b/changelog.d/fix-state-head-fallbacks.fixed.md index 047e13c35b9..061b1b2b93b 100644 --- a/changelog.d/fix-state-head-fallbacks.fixed.md +++ b/changelog.d/fix-state-head-fallbacks.fixed.md @@ -1 +1 @@ -Montana itemized deductions count only federally deductible mortgage interest, so interest on acquisition debt above the IRC 163(h)(3) caps is no longer deducted. The Virginia EITC reports the whole credit the tax applies for spouses filing separately, not only the share of the spouse listed as head. +Montana itemized deductions count only federally deductible mortgage interest, so interest on acquisition debt above the IRC 163(h)(3) caps is no longer deducted. The Virginia EITC reports the credit the tax applies for spouses filing separately, not only the share of the spouse listed as head. Maryland's two-income subtraction gives each spouse their own losses and deductions when the couple has no gross income, instead of giving all of them to the head. diff --git a/changelog.d/fix-state-head-fallbacks.removed.md b/changelog.d/fix-state-head-fallbacks.removed.md new file mode 100644 index 00000000000..8e41282ac40 --- /dev/null +++ b/changelog.d/fix-state-head-fallbacks.removed.md @@ -0,0 +1 @@ +Removed `va_eitc_person` and `va_agi_share`, which fed only the head-share branch of `va_eitc`; `va_eitc` now reports the tax unit's applied Virginia EITC for every filing status. diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml new file mode 100644 index 00000000000..306e9a48cac --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml @@ -0,0 +1,107 @@ +# Worksheet 13D (2025 resident booklet, p. 18) adds each spouse's portion of +# federal AGI (line 1) and of the Maryland additions (line 2), subtracts each +# spouse's subtractions (line 4), and takes the smaller result, at least zero +# and at most $1,200. In these couples neither spouse has gross income, so +# their federal AGI is their own losses and deductions. md_total_additions is +# an input (it has no formula); the formula splits it equally, as in a couple +# who each own half of it. + +- name: Both spouses have losses, so each keeps their own + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + self_employment_income: -20_000 + spouse: + age: 45 + is_tax_unit_spouse: true + self_employment_income: -20_000 + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 70_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Line 5 for each spouse: -20,000 + 35,000 = 15,000. Line 7: the + # smaller, 15,000, capped at 1,200. + adjusted_gross_income: -40_000 + md_two_income_subtraction: 1_200 + +- name: One spouse has the whole loss + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + spouse: + age: 45 + is_tax_unit_spouse: true + self_employment_income: -10_000 + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 12_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Line 5: head 0 + 6,000 = 6,000; spouse -10,000 + 6,000 = -4,000. + # Line 6: the smaller, not less than zero, is 0. + md_two_income_subtraction: 0 + +- name: The same couple with the head and spouse labels exchanged + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + self_employment_income: -10_000 + spouse: + age: 45 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 12_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + md_two_income_subtraction: 0 + +- name: One spouse paid all the student loan interest + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_tax_unit_head: true + student_loan_interest: 2_000 + spouse: + age: 30 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 6_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Line 5: head -2,000 + 3,000 = 1,000; spouse 0 + 3,000 = 3,000. + adjusted_gross_income: -2_000 + md_two_income_subtraction: 1_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml index d50865834e9..291bb803808 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml @@ -75,7 +75,7 @@ output: mt_itemized_deductions_for_federal_itemization_indiv: [1_000, 700, 0] -- name: Spouses filing separately split the federally capped mortgage interest +- name: Spouses filing separately on the same form split the federally capped mortgage interest of their joint federal return period: 2022 absolute_error_margin: 0.01 input: @@ -100,13 +100,18 @@ first_home_mortgage_balance: 1_000_000 first_home_mortgage_origination_year: 2019 charitable_deduction: 0 + filing_status: JOINT households: household: members: [head, spouse] state_code: MT output: - # $30,000 of the $40,000 interest is allowed by federal law (the - # $750,000 cap on $1,000,000 of post-2017 debt). Each spouse reported a - # share of the interest and deducts that share of the allowed amount. + # $30,000 of the $40,000 interest is allowed by federal law on the joint + # federal return (the $750,000 cap on $1,000,000 of post-2017 debt). + # Each spouse reported a share of the interest and deducts that share + # of the allowed amount. No Montana source says whether spouses filing + # separately on the same form each get the $375,000 separate cap; the + # instructions let them split jointly paid expenses in any proportion + # (2023 Form 2 instructions, p. 13), so the joint federal amount is used. deductible_mortgage_interest: [22_500, 7_500] mt_itemized_deductions_for_federal_itemization_indiv: [22_500, 7_500] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml index d3dbd04270a..85519179428 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml @@ -141,3 +141,32 @@ # $40,000 is allowed. deductible_mortgage_interest: [40_000, 0] mt_itemized_deductions_for_federal_itemization_joint: [40_000, 0] + +- name: A federal separate return uses the $375,000 cap + period: 2022 + absolute_error_margin: 0.01 + input: + people: + filer: + is_tax_unit_head: true + home_mortgage_interest: 40_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [filer] + filing_status: SEPARATE + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2019 + charitable_deduction: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [filer] + state_code: MT + output: + # 26 U.S.C. 163(h)(3)(F)(i)(II): $375,000 for a married individual + # filing a separate return, so $15,000 of the $40,000 is allowed. + deductible_mortgage_interest: 15_000 + mt_itemized_deductions_for_federal_itemization_joint: 15_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml index 0b83bfadd6a..ac0685090a1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml @@ -75,7 +75,7 @@ output: mt_itemized_deductions_indiv: [1_000, 700, 0] -- name: Spouses filing separately split the federally capped mortgage interest +- name: Spouses filing separately on the same form split the federally capped mortgage interest of their joint federal return period: 2022 absolute_error_margin: 0.01 input: @@ -100,13 +100,18 @@ first_home_mortgage_balance: 1_000_000 first_home_mortgage_origination_year: 2019 charitable_deduction: 0 + filing_status: JOINT households: household: members: [head, spouse] state_code: MT output: - # $30,000 of the $40,000 interest is allowed by federal law (the - # $750,000 cap on $1,000,000 of post-2017 debt). Each spouse reported a - # share of the interest and deducts that share of the allowed amount. + # $30,000 of the $40,000 interest is allowed by federal law on the joint + # federal return (the $750,000 cap on $1,000,000 of post-2017 debt). + # Each spouse reported a share of the interest and deducts that share + # of the allowed amount. No Montana source says whether spouses filing + # separately on the same form each get the $375,000 separate cap; the + # instructions let them split jointly paid expenses in any proportion + # (2023 Form 2 instructions, p. 13), so the joint federal amount is used. deductible_mortgage_interest: [22_500, 7_500] mt_itemized_deductions_indiv: [22_500, 7_500] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml index f82bb4e07d1..8289ef0c24c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml @@ -141,3 +141,32 @@ # $40,000 is allowed. deductible_mortgage_interest: [40_000, 0] mt_itemized_deductions_joint: [40_000, 0] + +- name: A federal separate return uses the $375,000 cap + period: 2022 + absolute_error_margin: 0.01 + input: + people: + filer: + is_tax_unit_head: true + home_mortgage_interest: 40_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [filer] + filing_status: SEPARATE + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2019 + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + households: + household: + members: [filer] + state_code: MT + output: + # 26 U.S.C. 163(h)(3)(F)(i)(II): $375,000 for a married individual + # filing a separate return, so $15,000 of the $40,000 is allowed. + deductible_mortgage_interest: 15_000 + mt_itemized_deductions_joint: 15_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml index d016cdf73a2..31a057b6f5c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/credits/eitc/va_eitc.yaml @@ -64,8 +64,9 @@ # The spouses split the Virginia EIC by their shares of the earned income # used for the federal EITC and claim those shares on their two separate # returns, and only one of them may claim the Credit for Low-Income - # Individuals (2021 Form 760 instructions, p. 32), so together they claim - # the whole credit. It does not depend on which spouse is listed first. + # Individuals (2021 Form 760 instructions, p. 32). The tax unit holds both + # returns, so va_eitc reports the credit its tax applies, the sum of the + # shares, whichever spouse is listed first. The credit amounts are inputs. va_eitc: 100 - name: The same couple with the head and spouse labels exchanged diff --git a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py index 22b60ac4090..7720c402b8b 100644 --- a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py +++ b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py @@ -1,6 +1,18 @@ from policyengine_us.model_api import * +# Person-level sources of the business and capital losses in loss_ald. +LOSS_SOURCES = [ + "total_self_employment_income", + "farm_operations_income", + "rental_income", + "farm_rent_income", + "estate_income", + "partnership_s_corp_income", + "capital_gains", +] + + class md_two_income_subtraction(Variable): value_type = float entity = TaxUnit @@ -10,6 +22,9 @@ class md_two_income_subtraction(Variable): reference = ( "https://interactive.marylandtaxes.gov/Individuals/iFile_ChooseForm/PriorYearForms/Resident_Booklet_2021.pdf#page=16", "https://mgaleg.maryland.gov/mgawebsite/Laws/StatuteText?article=gtg§ion=10-207&enactments=false", + "https://www.marylandcomptroller.gov/content/dam/mdcomp/tax/instructions/2025/resident-booklet.pdf#page=18", + # Worksheet 13D line 1: "the portion of federal adjusted gross income + # from Line 1 of Form 502 attributable to each spouse" ) defined_for = StateCode.MD @@ -24,9 +39,29 @@ def formula(tax_unit, period, parameters): is_spouse = person("is_tax_unit_spouse", period) head_gross_income = tax_unit.sum(where(is_head, gross_income, 0)) couple_gross_income = tax_unit.sum(where(is_head | is_spouse, gross_income, 0)) - # Compute the head's share of the couple's cross income. - # Use a mask rather than where to avoid a divide-by-zero warning. Default to one. - head_frac = np.ones_like(couple_gross_income) + # Without gross income, the couple's AGI is made up of losses and + # above-the-line deductions, and each spouse's portion follows the + # losses and deductions that are that spouse's own. Deductions + # recorded only for the tax unit are split equally, as in + # adjusted_gross_income_person. Use masks rather than where to avoid + # divide-by-zero warnings. + filer = is_head | is_spouse + own_items = filer * sum( + max_(0, -person(source, period)) for source in LOSS_SOURCES + ) + for deduction in sorted(parameters(period).gov.irs.ald.deductions): + if deduction == "loss_ald": + continue + variable = person.entity.get_variable(deduction, check_existence=True) + if variable.entity.is_person: + own_items = own_items + filer * person(deduction, period) + couple_items = tax_unit.sum(own_items) + head_frac = np.full_like(couple_gross_income, 0.5) + has_items = couple_items > 0 + head_frac[has_items] = ( + tax_unit.sum(is_head * own_items)[has_items] / couple_items[has_items] + ) + # With gross income, use the head's share of the couple's gross income. mask = couple_gross_income > 0 head_frac[mask] = head_gross_income[mask] / couple_gross_income[mask] head_us_agi = head_frac * us_agi diff --git a/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py b/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py index 12ed37aef99..2dafb5b97ff 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py +++ b/policyengine_us/variables/gov/states/va/tax/income/credits/eitc/va_eitc.py @@ -7,11 +7,13 @@ class va_eitc(Variable): label = "Virginia Earned Income Tax Credit" unit = USD documentation = ( - "Refundable or non-refundable Virginia EITC. Spouses filing separately " - "split the Virginia EIC by their shares of the earned income used for " - "the federal EITC, and only one of them may claim the Credit for " - "Low-Income Individuals, so their returns together claim the whole " - "credit. This is the amount the tax unit's income tax applies." + "The Virginia EITC the tax unit's income tax applies: the refundable " + "credit if it is claimed, otherwise the non-refundable credit after " + "its tax liability limit. Spouses filing separately split the " + "Virginia EIC by their shares of the earned income used for the " + "federal EITC, and only one of them may claim the Credit for " + "Low-Income Individuals, so the credit for a tax unit holding both " + "spouses is the sum of their shares." ) definition_period = YEAR reference = ( From 880058056e761f2faf548f331986fb296f7c2346 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 10:23:42 -0400 Subject: [PATCH 5/7] Split Maryland zero-income couples' allowed losses by kind, and the rest equally The re-review of #10004 found two gaps in the previous commit. It weighted by raw losses, so a spouse with a -$100,000 capital loss (only $3,000 allowed) took most of the couple's -$23,000 AGI: $833 instead of the worksheet's $0. And it scaled deductions recorded only for the tax unit by the spouses' own items instead of splitting them equally. With no gross income, md_two_income_subtraction now builds each spouse's portion of federal AGI from: - the allowed business and capital parts of loss_ald (the capital part is limited_capital_loss plus capital_losses_allowed_against_gains), each split by the spouses' own losses of that kind; - their own person-level above-the-line deductions; - half of whatever is recorded only for the tax unit. New YAML cases cover the capped capital loss under both labellings and a tax-unit HSA deduction. 600 random zero-income couples give the same result under either labelling. The Montana property test now checks the model's separate-filing switch against the 2023 boundary instead of skipping when it is off, and says what the grid catches that the YAML cases do not. Co-Authored-By: Claude Opus 5.5 --- ...tgage_interest_federal_limit_properties.py | 20 ++++- .../md_two_income_subtraction.yaml | 81 +++++++++++++++++++ .../subtractions/md_two_income_subtraction.py | 80 ++++++++++++------ 3 files changed, 155 insertions(+), 26 deletions(-) diff --git a/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py b/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py index de54e344a32..17700756dd7 100644 --- a/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py +++ b/policyengine_us/tests/core/test_mt_mortgage_interest_federal_limit_properties.py @@ -24,6 +24,12 @@ 2. Spouses filing separately on the same form (through 2023): each spouse's deduction is the allowed amount times the share of the interest that spouse reported, and the two sum to the allowed amount. + +The YAML cases check single points of this rule. The grid checks it for every +combination of balance, origination year, interest, the spouse's share, year +and filing path against the restatement above, so a change in the caps, the +grandfathering year, the proration or the spouse split is caught wherever it +binds. """ from itertools import product @@ -34,6 +40,10 @@ from policyengine_us import Simulation YEARS = range(2021, 2027) +# Montana ended filing separately on the same form from tax year 2024 (SB 399 +# of 2021; see the references in +# married_filing_separately_on_same_return_allowed.yaml). +LAST_SEPARATE_ON_SAME_FORM_YEAR = 2023 BALANCES = (0, 300_000, 750_000, 1_000_000, 2_500_000) ORIGINATION_YEARS = (2005, 2017, 2018, 2024) INTERESTS = (0, 15_000, 40_000) @@ -124,9 +134,15 @@ def per_person(variable): err_msg=f"{variable} in {year}", ) - # 2. Spouses filing separately on the same form. + # 2. Spouses filing separately on the same form, through 2023. Check the + # model's switch against that boundary, so turning it off by mistake + # fails here rather than skipping these checks. p = sim.tax_benefit_system.parameters(year).gov.states.mt.tax.income - if not p.married_filing_separately_on_same_return_allowed: + separate_allowed = year <= LAST_SEPARATE_ON_SAME_FORM_YEAR + assert bool(p.married_filing_separately_on_same_return_allowed) == ( + separate_allowed + ) + if not separate_allowed: return expected = np.stack([allowed * (1 - spouse_share), allowed * spouse_share], 1) for variable in ( diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml index 306e9a48cac..ca07fbd6327 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml @@ -105,3 +105,84 @@ # Line 5: head -2,000 + 3,000 = 1,000; spouse 0 + 3,000 = 3,000. adjusted_gross_income: -2_000 md_two_income_subtraction: 1_000 + +- name: Each spouse keeps their own allowed loss when a capital loss exceeds the limit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + long_term_capital_gains: -100_000 + spouse: + age: 45 + is_tax_unit_spouse: true + self_employment_income: -20_000 + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 40_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Federal AGI is -23,000: the $20,000 business loss and $3,000 of the + # capital loss (26 U.S.C. 1211(b)). Line 5: head -3,000 + 20,000 = + # 17,000; spouse -20,000 + 20,000 = 0. Line 6: 0. + adjusted_gross_income: -23_000 + md_two_income_subtraction: 0 + +- name: The same couple with the head and spouse labels exchanged + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + self_employment_income: -20_000 + spouse: + age: 45 + is_tax_unit_spouse: true + long_term_capital_gains: -100_000 + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 40_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + md_two_income_subtraction: 0 + +- name: A deduction recorded only for the tax unit is split equally + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_tax_unit_head: true + student_loan_interest: 2_000 + spouse: + age: 30 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + health_savings_account_ald: 2_000 + md_total_additions: 7_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Federal AGI is -4,000. The head's own student loan interest is the + # head's; the HSA deduction is recorded only for the tax unit, so each + # spouse takes half. Line 5: head -2,000 - 1,000 + 3,500 = 500; spouse + # -1,000 + 3,500 = 2,500. Line 6: 500. + adjusted_gross_income: -4_000 + md_two_income_subtraction: 500 diff --git a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py index 7720c402b8b..50ab26b662c 100644 --- a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py +++ b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py @@ -1,18 +1,27 @@ from policyengine_us.model_api import * -# Person-level sources of the business and capital losses in loss_ald. -LOSS_SOURCES = [ +# Person-level sources of the business losses in loss_ald. +BUSINESS_LOSS_SOURCES = [ "total_self_employment_income", "farm_operations_income", "rental_income", "farm_rent_income", "estate_income", "partnership_s_corp_income", - "capital_gains", ] +def _head_part(amount, head_own, spouse_own): + """The head's part of an amount split by the spouses' own amounts, or + equally when neither has any.""" + total = head_own + spouse_own + frac = np.full_like(total, 0.5) + mask = total > 0 + frac[mask] = head_own[mask] / total[mask] + return amount * frac + + class md_two_income_subtraction(Variable): value_type = float entity = TaxUnit @@ -39,33 +48,56 @@ def formula(tax_unit, period, parameters): is_spouse = person("is_tax_unit_spouse", period) head_gross_income = tax_unit.sum(where(is_head, gross_income, 0)) couple_gross_income = tax_unit.sum(where(is_head | is_spouse, gross_income, 0)) - # Without gross income, the couple's AGI is made up of losses and - # above-the-line deductions, and each spouse's portion follows the - # losses and deductions that are that spouse's own. Deductions - # recorded only for the tax unit are split equally, as in - # adjusted_gross_income_person. Use masks rather than where to avoid - # divide-by-zero warnings. + # Compute the head's share of the couple's gross income. + # Use a mask rather than where to avoid a divide-by-zero warning. + head_frac = np.ones_like(couple_gross_income) + mask = couple_gross_income > 0 + head_frac[mask] = head_gross_income[mask] / couple_gross_income[mask] + # Gross income is never below zero (losses are deducted in loss_ald), + # so without gross income the couple's AGI is their allowed losses and + # above-the-line deductions, and each spouse's portion is the part + # that is their own. The allowed business and capital losses in + # loss_ald are each split by the spouses' own losses of that kind, + # and person-level deductions go to their owner. Whatever is recorded + # only for the tax unit is split equally. filer = is_head | is_spouse - own_items = filer * sum( - max_(0, -person(source, period)) for source in LOSS_SOURCES + own_business = filer * sum( + max_(0, -person(source, period)) for source in BUSINESS_LOSS_SOURCES + ) + unit_business = max_(0, -tax_unit("other_net_gain", period)) / 2 + own_capital = filer * person("capital_losses", period) + capital_part = add( + tax_unit, + period, + ["limited_capital_loss", "capital_losses_allowed_against_gains"], ) + loss_ald = tax_unit("loss_ald", period) + business_part = max_(0, loss_ald - capital_part) + head_loss = _head_part( + business_part, + tax_unit.sum(is_head * own_business) + unit_business, + tax_unit.sum(is_spouse * own_business) + unit_business, + ) + _head_part( + capital_part, + tax_unit.sum(is_head * own_capital), + tax_unit.sum(is_spouse * own_capital), + ) + spouse_loss = loss_ald - head_loss + own_deductions = 0 for deduction in sorted(parameters(period).gov.irs.ald.deductions): - if deduction == "loss_ald": - continue variable = person.entity.get_variable(deduction, check_existence=True) if variable.entity.is_person: - own_items = own_items + filer * person(deduction, period) - couple_items = tax_unit.sum(own_items) - head_frac = np.full_like(couple_gross_income, 0.5) - has_items = couple_items > 0 - head_frac[has_items] = ( - tax_unit.sum(is_head * own_items)[has_items] / couple_items[has_items] + own_deductions = own_deductions + filer * person(deduction, period) + head_own = -head_loss - tax_unit.sum(is_head * own_deductions) + spouse_own = -spouse_loss - tax_unit.sum(is_spouse * own_deductions) + unattributed = us_agi - head_own - spouse_own + no_gross_income = couple_gross_income <= 0 + head_us_agi = where( + no_gross_income, head_own + unattributed / 2, head_frac * us_agi + ) + spouse_us_agi = where( + no_gross_income, spouse_own + unattributed / 2, (1 - head_frac) * us_agi ) - # With gross income, use the head's share of the couple's gross income. - mask = couple_gross_income > 0 - head_frac[mask] = head_gross_income[mask] / couple_gross_income[mask] - head_us_agi = head_frac * us_agi - spouse_us_agi = (1 - head_frac) * us_agi # compute head and spouse MD AGI additions using ad hoc rule total_additions = tax_unit("md_total_additions", period) From 74890ccec113883f60d7847adbab1177accbea16 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 17:44:50 -0400 Subject: [PATCH 6/7] Give Maryland spouses the deductions their person-level amounts record Re-review 3 of #10004: alimony, educator expense and HSA deductions are tax-unit variables with person-level counterparts that record whose they are, but the zero-income fallback split them equally. One spouse paying $2,000 of deductible alimony then reached the $1,200 cap instead of the worksheet's $1,000. Each such deduction is now split by the spouses' counterpart amounts, and equally only when neither has one, as adjusted_gross_income_person does in #9890. New YAML cases cover alimony (both labellings), educator expenses and an HSA deduction with a recorded owner; main already got these right. Co-Authored-By: Claude Opus 5.5 --- .../md_two_income_subtraction.yaml | 103 ++++++++++++++++++ .../subtractions/md_two_income_subtraction.py | 28 ++++- 2 files changed, 125 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml index ca07fbd6327..0fdd8ccf8d0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.yaml @@ -186,3 +186,106 @@ # -1,000 + 3,500 = 2,500. Line 6: 500. adjusted_gross_income: -4_000 md_two_income_subtraction: 500 + +- name: Deductible alimony paid by one spouse is that spouse's + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_tax_unit_head: true + alimony_expense: 2_000 + divorce_year: 2018 + spouse: + age: 50 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 6_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Line 5: head -2,000 + 3,000 = 1,000; spouse 3,000. An equal split of + # the alimony deduction would give 2,000 each and the $1,200 cap. + adjusted_gross_income: -2_000 + md_two_income_subtraction: 1_000 + +- name: The same alimony couple with the head and spouse labels exchanged + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_tax_unit_head: true + spouse: + age: 50 + is_tax_unit_spouse: true + alimony_expense: 2_000 + divorce_year: 2018 + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 6_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + md_two_income_subtraction: 1_000 + +- name: Educator expenses are the educator's + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_tax_unit_head: true + spouse: + age: 40 + is_tax_unit_spouse: true + educator_expense: 300 + tax_units: + tax_unit: + members: [head, spouse] + md_total_additions: 1_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Line 5: head 500; spouse -300 + 500 = 200. + adjusted_gross_income: -300 + md_two_income_subtraction: 200 + +- name: An HSA deduction with a recorded owner is that spouse's + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_tax_unit_head: true + spouse: + age: 30 + is_tax_unit_spouse: true + student_loan_interest: 2_000 + health_savings_account_ald_person: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + health_savings_account_ald: 2_000 + md_total_additions: 7_000 + households: + household: + members: [head, spouse] + state_code: MD + output: + # Line 5: head 3,500; spouse -2,000 - 2,000 + 3,500 = -500. Line 6: 0. + adjusted_gross_income: -4_000 + md_two_income_subtraction: 0 diff --git a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py index 50ab26b662c..1de24a86feb 100644 --- a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py +++ b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py @@ -58,8 +58,8 @@ def formula(tax_unit, period, parameters): # above-the-line deductions, and each spouse's portion is the part # that is their own. The allowed business and capital losses in # loss_ald are each split by the spouses' own losses of that kind, - # and person-level deductions go to their owner. Whatever is recorded - # only for the tax unit is split equally. + # and deductions go to their owner where a person-level amount + # records one. Whatever has no owner is split equally. filer = is_head | is_spouse own_business = filer * sum( max_(0, -person(source, period)) for source in BUSINESS_LOSS_SOURCES @@ -83,13 +83,29 @@ def formula(tax_unit, period, parameters): tax_unit.sum(is_spouse * own_capital), ) spouse_loss = loss_ald - head_loss - own_deductions = 0 + # A tax-unit deduction with a person-level counterpart is split by + # the spouses' own amounts, or equally when neither has one. + head_deductions = 0 + spouse_deductions = 0 for deduction in sorted(parameters(period).gov.irs.ald.deductions): variable = person.entity.get_variable(deduction, check_existence=True) if variable.entity.is_person: - own_deductions = own_deductions + filer * person(deduction, period) - head_own = -head_loss - tax_unit.sum(is_head * own_deductions) - spouse_own = -spouse_loss - tax_unit.sum(is_spouse * own_deductions) + amount = filer * person(deduction, period) + head_deductions = head_deductions + tax_unit.sum(is_head * amount) + spouse_deductions = spouse_deductions + tax_unit.sum(is_spouse * amount) + continue + counterpart = f"{deduction}_person" + if person.entity.get_variable(counterpart) is None: + continue + own = filer * person(counterpart, period) + unit_amount = tax_unit(deduction, period) + head_part = _head_part( + unit_amount, tax_unit.sum(is_head * own), tax_unit.sum(is_spouse * own) + ) + head_deductions = head_deductions + head_part + spouse_deductions = spouse_deductions + unit_amount - head_part + head_own = -head_loss - head_deductions + spouse_own = -spouse_loss - spouse_deductions unattributed = us_agi - head_own - spouse_own no_gross_income = couple_gross_income <= 0 head_us_agi = where( From 95a1e88c201cdd217cad5eb7d68da7f9e7533c6f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 17:59:54 -0400 Subject: [PATCH 7/7] Sum Maryland business losses in a loop, not built-in sum() over entity calls test_no_builtin_sum_over_entity_calls failed CI at 880058056e. The loop adds the same arrays, so results are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../income/agi/subtractions/md_two_income_subtraction.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py index 1de24a86feb..8db3537a2d3 100644 --- a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py +++ b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py @@ -61,9 +61,10 @@ def formula(tax_unit, period, parameters): # and deductions go to their owner where a person-level amount # records one. Whatever has no owner is split equally. filer = is_head | is_spouse - own_business = filer * sum( - max_(0, -person(source, period)) for source in BUSINESS_LOSS_SOURCES - ) + own_business = 0 + for source in BUSINESS_LOSS_SOURCES: + own_business = own_business + max_(0, -person(source, period)) + own_business = filer * own_business unit_business = max_(0, -tax_unit("other_net_gain", period)) / 2 own_capital = filer * person("capital_losses", period) capital_part = add(