From 52ffe10226cd26b3ff24e48ce7f9bc555924dd07 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:08:30 -0400 Subject: [PATCH 01/13] Align Montana separate-filing allocations and the elderly credit with the forms - Federal income tax deduction (through 2023): attribute federal tax to the spouse who earned the income and cap each spouse at $5,000, instead of prorating the jointly capped amount by Montana AGI (2022 Form 2 instructions, Itemized Deductions Schedule, line 4). - Child and dependent care expense deduction (through 2023): phase out on the couple's combined Montana AGI and split the result equally only for spouses filing separately on the same form; a single filer and a joint return take the whole deduction (Form 2441-M lines 3-7). - Elderly homeowner/renter credit: count federal AGI without regard to losses (MCA 15-30-2337(9)(a); Form 2EC line 9). Co-Authored-By: Claude Opus 5.5 --- ...ix-mt-separate-filing-allocations.fixed.md | 1 + .../deductions/itemized/misc_deductions.yaml | 7 +- ...mt_elderly_homeowner_or_renter_credit.yaml | 31 ++++ ..._renter_credit_gross_household_income.yaml | 47 ++++++ ...hild_dependent_care_expense_deduction.yaml | 134 ++++++++++++++---- ...duction_for_federal_itemization_indiv.yaml | 27 ++-- ...uctions_for_federal_itemization_joint.yaml | 6 +- ...mt_federal_income_tax_deduction_indiv.yaml | 103 ++++++++++++-- .../mt_itemized_deductions_joint.yaml | 6 +- ...or_renter_credit_gross_household_income.py | 16 ++- ..._child_dependent_care_expense_deduction.py | 44 +++--- ...are_expense_deduction_eligible_expenses.py | 34 +++++ ..._dependent_care_expense_deduction_joint.py | 30 ++++ ...deduction_for_federal_itemization_indiv.py | 26 ++-- ...eductions_for_federal_itemization_indiv.py | 1 + ...eductions_for_federal_itemization_joint.py | 1 + .../mt_federal_income_tax_deduction_indiv.py | 31 ++-- .../mt_federal_income_tax_deduction_share.py | 33 +++++ .../general/mt_itemized_deductions_indiv.py | 1 + .../general/mt_itemized_deductions_joint.py | 1 + 20 files changed, 472 insertions(+), 108 deletions(-) create mode 100644 changelog.d/fix-mt-separate-filing-allocations.fixed.md create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_eligible_expenses.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_share.py diff --git a/changelog.d/fix-mt-separate-filing-allocations.fixed.md b/changelog.d/fix-mt-separate-filing-allocations.fixed.md new file mode 100644 index 00000000000..c7d3be16f20 --- /dev/null +++ b/changelog.d/fix-mt-separate-filing-allocations.fixed.md @@ -0,0 +1 @@ +Montana's federal income tax deduction for spouses filing separately on the same form now attributes federal tax to the spouse who earned the income and caps each spouse at 5,000; the child and dependent care expense deduction phases out on the couple's combined Montana AGI and is halved only for spouses filing separately on the same form; and the elderly homeowner/renter credit counts income without regard to losses. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml index 1eeb0351b1a..c4744269768 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml @@ -1,8 +1,11 @@ description: Montana counts these sources as miscellaneous deductions. +# The child and dependent care expense deduction is also a miscellaneous +# deduction (Form 2 Itemized Deductions Schedule, line 14). It is added +# separately because its income limit uses the return's own Montana AGI, which +# differs between a joint return and separate columns (Form 2441-M line 3). values: 2021-01-01: - - mt_child_dependent_care_expense_deduction - - casualty_loss_deduction + - casualty_loss_deduction 2024-01-01: [] metadata: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index 9e4f2540b71..154bf57d561 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -223,3 +223,34 @@ # Same household, larger exclusion ($12,600) leaves a smaller expected # contribution, so the credit is higher: $539.67. mt_elderly_homeowner_or_renter_credit: 539.67 + +- name: 2023 Form 2EC worked case, a capital loss does not reduce gross household income + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: -3_000 + social_security_retirement: 10_000 + real_estate_taxes: 2_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Line 1 federal AGI 17_000 (Social Security untaxed: 17_000 + 5_000 is + # under the 25_000 base); line 5 untaxed Social Security 10_000; line 9 + # losses 3_000; line 10 gross household income 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + # Line 12 = 30_000 - 12_600 = 17_400; line 13 multiplier 0.05; + # line 14 = 870. + mt_elderly_homeowner_or_renter_credit_net_household_income: 870 + # Line 18 = 2_000; line 19 = 1_130; line 20 = min(1_130, 1_150); + # line 21 = 100% (under 35_000); line 22 = 1_130. + mt_elderly_homeowner_or_renter_credit: 1_130 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml index cee870a82af..f7d12a544dc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml @@ -37,3 +37,50 @@ # § 15-30-2337(9)(a)(viii) requires all SS to be counted, even the # portion excluded from federal AGI. mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + +- name: A capital loss deducted in federal AGI is added back + period: 2023 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: -3_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # § 15-30-2337(9)(a): federal AGI "without regard to loss". 2023 Form 2EC: + # line 1 federal AGI 20_000 - 3_000 = 17_000; line 9 adds back the 3_000 + # capital loss; line 10 = 20_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 + +- name: A business loss deducted in federal AGI is added back once for the return + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + mt_refundable_credits_before_renter_credit: 0 + person2: + age: 68 + self_employment_income: -5_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Federal AGI 20_000 - 5_000 = 15_000; the 5_000 loss is added back, so + # gross household income is 20_000 (Form 2EC line 18). Net household + # income: (20_000 - 12_600) * 0.035 = 259, allocated to the head. + mt_elderly_homeowner_or_renter_credit_net_household_income: [259, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml index df49679c68b..b7ce4721911 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml @@ -19,8 +19,8 @@ state_code: MT output: # One qualifying individual caps expenses at 2_400. - # Head receives half: 2_400 * 0.5 = 1_200. - mt_child_dependent_care_expense_deduction: [1_200, 0] + # A single filer enters the full line 7 amount: 2_400. + mt_child_dependent_care_expense_deduction: [2_400, 0] - name: Care expense greater than maximum amount with moderate AGI and 1 child period: 2021 @@ -43,8 +43,8 @@ state_code: MT output: # 2_400 - 0.5 * (20_000 - 18_000) = 1_400. - # Head receives half: 1_400 * 0.5 = 700. - mt_child_dependent_care_expense_deduction: [700, 0] + # A single filer enters the full line 7 amount: 1_400. + mt_child_dependent_care_expense_deduction: [1_400, 0] - name: Care expense greater than maximum amount with high AGI and 1 child period: 2021 @@ -92,8 +92,8 @@ state_code: MT output: # Two qualifying individuals cap expenses at 3_600. - # Head receives half: 3_600 * 0.5 = 1_800. - mt_child_dependent_care_expense_deduction: [1_800, 0, 0] + # A single filer enters the full line 7 amount: 3_600. + mt_child_dependent_care_expense_deduction: [3_600, 0, 0] - name: Care expense greater than maximum amount with moderate AGI and 2 children period: 2021 @@ -119,8 +119,8 @@ state_code: MT output: # 3_600 - 0.5 * (22_000 - 18_000) = 1_600. - # Head receives half: 1_600 * 0.5 = 800. - mt_child_dependent_care_expense_deduction: [800, 0, 0] + # A single filer enters the full line 7 amount: 1_600. + mt_child_dependent_care_expense_deduction: [1_600, 0, 0] - name: Care expense greater than maximum amount with high AGI and 2 children period: 2021 @@ -174,8 +174,8 @@ state_code: MT output: # Three qualifying individuals cap expenses at 4_800. - # Head receives half: 4_800 * 0.5 = 2_400. - mt_child_dependent_care_expense_deduction: [2_400, 0, 0, 0] + # A single filer enters the full line 7 amount: 4_800. + mt_child_dependent_care_expense_deduction: [4_800, 0, 0, 0] - name: Care expense greater than maximum amount with moderate AGI and 3 children period: 2021 @@ -204,8 +204,8 @@ state_code: MT output: # 4_800 - 0.5 * (24_000 - 18_000) = 1_800. - # Head receives half: 1_800 * 0.5 = 900. - mt_child_dependent_care_expense_deduction: [900, 0, 0, 0] + # A single filer enters the full line 7 amount: 1_800. + mt_child_dependent_care_expense_deduction: [1_800, 0, 0, 0] - name: Care expense greater than maximum amount with high AGI and 3 children period: 2021 @@ -255,8 +255,8 @@ members: [person1, person2] state_code: MT output: - # Head receives half: 2_000 * 0.5 = 1_000. - mt_child_dependent_care_expense_deduction: [1_000, 0] + # A single filer enters the full line 7 amount: 2_000. + mt_child_dependent_care_expense_deduction: [2_000, 0] - name: Care expense less than maximum amount with high AGI and 1 child period: 2021 @@ -303,8 +303,8 @@ members: [person1, person2, person3] state_code: MT output: - # Head receives half: 3_000 * 0.5 = 1_500. - mt_child_dependent_care_expense_deduction: [1_500, 0, 0] + # A single filer enters the full line 7 amount: 3_000. + mt_child_dependent_care_expense_deduction: [3_000, 0, 0] - name: Care expense less than maximum amount with high AGI and 2 children period: 2021 @@ -357,8 +357,8 @@ members: [person1, person2, person3, person4] state_code: MT output: - # Head receives half: 4_000 * 0.5 = 2_000. - mt_child_dependent_care_expense_deduction: [2_000, 0, 0, 0] + # A single filer enters the full line 7 amount: 4_000. + mt_child_dependent_care_expense_deduction: [4_000, 0, 0, 0] - name: Care expense less than maximum amount with high AGI and 3 children period: 2021 @@ -441,8 +441,8 @@ state_code: MT output: # One qualifying individual caps expenses at 2_400 (Form 2441-M line 2). - # Head receives half: 2_400 * 0.5 = 1_200. - mt_child_dependent_care_expense_deduction: [1_200, 0] + # A single filer enters the full line 7 amount: 2_400. + mt_child_dependent_care_expense_deduction: [2_400, 0] - name: Case 18, capable disabled adult care expenses are excluded. period: 2021 @@ -470,8 +470,8 @@ state_code: MT output: # Only the child qualifies, so 2_000 of adult care expenses are excluded. - # Head receives half of the 1_000 childcare expense deduction. - mt_child_dependent_care_expense_deduction: [500, 0, 0] + # A single filer enters the full 1_000 childcare expense deduction. + mt_child_dependent_care_expense_deduction: [1_000, 0, 0] - name: Case 19, a child and an incapacitated adult dependent both count toward the tier. period: 2021 @@ -500,8 +500,8 @@ output: # Two qualifying individuals cap expenses at 3_600 (Form 2441-M line 2). # Childcare 3_000 + adult care 1_000 = 4_000, capped at 3_600. - # Head receives half: 3_600 * 0.5 = 1_800. - mt_child_dependent_care_expense_deduction: [1_800, 0, 0] + # A single filer enters the full line 7 amount: 3_600. + mt_child_dependent_care_expense_deduction: [3_600, 0, 0] - name: Case 20, adult care expenses cannot exceed the one-qualifying-individual tier cap. period: 2021 @@ -525,8 +525,8 @@ state_code: MT output: # One qualifying individual caps expenses at 2_400 (Form 2441-M line 2). - # Head receives half: 2_400 * 0.5 = 1_200. - mt_child_dependent_care_expense_deduction: [1_200, 0] + # A single filer enters the full line 7 amount: 2_400. + mt_child_dependent_care_expense_deduction: [2_400, 0] - name: Case 21, incapacitated spouse care expenses qualify. period: 2021 @@ -606,8 +606,8 @@ output: # One qualifying individual caps expenses at 2_400 (Form 2441-M line 2). # Childcare 1_500 + care expenses 1_000 = 2_500, capped at 2_400. - # Head receives half: 2_400 * 0.5 = 1_200. - mt_child_dependent_care_expense_deduction: [1_200, 0] + # A single filer enters the full line 7 amount: 2_400. + mt_child_dependent_care_expense_deduction: [2_400, 0] - name: Case 24, combined childcare and adult care expenses are capped by the three-qualifying-individual tier. period: 2021 @@ -643,3 +643,81 @@ # Childcare 4_000 + adult care 2_000 = 6_000, capped at 4_800. # The deduction is divided equally between spouses: 4_800 * 0.5 = 2_400. mt_child_dependent_care_expense_deduction: [2_400, 2_400, 0, 0] + +- name: Case 25, the phase-out uses the couple's combined Montana AGI, then each column gets half + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + mt_agi_indiv: 20_000 + person2: + is_tax_unit_spouse: true + mt_agi_indiv: 0 + person3: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + tax_unit_childcare_expenses: 3_000 + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Form 2441-M (2022): line 2 = min(3_000, 2_400) = 2_400; line 3 adds + # columns A and B of Form 2 line 14 = 20_000 + 0 = 20_000; line 5 = 2_000; + # line 6 = 1_000; line 7 = 1_400. Filing separately on the same form, each + # spouse enters one-half of line 7 (700). Applying the phase-out to each + # spouse's own AGI would give 700 and 1_200. + mt_child_dependent_care_expense_deduction_eligible_expenses: 2_400 + mt_child_dependent_care_expense_deduction: [700, 700, 0] + +- name: Case 26, a joint return uses the return's Montana AGI and claims the whole deduction + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + person2: + is_tax_unit_spouse: true + person3: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + tax_unit_childcare_expenses: 3_000 + mt_agi_joint: 20_000 + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Form 2441-M lines 2-7 with line 3 = 20_000: 2_400 - 0.5 * 2_000 = 1_400, + # entered in full on the joint return. + mt_child_dependent_care_expense_deduction_joint: 1_400 + +- name: Case 27, the joint deduction is repealed beginning in 2024 + period: 2024 + input: + people: + person1: + is_tax_unit_head: true + person2: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2] + tax_unit_childcare_expenses: 3_000 + mt_agi_joint: 10_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_child_dependent_care_expense_deduction_joint: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.yaml index e0fed58cbeb..941a60e6c1b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.yaml @@ -1,39 +1,46 @@ -- name: Deductions split evenly +# 2022 Form 2 instructions, Itemized Deductions Schedule, line 4 (PDF p. 33): +# withholding is reported by the spouse who earned the income, and each +# spouse filing separately is limited to 5,000. + +- name: Each spouse deducts their own withholding estimate period: 2021 input: people: person1: - mt_agi_indiv: 500 + mt_withheld_income_tax: 3_000 + adjusted_gross_income_person: 50_000 person2: - mt_agi_indiv: 500 + mt_withheld_income_tax: 1_000 + adjusted_gross_income_person: 50_000 tax_units: tax_unit: members: [person1, person2] - mt_federal_income_tax_deduction_for_federal_itemization: 3_000 state_filing_status_if_married_filing_separately_on_same_return: SEPARATE households: household: members: [person1, person2] state_code: MT output: - mt_federal_income_tax_deduction_for_federal_itemization_indiv: [1_500, 1_500] + # Prorating the couple's 4_000 by Montana AGI would give 2_000 / 2_000. + mt_federal_income_tax_deduction_for_federal_itemization_indiv: [3_000, 1_000] -- name: Deductions capped at max amount individually +- name: Each spouse is capped at 5,000 on their own amount period: 2021 input: people: person1: - mt_agi_indiv: 100 + mt_withheld_income_tax: 7_000 + adjusted_gross_income_person: 60_000 person2: - mt_agi_indiv: 900 + mt_withheld_income_tax: 1_000 + adjusted_gross_income_person: 40_000 tax_units: tax_unit: members: [person1, person2] - mt_federal_income_tax_deduction_for_federal_itemization: 10_000 state_filing_status_if_married_filing_separately_on_same_return: SEPARATE households: household: members: [person1, person2] state_code: MT output: - mt_federal_income_tax_deduction_for_federal_itemization_indiv: [1_000, 5_000] + mt_federal_income_tax_deduction_for_federal_itemization_indiv: [5_000, 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_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..41a8d808975 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 @@ -7,7 +7,7 @@ mt_salt_deduction: 5_000 mt_medical_expense_deduction_joint: 1_000 casualty_loss_deduction: 400 - mt_child_dependent_care_expense_deduction: 123 + mt_child_dependent_care_expense_deduction_joint: 123 mt_federal_income_tax_deduction_for_federal_itemization: 2_000 state_code: MT output: @@ -22,7 +22,7 @@ mt_salt_deduction: 10_000 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 0 - mt_child_dependent_care_expense_deduction: 0 + mt_child_dependent_care_expense_deduction_joint: 0 mt_federal_income_tax_deduction_for_federal_itemization: 2_000 state_code: MT output: @@ -37,7 +37,7 @@ mt_salt_deduction: 1_035 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 745 - mt_child_dependent_care_expense_deduction: 1_004 + mt_child_dependent_care_expense_deduction_joint: 1_004 mt_federal_income_tax_deduction_for_federal_itemization: 2_000 state_code: MT output: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_federal_income_tax_deduction_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_federal_income_tax_deduction_indiv.yaml index 4b0d588ce5e..c7604752c4d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_federal_income_tax_deduction_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_federal_income_tax_deduction_indiv.yaml @@ -1,39 +1,120 @@ -- name: Deductions split evenly +# 2022 Form 2 instructions, Itemized Deductions Schedule, line 4 (PDF p. 33): +# "If you are married filing separately with your spouse, the federal income +# tax withheld should be reported by the spouse who earned the income." and +# "If your filing status is Married Filing Separately, you are each limited to +# the $5,000 federal tax deduction and you cannot arbitrarily allocate this +# deduction between spouses." + +- name: Federal income tax is attributed by each spouse's share of federal AGI period: 2021 + absolute_error_margin: 0.01 input: people: person1: - mt_agi_indiv: 500 + adjusted_gross_income_person: 60_000 person2: - mt_agi_indiv: 500 + adjusted_gross_income_person: 40_000 tax_units: tax_unit: members: [person1, person2] - mt_federal_income_tax_deduction_unit: 3_000 + income_tax_before_refundable_credits: 3_000 state_filing_status_if_married_filing_separately_on_same_return: SEPARATE households: household: members: [person1, person2] state_code: MT output: - mt_federal_income_tax_deduction_indiv: [1_500, 1_500] + mt_federal_income_tax_deduction_share: [0.6, 0.4] + mt_federal_income_tax_deduction_indiv: [1_800, 1_200] -- name: Deductions capped at max amount individually - period: 2021 +- name: Each spouse's own federal tax is capped at 5,000, not a share of the joint cap + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + adjusted_gross_income_person: 80_000 + person2: + adjusted_gross_income_person: 20_000 + tax_units: + tax_unit: + members: [person1, person2] + income_tax_before_refundable_credits: 30_000 + state_filing_status_if_married_filing_separately_on_same_return: SEPARATE + households: + household: + members: [person1, person2] + state_code: MT + output: + # Federal tax attributed 24_000 / 6_000; each spouse is limited to 5_000. + # Capping the couple at 10_000 first and then splitting 80% / 20% would + # give 5_000 / 2_000. + mt_federal_income_tax_deduction_indiv: [5_000, 5_000] + +- name: Montana-only subtractions do not shift federal tax between spouses + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + adjusted_gross_income_person: 50_000 + person2: + adjusted_gross_income_person: 50_000 + mt_subtractions: 20_000 + tax_units: + tax_unit: + members: [person1, person2] + income_tax_before_refundable_credits: 6_000 + state_filing_status_if_married_filing_separately_on_same_return: SEPARATE + households: + household: + members: [person1, person2] + state_code: MT + output: + # The federal tax was paid on 50_000 of federal income each. Prorating by + # Montana AGI (50_000 / 30_000) would give 3_750 / 2_250. + mt_federal_income_tax_deduction_indiv: [3_000, 3_000] + +- name: A dependent's income does not draw a share of the parents' federal tax + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + adjusted_gross_income_person: 40_000 + person2: + adjusted_gross_income_person: 0 + person3: + is_tax_unit_dependent: true + adjusted_gross_income_person: 0 + tax_units: + tax_unit: + members: [person1, person2, person3] + income_tax_before_refundable_credits: 2_000 + state_filing_status_if_married_filing_separately_on_same_return: SEPARATE + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_federal_income_tax_deduction_indiv: [2_000, 0, 0] + +- name: The deduction is repealed from 2024 + period: 2024 input: people: person1: - mt_agi_indiv: 100 + adjusted_gross_income_person: 60_000 person2: - mt_agi_indiv: 900 + adjusted_gross_income_person: 40_000 tax_units: tax_unit: members: [person1, person2] - mt_federal_income_tax_deduction_unit: 10_000 + income_tax_before_refundable_credits: 3_000 state_filing_status_if_married_filing_separately_on_same_return: SEPARATE households: household: members: [person1, person2] state_code: MT output: - mt_federal_income_tax_deduction_indiv: [1_000, 5_000] + mt_federal_income_tax_deduction_indiv: [0, 0] 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..329641eafa3 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 @@ -7,7 +7,7 @@ mt_salt_deduction: 5_000 mt_medical_expense_deduction_joint: 1_000 casualty_loss_deduction: 400 - mt_child_dependent_care_expense_deduction: 123 + mt_child_dependent_care_expense_deduction_joint: 123 mt_federal_income_tax_deduction_unit: 2_000 state_code: MT output: @@ -22,7 +22,7 @@ mt_salt_deduction: 10_000 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 0 - mt_child_dependent_care_expense_deduction: 0 + mt_child_dependent_care_expense_deduction_joint: 0 mt_federal_income_tax_deduction_unit: 2_000 state_code: MT output: @@ -37,7 +37,7 @@ mt_salt_deduction: 1_035 mt_medical_expense_deduction_joint: 3_000 casualty_loss_deduction: 745 - mt_child_dependent_care_expense_deduction: 1_004 + mt_child_dependent_care_expense_deduction_joint: 1_004 mt_federal_income_tax_deduction_unit: 2_000 state_code: MT output: diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py index e4a6976f246..4f18254eed0 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py @@ -8,7 +8,12 @@ class mt_elderly_homeowner_or_renter_credit_gross_household_income(Variable): unit = USD definition_period = YEAR defined_for = StateCode.MT - reference = "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-23/section-15-30-2337/" + reference = ( + "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-23/section-15-30-2337/", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + # 2023 Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 9 + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=52", + ) def formula(person, period, parameters): p = parameters( @@ -20,4 +25,11 @@ def formula(person, period, parameters): social_security = person("social_security", period) taxable_social_security = person("taxable_social_security", period) untaxed_social_security = max_(social_security - taxable_social_security, 0) - return sources + untaxed_social_security + # Income is federal AGI "without regard to loss" (§ 15-30-2337(9)(a)), + # so the business and capital losses deducted in computing federal + # AGI are added back (2023 Form 2EC line 9: "The gross household + # income cannot be reduced by any losses."). The return's losses are + # added once, to the head. + head = person("is_tax_unit_head", period) + losses = head * person.tax_unit("loss_ald", period) + return sources + untaxed_social_security + losses diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py index 2efb5ad2407..a7ee18fe4f8 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py @@ -7,9 +7,16 @@ class mt_child_dependent_care_expense_deduction(Variable): label = "Montana child dependent care expense deduction" unit = USD definition_period = YEAR + documentation = ( + "The deduction in each filer's column: an unmarried filer's return, or " + "each spouse's column when spouses file separately on the same form. " + "A joint return uses mt_child_dependent_care_expense_deduction_joint." + ) reference = ( "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", "https://web.archive.org/web/20230914140156/https://mtrevenue.gov/wp-content/uploads/dlm_uploads/2022/12/2441-M_2022.pdf#page=1", + # 2022 Form 2 instructions, Itemized Deductions Schedule, line 14 + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=35", ) defined_for = StateCode.MT @@ -19,26 +26,21 @@ def formula(person, period, parameters): ).gov.states.mt.tax.income.deductions.child_dependent_care_expense if not p.in_effect: return 0 - tax_unit = person.tax_unit - members = tax_unit.members - qualifying_individual = members( - "mt_child_dependent_care_expense_deduction_qualifying_individual", - period, - ) - # Form 2441-M line 1 - qualifying_individuals = tax_unit.sum(qualifying_individual) - qualifying_care_expenses = tax_unit.sum( - members("care_expenses", period) * qualifying_individual + # Form 2441-M line 2 + eligible_expenses = person.tax_unit( + "mt_child_dependent_care_expense_deduction_eligible_expenses", period ) - childcare_expenses = tax_unit("tax_unit_childcare_expenses", period) - total_expenses = childcare_expenses + qualifying_care_expenses - # Form 2441-M line 2: the lesser of actual dependent care expenses - # or the cap based on the number of qualifying individuals. - capped_expenses = min_(total_expenses, p.cap.calc(qualifying_individuals)) - # Line 3 - agi = person("mt_agi_indiv", period) - # Line 6 - reduction = p.phase_out.calc(agi) - # The deduction has to be allocated equally between spouses + # Line 3: add the Montana adjusted gross income in columns A and B, + # that is, the couple's combined income, not each spouse's own. head_or_spouse = person("is_tax_unit_head_or_spouse", period) - return head_or_spouse * (max_(0, capped_expenses - reduction) * 0.5) + combined_agi = person.tax_unit.sum( + head_or_spouse * person("mt_agi_indiv", period) + ) + # Lines 4-7 + deduction = max_(eligible_expenses - p.phase_out.calc(combined_agi), 0) + # Spouses filing separately on the same form each enter one-half of + # line 7 in their column; an unmarried filer enters all of it. + married = person.tax_unit("tax_unit_married", period) + head = person("is_tax_unit_head", period) + share = where(married, head_or_spouse * 0.5, head * 1.0) + return deduction * share diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_eligible_expenses.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_eligible_expenses.py new file mode 100644 index 00000000000..642214c3e55 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_eligible_expenses.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class mt_child_dependent_care_expense_deduction_eligible_expenses(Variable): + value_type = float + entity = TaxUnit + label = "Montana child and dependent care expenses eligible for the deduction" + unit = USD + definition_period = YEAR + reference = ( + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + "https://web.archive.org/web/20230914140156/https://mtrevenue.gov/wp-content/uploads/dlm_uploads/2022/12/2441-M_2022.pdf#page=1", + ) + defined_for = StateCode.MT + + def formula(tax_unit, period, parameters): + p = parameters( + period + ).gov.states.mt.tax.income.deductions.child_dependent_care_expense + person = tax_unit.members + qualifying_individual = person( + "mt_child_dependent_care_expense_deduction_qualifying_individual", + period, + ) + # Form 2441-M line 1 + qualifying_individuals = tax_unit.sum(qualifying_individual) + qualifying_care_expenses = tax_unit.sum( + person("care_expenses", period) * qualifying_individual + ) + childcare_expenses = tax_unit("tax_unit_childcare_expenses", period) + total_expenses = childcare_expenses + qualifying_care_expenses + # Form 2441-M line 2: the lesser of actual dependent care expenses + # or the cap based on the number of qualifying individuals. + return min_(total_expenses, p.cap.calc(qualifying_individuals)) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py new file mode 100644 index 00000000000..ae9a5f6eb02 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class mt_child_dependent_care_expense_deduction_joint(Variable): + value_type = float + entity = TaxUnit + label = "Montana child dependent care expense deduction on a joint return" + unit = USD + definition_period = YEAR + reference = ( + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + "https://web.archive.org/web/20230914140156/https://mtrevenue.gov/wp-content/uploads/dlm_uploads/2022/12/2441-M_2022.pdf#page=1", + # 2022 Form 2 instructions, Itemized Deductions Schedule, line 14 + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=35", + ) + defined_for = StateCode.MT + + def formula(tax_unit, period, parameters): + p = parameters( + period + ).gov.states.mt.tax.income.deductions.child_dependent_care_expense + if not p.in_effect: + return 0 + # Form 2441-M line 2 + eligible_expenses = tax_unit( + "mt_child_dependent_care_expense_deduction_eligible_expenses", period + ) + # Line 3: the return's Montana adjusted gross income; lines 4-7. + agi = tax_unit("mt_agi_joint", period) + return max_(eligible_expenses - p.phase_out.calc(agi), 0) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.py index 94951d1ad9c..967ce5bdfb5 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_federal_income_tax_deduction_for_federal_itemization_indiv.py @@ -7,28 +7,24 @@ class mt_federal_income_tax_deduction_for_federal_itemization_indiv(Variable): definition_period = YEAR label = "Montana federal income tax deduction when married couples are filing separately" reference = ( - "https://law.justia.com/codes/montana/2021/title-15/chapter-30/part-21/section-15-30-2131/" + "https://law.justia.com/codes/montana/2021/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2021) (1)(b) + # 2022 Form 2 instructions, Itemized Deductions Schedule, line 4 + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", ) unit = USD defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): - unit_deds = person.tax_unit( - "mt_federal_income_tax_deduction_for_federal_itemization", period - ) - person_agi = person("mt_agi_indiv", period) - total_agi = person.tax_unit.sum(person_agi) - - prorate = np.zeros_like(total_agi) - mask = total_agi > 0 - prorate[mask] = person_agi[mask] / total_agi[mask] + p = parameters( + period + ).gov.states.mt.tax.income.deductions.itemized.federal_income_tax + # The withholding estimate is computed on each person's own income, so + # each spouse deducts their own amount, up to their own cap. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + withheld_income_tax = head_or_spouse * person("mt_withheld_income_tax", period) filing_status = person.tax_unit( "state_filing_status_if_married_filing_separately_on_same_return", period, ) - p = parameters( - period - ).gov.states.mt.tax.income.deductions.itemized.federal_income_tax - cap = p.cap[filing_status] - return min_(unit_deds * prorate, cap) + return min_(withheld_income_tax, p.cap[filing_status]) 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..0d5e7d0e577 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 @@ -29,6 +29,7 @@ def formula(person, period, parameters): period, [ "mt_misc_deductions", + "mt_child_dependent_care_expense_deduction", "mt_medical_expense_deduction_indiv", "mt_salt_deduction", "mt_federal_income_tax_deduction_for_federal_itemization_indiv", 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..ed3371fd0ab 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 @@ -24,6 +24,7 @@ def formula(person, period, parameters): period, [ "mt_misc_deductions", + "mt_child_dependent_care_expense_deduction_joint", "mt_medical_expense_deduction_joint", "mt_salt_deduction", "mt_federal_income_tax_deduction_for_federal_itemization", diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_indiv.py index 792174d9142..f84c008f403 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_indiv.py @@ -7,26 +7,31 @@ class mt_federal_income_tax_deduction_indiv(Variable): definition_period = YEAR label = "Montana federal income tax deduction when married couples are filing separately" reference = ( - "https://law.justia.com/codes/montana/2021/title-15/chapter-30/part-21/section-15-30-2131/" + "https://law.justia.com/codes/montana/2021/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2021) (1)(b) + # 2022 Form 2 instructions, Itemized Deductions Schedule, line 4 + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", ) unit = USD defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): - unit_deds = person.tax_unit("mt_federal_income_tax_deduction_unit", period) - person_agi = person("mt_agi_indiv", period) - total_agi = person.tax_unit.sum(person_agi) - - prorate = np.zeros_like(total_agi) - mask = total_agi > 0 - prorate[mask] = person_agi[mask] / total_agi[mask] + p = parameters( + period + ).gov.states.mt.tax.income.deductions.itemized.federal_income_tax + if not p.applies: + return 0 + # Federal income tax is reported by the spouse who earned the income + # it was paid on, and each spouse is held to their own cap; the + # deduction cannot be arbitrarily allocated. Without each spouse's + # federal withholding, attribute the couple's federal income tax by + # each spouse's share of federal adjusted gross income, then cap it. + federal_income_tax = max_( + person.tax_unit("income_tax_before_refundable_credits", period), 0 + ) + share = person("mt_federal_income_tax_deduction_share", period) filing_status = person.tax_unit( "state_filing_status_if_married_filing_separately_on_same_return", period, ) - p = parameters( - period - ).gov.states.mt.tax.income.deductions.itemized.federal_income_tax - cap = p.cap[filing_status] - return min_(unit_deds * prorate, cap) + return min_(federal_income_tax * share, p.cap[filing_status]) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_share.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_share.py new file mode 100644 index 00000000000..2c6dfd8556b --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_tax/mt_federal_income_tax_deduction_share.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class mt_federal_income_tax_deduction_share(Variable): + value_type = float + entity = Person + definition_period = YEAR + label = "Share of the couple's federal income tax attributed to each spouse for the Montana deduction" + reference = ( + # 2022 Form 2 instructions, Itemized Deductions Schedule, line 4a: + # "If you are married filing separately with your spouse, the federal + # income tax withheld should be reported by the spouse who earned the + # income." + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", + ) + unit = "/1" + defined_for = "mt_married_filing_separately_on_same_return_eligible" + + def formula(person, period, parameters): + # Each filer's share of the federal income the tax was paid on, + # counting only positive federal adjusted gross income. A dependent's + # income is on the dependent's own return. With no positive income, + # the tax is attributed to the head. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + income = head_or_spouse * max_( + person("adjusted_gross_income_person", period), 0 + ) + total_income = person.tax_unit.sum(income) + share = np.zeros_like(total_income) + mask = total_income > 0 + share[mask] = income[mask] / total_income[mask] + head = person("is_tax_unit_head", period) + return where(total_income > 0, share, head) 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..ff75bc2277f 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 @@ -31,6 +31,7 @@ def formula(person, period, parameters): period, [ "mt_misc_deductions", + "mt_child_dependent_care_expense_deduction", "mt_medical_expense_deduction_indiv", "mt_salt_deduction", "mt_federal_income_tax_deduction_indiv", 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..1a13c3a97de 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 @@ -24,6 +24,7 @@ def formula(person, period, parameters): period, [ "mt_misc_deductions", + "mt_child_dependent_care_expense_deduction_joint", "mt_medical_expense_deduction_joint", "mt_salt_deduction", "mt_federal_income_tax_deduction_unit", From 304d880bb6e205730d6dd25b00db4cb89f8551ca Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:34:58 -0400 Subject: [PATCH 02/13] Update the 2021 Montana rebate case for per-spouse federal tax deductions Each spouse filing separately on the same form deducts their own federal income tax up to $5,000, so both spouses itemize and Montana tax is 6,295.56. Co-Authored-By: Claude Opus 5.5 --- .../baseline/gov/states/mt/tax/income/integration.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml index 1427235ceaa..687a3ecabbf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/integration.yaml @@ -564,9 +564,12 @@ output: # $2,500 per-return cap split $1,250 per spouse (was double-counted to $5,000) mt_income_tax_rebate: [1_250, 1_250] - # Files separately: 5,099.51 - 1,250 + 3,719.51 - 1,250 = 6,319.02 + # Each spouse deducts their own federal income tax up to $5,000 (2021 + # federal tax 21,175 attributed 90/160 and 70/160), so both itemize 5,000 + # over the 4,830 standard deduction. Taxable income 82,420 and 62,420. + # Files separately: 5,087.78 - 1,250 + 3,707.78 - 1,250 = 6,295.56 # (old double-count subtracted $2,500 per column, understating tax by $2,500) - mt_income_tax: 6_319.02 + mt_income_tax: 6_295.56 - name: MT property tax rebate is not a Form 2 credit and is not tripled (issue 9347) absolute_error_margin: 0.01 From 6eefe271dc9ea5e5bd119221eb70b6e176801e8e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:30:36 -0400 Subject: [PATCH 03/13] Address review: no care deduction on a separate form; name the 2023 schedule - A married person filing on a separate form (federal filing status SEPARATE) cannot take the child and dependent care expense deduction: "married couples shall file a joint return or file separately on the same form" (MCA 15-30-2131(1)(c)(vi)(A)); 2022 instructions, line 14. - The 2023 credit schedule was the Elderly Homeowner/Renter Credit Schedule; it became Schedule 2EC in 2024. Co-Authored-By: Claude Opus 5.5 --- ...mt_elderly_homeowner_or_renter_credit.yaml | 2 +- ..._renter_credit_gross_household_income.yaml | 5 ++-- ...hild_dependent_care_expense_deduction.yaml | 28 +++++++++++++++++++ ...or_renter_credit_gross_household_income.py | 7 +++-- ..._child_dependent_care_expense_deduction.py | 7 ++++- ..._dependent_care_expense_deduction_joint.py | 7 ++++- 6 files changed, 48 insertions(+), 8 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index 154bf57d561..8cfb8bc8538 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -224,7 +224,7 @@ # contribution, so the credit is higher: $539.67. mt_elderly_homeowner_or_renter_credit: 539.67 -- name: 2023 Form 2EC worked case, a capital loss does not reduce gross household income +- name: 2023 Elderly Homeowner/Renter Credit Schedule worked case, a capital loss does not reduce gross household income period: 2023 absolute_error_margin: 0.01 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml index f7d12a544dc..38e02773559 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml @@ -55,7 +55,8 @@ members: [person1] state_name: MT output: - # § 15-30-2337(9)(a): federal AGI "without regard to loss". 2023 Form 2EC: + # § 15-30-2337(9)(a): federal AGI "without regard to loss". 2023 Elderly + # Homeowner/Renter Credit Schedule: # line 1 federal AGI 20_000 - 3_000 = 17_000; line 9 adds back the 3_000 # capital loss; line 10 = 20_000. mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 @@ -81,6 +82,6 @@ state_name: MT output: # Federal AGI 20_000 - 5_000 = 15_000; the 5_000 loss is added back, so - # gross household income is 20_000 (Form 2EC line 18). Net household + # gross household income is 20_000 (2024 Schedule 2EC line 18). Net household # income: (20_000 - 12_600) * 0.035 = 259, allocated to the head. mt_elderly_homeowner_or_renter_credit_net_household_income: [259, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml index b7ce4721911..4cdfcd2395d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.yaml @@ -721,3 +721,31 @@ state_code: MT output: mt_child_dependent_care_expense_deduction_joint: 0 + +- name: Case 28, a married person filing on a separate form cannot take the deduction + period: 2022 + input: + people: + person1: + is_tax_unit_head: true + mt_agi_indiv: 15_000 + person2: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2] + filing_status: SEPARATE + tax_unit_childcare_expenses: 2_000 + mt_agi_joint: 15_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # MCA 15-30-2131(1)(c)(vi)(A): "married couples shall file a joint return + # or file separately on the same form". 2022 instructions, line 14: "If + # you are married and filing separately on separate forms, or your spouse + # is not filing, you cannot qualify for this deduction." + mt_child_dependent_care_expense_deduction: [0, 0] + mt_child_dependent_care_expense_deduction_joint: 0 diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py index 4f18254eed0..21c77cdc399 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py @@ -12,6 +12,7 @@ class mt_elderly_homeowner_or_renter_credit_gross_household_income(Variable): "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-23/section-15-30-2337/", "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", # 2023 Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 9 + # (renamed Schedule 2EC from 2024) "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=52", ) @@ -27,9 +28,9 @@ def formula(person, period, parameters): untaxed_social_security = max_(social_security - taxable_social_security, 0) # Income is federal AGI "without regard to loss" (§ 15-30-2337(9)(a)), # so the business and capital losses deducted in computing federal - # AGI are added back (2023 Form 2EC line 9: "The gross household - # income cannot be reduced by any losses."). The return's losses are - # added once, to the head. + # AGI are added back (2023 Elderly Homeowner/Renter Credit Schedule, + # line 9: "The gross household income cannot be reduced by any + # losses."). The return's losses are added once, to the head. head = person("is_tax_unit_head", period) losses = head * person.tax_unit("loss_ald", period) return sources + untaxed_social_security + losses diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py index a7ee18fe4f8..229fe2db8e6 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction.py @@ -43,4 +43,9 @@ def formula(person, period, parameters): married = person.tax_unit("tax_unit_married", period) head = person("is_tax_unit_head", period) share = where(married, head_or_spouse * 0.5, head * 1.0) - return deduction * share + # Married couples must file a joint return or file separately on the + # same form (MCA 15-30-2131(1)(c)(vi)(A)); a spouse filing on a + # separate form cannot take the deduction. + filing_status = person.tax_unit("filing_status", period) + separate_form = filing_status == filing_status.possible_values.SEPARATE + return deduction * share * ~separate_form diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py index ae9a5f6eb02..967ce181cb0 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/cdcc/mt_child_dependent_care_expense_deduction_joint.py @@ -27,4 +27,9 @@ def formula(tax_unit, period, parameters): ) # Line 3: the return's Montana adjusted gross income; lines 4-7. agi = tax_unit("mt_agi_joint", period) - return max_(eligible_expenses - p.phase_out.calc(agi), 0) + deduction = max_(eligible_expenses - p.phase_out.calc(agi), 0) + # A married person filing on a separate form cannot take the deduction + # (MCA 15-30-2131(1)(c)(vi)(A)). + filing_status = tax_unit("filing_status", period) + separate_form = filing_status == filing_status.possible_values.SEPARATE + return deduction * ~separate_form From 940e69f031f2c07e7855fcfff4a5e617167f7f12 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:33:59 -0400 Subject: [PATCH 04/13] Count household members' income in the Montana elderly credit MCA 15-30-2337(4) defines gross household income as "all income received by all individuals of a household while they are members of the household". The 2023 Elderly Homeowner/Renter Credit Schedule combines the returns of "ALL members of the household" (lines 1-7 and 9) and has non-filing members report their wages on line 8; the 2024 and 2025 Schedule 2EC put "Income received by other members of your household" on line 17. The model summed each person's federal AGI over the tax unit. A tax unit dependent's federal AGI is zero (their income is on their own return), so a dependent's wages and other income were left out, and household members who file their own returns were never counted. - Add dependent_gross_income to the gross income sources. It counts a dependent's gross income items, gains but not losses, and the taxable part of their Social Security; the formula now subtracts that part before adding the untaxed remainder, so the benefits count once. - Sum gross household income over the household instead of the tax unit in the net household income and the credit. Co-Authored-By: Claude Opus 5.5 --- ...x-mt-ehrc-household-member-income.fixed.md | 1 + .../gross_income_sources.yaml | 9 ++ ...mt_elderly_homeowner_or_renter_credit.yaml | 145 ++++++++++++++++++ ..._renter_credit_gross_household_income.yaml | 75 +++++++++ ...or_renter_credit_net_household_income.yaml | 26 ++++ .../mt_elderly_homeowner_or_renter_credit.py | 15 +- ...or_renter_credit_gross_household_income.py | 15 +- ...r_or_renter_credit_net_household_income.py | 11 +- 8 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 changelog.d/fix-mt-ehrc-household-member-income.fixed.md diff --git a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md new file mode 100644 index 00000000000..0b6584283d6 --- /dev/null +++ b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md @@ -0,0 +1 @@ +Count household members' income in the Montana elderly homeowner/renter credit: a tax unit dependent's gross income, and the income of household members who file their own returns. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml index c949e5c54fe..298ed1755e8 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml @@ -10,11 +10,20 @@ metadata: href: https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-23/section-15-30-2337/ - title: 2024 Montana Individual Income Tax Return href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=46 + - title: 2023 Montana Form 2 Instructions, Elderly Homeowner/Renter Credit Schedule, lines 1 and 8 (income of each member of the household) + href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 + - title: 2024 Montana Form 2 Instructions, Schedule 2EC, line 17 (income received by other members of your household) + href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=48 values: 2009-01-01: - adjusted_gross_income_person # Federal AGI baseline; covers taxable # pensions, IRA, wages, interest, dividends, etc. + # A tax unit dependent's income is on their own return, so + # adjusted_gross_income_person is zero for them; count their gross income + # directly. It includes gains but not losses, and only the taxable part + # of their Social Security (the rest is added in the variable formula). + - dependent_gross_income - tax_exempt_interest_income # § 15-30-2337(9)(a)(vii) # AGI already includes taxable IRA distributions via # taxable_retirement_distributions; only the tax-exempt portion diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index 8cfb8bc8538..6f2d13342d2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -254,3 +254,148 @@ # Line 18 = 2_000; line 19 = 1_130; line 20 = min(1_130, 1_150); # line 21 = 100% (under 35_000); line 22 = 1_130. mt_elderly_homeowner_or_renter_credit: 1_130 + +- name: 2024 Schedule 2EC worked case, a dependent's wages count toward gross household income + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 16 + employment_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 5 pensions 20_000; line 17 other household members' income + # 10_000 (the dependent's wages); line 18 gross household income 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + # Line 19 12_600; line 20 = 30_000 - 12_600 = 17_400; line 21 = 0.05; + # line 22 = 870. + mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] + # Line 23 = 2_000; line 26 = 2_000; line 27 = 2_000 - 870 = 1_130; + # line 28 = min(1_130, 1_150) = 1_130; line 29 = 100% (line 18 is under + # 35_000); line 30 = 1_130. Without the dependent's wages, gross + # household income would be 20_000 and the credit the 1_150 cap. + mt_elderly_homeowner_or_renter_credit: [1_130, 0] + +- name: 2024 Schedule 2EC worked case, a household member filing their own return counts + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 45 + taxable_interest_income: 10_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The claimant's adult daughter lives in the household and files her + # own return. § 15-30-2337(7)(a): a household is "an association of + # persons who live in the same dwelling"; the 2024 instructions: "You do + # not need to be related to be in the same household." Line 5 = 20_000; + # line 17 = 10_000 (her interest); line 18 = 30_000; line 22 = 870; + # line 27 = 2_000 - 870 = 1_130; line 30 = 1_130. Her tax unit has no + # one aged 62 or older, so it claims nothing. + mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] + mt_elderly_homeowner_or_renter_credit: [1_130, 0] + +- name: 2024 Schedule 2EC worked case, a dependent's wages move the credit into the 40 percent band + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 17 + employment_income: 16_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 20_000 + 16_000 = 36_000; line 20 = 23_400; line 22 = + # 23_400 x 0.05 = 1_170; line 27 = 2_000 - 1_170 = 830; line 28 = 830; + # line 29 = 40% (36_000 is in the 35_000 to 37_500 band); + # line 30 = 830 x 0.4 = 332. + mt_elderly_homeowner_or_renter_credit_net_household_income: [1_170, 0] + mt_elderly_homeowner_or_renter_credit: [332, 0] + +- name: 2024 Schedule 2EC, a dependent's wages bring gross household income to 45,000 + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 17 + employment_income: 25_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 45_000. The claimant must have gross household income "less + # than $45,000"; line 29 = 0% at 45_000 and above. + mt_elderly_homeowner_or_renter_credit: [0, 0] + +- name: 2023 Elderly Homeowner/Renter Credit Schedule worked case, a non-filing dependent's wages count + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 16 + employment_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 1 federal AGI 20_000 (the head's return). The dependent's 10_000 + # of wages is under the 13_850 filing threshold for dependents; the + # 2023 instructions say members of the household not filing a Form 2 or + # federal Form 1040 "must report all wages" on line 8. Line 10 = 30_000; + # line 12 = 17_400; line 13 = 0.05; line 14 = 870; line 15 = 2_000; + # line 18 = 2_000; line 19 = 1_130; line 20 = 1_130; line 21 = 100%; + # line 22 = 1_130. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] + mt_elderly_homeowner_or_renter_credit: [1_130, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml index 38e02773559..88dbe3c9d4e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml @@ -85,3 +85,78 @@ # gross household income is 20_000 (2024 Schedule 2EC line 18). Net household # income: (20_000 - 12_600) * 0.035 = 259, allocated to the head. mt_elderly_homeowner_or_renter_credit_net_household_income: [259, 0] + +- name: A dependent's wages count in gross household income + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 16 + employment_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # § 15-30-2337(4): "all income received by all individuals of a + # household". The dependent's wages are on their own return, not in the + # head's federal AGI. 2024 Schedule 2EC: line 5 pensions 20_000 (head); + # line 17 income of other household members 10_000 (dependent's wages); + # line 18 gross household income 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: A dependent's Social Security counts once, in full + period: 2024 + input: + people: + person1: + age: 70 + person2: + age: 17 + employment_income: 25_000 + social_security_survivors: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # On the dependent's own return, provisional income is + # 25_000 + 10_000 / 2 = 30_000, so 50% x (30_000 - 25_000) = 2_500 of the + # benefits is taxable (IRC 86) and is part of dependent_gross_income. + dependent_taxable_social_security: [0, 2_500] + # § 15-30-2337(9)(a)(viii) counts all Social Security, and 2024 + # Schedule 2EC line 6 takes "the total taxable and nontaxable" benefits: + # wages 25_000 + Social Security 10_000 = 35_000, not 37_500. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [0, 35_000] + +- name: A dependent's capital loss does not reduce gross household income + period: 2024 + input: + people: + person1: + age: 70 + person2: + age: 16 + employment_income: 10_000 + long_term_capital_gains: -3_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # § 15-30-2337(9)(a): income "without regard to loss". 2024 Schedule 2EC + # line 7: "do not include any capital losses". Wages 10_000 count; the + # 3_000 loss does not reduce them. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [0, 10_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.yaml index dcc4e975a04..052498f9036 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.yaml @@ -40,3 +40,29 @@ output: # (48,000 - 6,300) x 0.05 with the 2021 exclusion of $6,300. mt_elderly_homeowner_or_renter_credit_net_household_income: [2_085, 0] + +- name: Gross household income includes members outside the claimant's tax unit + period: 2024 + input: + people: + person1: + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 + mt_elderly_homeowner_or_renter_credit_eligible: true + person2: + mt_elderly_homeowner_or_renter_credit_gross_household_income: 10_000 + mt_elderly_homeowner_or_renter_credit_eligible: false + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # 2024 Schedule 2EC line 18 = 20_000 + 10_000 (line 17, the other member) + # = 30_000; line 20 = 30_000 - 12_600 = 17_400; line 21 = 0.05; + # line 22 = 870, on the claimant's row. person2's tax unit has no + # claimant, so 0. + mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py index d233f4ad92c..f2cbf8e8162 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py @@ -8,16 +8,23 @@ class mt_elderly_homeowner_or_renter_credit(Variable): unit = USD definition_period = YEAR defined_for = "mt_elderly_homeowner_or_renter_credit_eligible" + reference = ( + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + # 2024 Form 2, Schedule 2EC + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + ) def formula(person, period, parameters): p = parameters( period ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter - # § 15-30-2337(4) defines gross household income at the household - # (tax-unit) level, so the multiplier phase-out must be applied to - # the aggregated amount rather than each person's share. + # § 15-30-2337(4) defines gross household income as "all income + # received by all individuals of a household", including members + # outside the claimant's return (2024 Schedule 2EC line 17), so the + # multiplier phase-out applies to the household's total income rather + # than each person's share. gross_household_income = add( - person.tax_unit, + person.household, period, ["mt_elderly_homeowner_or_renter_credit_gross_household_income"], ) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py index 21c77cdc399..d2db70646cc 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py @@ -14,6 +14,9 @@ class mt_elderly_homeowner_or_renter_credit_gross_household_income(Variable): # 2023 Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 9 # (renamed Schedule 2EC from 2024) "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=52", + # 2024 Schedule 2EC, line 17: income received by other members of + # the household + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=48", ) def formula(person, period, parameters): @@ -21,10 +24,16 @@ def formula(person, period, parameters): period ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter sources = add(person, period, p.gross_income_sources) - # AGI only captures the taxable portion of Social Security; add the - # untaxed portion so all SS is counted per § 15-30-2337(9)(a)(viii). + # The sources count only the taxable portion of Social Security: a + # filer's in federal AGI, a dependent's in their own gross income. + # Add the untaxed portion so all SS is counted per + # § 15-30-2337(9)(a)(viii). social_security = person("social_security", period) - taxable_social_security = person("taxable_social_security", period) + taxable_social_security = add( + person, + period, + ["taxable_social_security", "dependent_taxable_social_security"], + ) untaxed_social_security = max_(social_security - taxable_social_security, 0) # Income is federal AGI "without regard to loss" (§ 15-30-2337(9)(a)), # so the business and capital losses deducted in computing federal diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py index 4c996aded7b..a46ccf6f5c6 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py @@ -8,6 +8,11 @@ class mt_elderly_homeowner_or_renter_credit_net_household_income(Variable): unit = USD definition_period = YEAR defined_for = "mt_elderly_homeowner_or_renter_credit_eligible" + reference = ( + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + # 2024 Form 2, Schedule 2EC + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + ) def formula(person, period, parameters): p = parameters( @@ -16,10 +21,12 @@ def formula(person, period, parameters): # Only one claim is allowed per household # married taxpayer who are living apart may qualify for only one credit per year standard_exclusion = p.net_household_income.standard_exclusion - # Allocate the income to the head + # Gross household income counts every member of the household, + # including those outside the claimant's return (§ 15-30-2337(4); + # 2024 Schedule 2EC line 17). Allocate it to the head. head = person("is_tax_unit_head", period) gross_household_income = add( - person.tax_unit, + person.household, period, ["mt_elderly_homeowner_or_renter_credit_gross_household_income"], ) From 54d30751e0a16e81675a43dd884138b94dbcfcbc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:45:21 -0400 Subject: [PATCH 05/13] Count a dependent's income only for tax unit dependents; add property tests dependent_gross_income counts anyone who is not the head or spouse, while irs_gross_income counts anyone who is not a dependent. A person with neither role (a minor alone in a tax unit whose dependent flag is input as false) was counted by both. Count dependent_gross_income in the formula for tax unit dependents only, so the two paths split people exactly. The property tests check, for random claimants and household members, that gross household income is the Schedule 2EC total whether the other member is a dependent or files their own return, that the credit is the same either way, stays within its bounds, and never rises with income. Co-Authored-By: Claude Opus 5.5 --- .../gross_income_sources.yaml | 9 - ...est_mt_elderly_credit_household_members.py | 241 ++++++++++++++++++ ...or_renter_credit_gross_household_income.py | 19 +- 3 files changed, 256 insertions(+), 13 deletions(-) create mode 100644 policyengine_us/tests/test_mt_elderly_credit_household_members.py diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml index 298ed1755e8..c949e5c54fe 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml @@ -10,20 +10,11 @@ metadata: href: https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-23/section-15-30-2337/ - title: 2024 Montana Individual Income Tax Return href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=46 - - title: 2023 Montana Form 2 Instructions, Elderly Homeowner/Renter Credit Schedule, lines 1 and 8 (income of each member of the household) - href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 - - title: 2024 Montana Form 2 Instructions, Schedule 2EC, line 17 (income received by other members of your household) - href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=48 values: 2009-01-01: - adjusted_gross_income_person # Federal AGI baseline; covers taxable # pensions, IRA, wages, interest, dividends, etc. - # A tax unit dependent's income is on their own return, so - # adjusted_gross_income_person is zero for them; count their gross income - # directly. It includes gains but not losses, and only the taxable part - # of their Social Security (the rest is added in the variable formula). - - dependent_gross_income - tax_exempt_interest_income # § 15-30-2337(9)(a)(vii) # AGI already includes taxable IRA distributions via # taxable_retirement_distributions; only the tax-exempt portion diff --git a/policyengine_us/tests/test_mt_elderly_credit_household_members.py b/policyengine_us/tests/test_mt_elderly_credit_household_members.py new file mode 100644 index 00000000000..d1c1fff15ef --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_household_members.py @@ -0,0 +1,241 @@ +"""Property tests: household members' income in the Montana elderly +homeowner/renter credit. + +MCA 15-30-2337(4) defines gross household income as "all income received by +all individuals of a household while they are members of the household", and +(9)(a) counts federal adjusted gross income "without regard to loss" plus +nontaxable income, including all Social Security. The 2024 Schedule 2EC lists +the claimant's income by category (line 7: "do not include any capital +losses"; line 6: "the total taxable and nontaxable Social Security benefits") +and puts the income of "other members of your household" on line 17. + +The model reaches the same quantity by two paths. A member who is the +claimant's tax unit dependent is counted through dependent_gross_income and +dependent_taxable_social_security; a member who files their own return is +counted through federal AGI, the loss add-back and taxable_social_security. +For a claimant aged 62 or older (taxable pension, Social Security, property +tax, rent) and one other adult in the household (taxable interest, a +long-term capital gain or loss, Social Security): + +1. The household's gross household income is the claimant's pension and + Social Security plus the other member's interest, positive capital gain + and Social Security, whichever path counts the other member. +2. So the claimant's credit is the same in both arrangements, and the other + member's own tax unit, with no one aged 62 or older, claims nothing. +3. The credit is between zero and the cap, and zero when gross household + income is $45,000 or more (line 29). +4. More interest for the other member never raises the credit: gross + household income rises, so net household income (line 22) cannot fall + and the credit multiplier (line 29) cannot rise. + +No one has earned income, so no refundable credit enters line 8. The model +computes in single precision, so comparisons allow one cent or eight float32 +spacings at the household's total income, whichever is larger. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEAR = 2024 +CAP = 1_150 +INCOME_LIMIT = 45_000 +TOLERANCE = 0.01 + + +def build_situation(cases): + """Three Montana households per case: (A) the other member is the + claimant's dependent, (B) the other member files their own return and + (C) as A with the other member's extra interest added.""" + people, tax_units, marital_units, spm_units, families, households = ( + {}, + {}, + {}, + {}, + {}, + {}, + ) + for i, c in enumerate(cases): + for arrangement in "ABC": + key = f"{arrangement}{i}" + claimant, member = f"claimant_{key}", f"member_{key}" + interest = c["interest"] + ( + c["extra_interest"] if arrangement == "C" else 0 + ) + people[claimant] = { + "age": {YEAR: c["claimant_age"]}, + "taxable_pension_income": {YEAR: c["pension"]}, + "social_security_retirement": {YEAR: c["claimant_ss"]}, + "real_estate_taxes": {YEAR: c["property_tax"]}, + "rent": {YEAR: c["rent"]}, + } + people[member] = { + "age": {YEAR: c["member_age"]}, + "taxable_interest_income": {YEAR: interest}, + "long_term_capital_gains": {YEAR: c["capital_gain"]}, + "social_security_survivors": {YEAR: c["member_ss"]}, + } + if arrangement == "B": + tax_units[f"tax_unit_{key}"] = {"members": [claimant]} + tax_units[f"tax_unit_{key}_member"] = {"members": [member]} + else: + tax_units[f"tax_unit_{key}"] = {"members": [claimant, member]} + # Set for everyone: an input for some people gives the others + # its default (false), not its formula. Without it the model + # takes an adult in the claimant's tax unit to be the spouse. + people[claimant]["is_tax_unit_dependent"] = {YEAR: False} + people[member]["is_tax_unit_dependent"] = {YEAR: arrangement != "B"} + marital_units[f"marital_unit_{key}"] = {"members": [claimant]} + marital_units[f"marital_unit_{key}_member"] = {"members": [member]} + spm_units[f"spm_unit_{key}"] = {"members": [claimant, member]} + families[f"family_{key}"] = {"members": [claimant, member]} + households[f"household_{key}"] = { + "members": [claimant, member], + "state_code": {YEAR: "MT"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": spm_units, + "families": families, + "households": households, + } + + +def calculate(cases): + """Household totals of gross household income and the credit, and the + other member's credit, as arrays indexed [arrangement, case].""" + simulation = Simulation(situation=build_situation(cases)) + gross_household_income = np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit_gross_household_income", + YEAR, + map_to="household", + ) + ) + credit = np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit", YEAR, map_to="household" + ) + ) + person_credit = np.asarray( + simulation.calculate("mt_elderly_homeowner_or_renter_credit", YEAR) + ) + dependent = np.asarray(simulation.calculate("is_tax_unit_dependent", YEAR)) + # Households and people are in situation order: A0, B0, C0, A1, ...; + # the member follows the claimant. + n = len(cases) + return { + "gross_household_income": gross_household_income.reshape(n, 3).T, + "credit": credit.reshape(n, 3).T, + "member_credit": person_credit[1::2].reshape(n, 3).T, + "member_is_dependent": dependent[1::2].reshape(n, 3).T, + } + + +def expected_gross_household_income(c, extra_interest=0): + """2024 Schedule 2EC line 18: lines 5 and 6 for the claimant, and line + 17 for the other member's interest, gains and Social Security.""" + claimant = c["pension"] + c["claimant_ss"] + member = c["interest"] + extra_interest + max(0, c["capital_gain"]) + return claimant + member + c["member_ss"] + + +def tolerance(amount): + return max(TOLERANCE, 8 * float(np.spacing(np.float32(abs(amount))))) + + +def assert_properties(cases): + result = calculate(cases) + a, b, c_ = 0, 1, 2 + # The other member is a dependent in A and C and their own filer in B. + assert result["member_is_dependent"].tolist() == [ + [True] * len(cases), + [False] * len(cases), + [True] * len(cases), + ] + for i, c in enumerate(cases): + expected = expected_gross_household_income(c) + tol = tolerance(expected + c["extra_interest"]) + ghi = result["gross_household_income"] + credit = result["credit"] + # 1. Both paths give the Schedule 2EC total. + assert ghi[a, i] == pytest.approx(expected, abs=tol), c + assert ghi[b, i] == pytest.approx(expected, abs=tol), c + assert ghi[c_, i] == pytest.approx(expected + c["extra_interest"], abs=tol), c + # 2. The claimant's credit does not depend on the arrangement, and + # the other member claims nothing. + assert credit[a, i] == pytest.approx(credit[b, i], abs=tol), c + assert np.all(result["member_credit"][:, i] == 0), c + for arrangement in (a, b, c_): + # 3. Bounds and the income limit. + assert -tol <= credit[arrangement, i] <= CAP + tol, c + if ghi[arrangement, i] >= INCOME_LIMIT + tol: + assert credit[arrangement, i] == 0, c + # 4. More income never raises the credit. + assert credit[c_, i] <= credit[a, i] + tol, c + + +def test_worked_example(): + """2024 Schedule 2EC: line 5 pension 20,000; line 17 the other member's + interest 10,000; line 18 = 30,000; line 22 = (30,000 - 12,600) x 0.05 = + 870; line 27 = 2,000 - 870 = 1,130; line 30 = 1,130 in both + arrangements. With 5,000 more interest, line 18 = 35,000; line 22 = + 1,120; line 28 = 880; line 29 = 40%; line 30 = 352.""" + case = { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 2_000, + "rent": 0, + "member_age": 40, + "interest": 10_000, + "capital_gain": 0, + "member_ss": 0, + "extra_interest": 5_000, + } + result = calculate([case]) + assert result["gross_household_income"][:, 0] == pytest.approx( + [30_000, 30_000, 35_000], abs=TOLERANCE + ) + assert result["credit"][:, 0] == pytest.approx([1_130, 1_130, 352], abs=TOLERANCE) + assert_properties([case]) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + @st.composite + def case(draw): + return { + "claimant_age": draw(st.integers(min_value=62, max_value=90)), + "pension": draw(amount(40_000)), + "claimant_ss": draw(amount(30_000)), + "property_tax": draw(st.integers(min_value=0, max_value=4_000)), + "rent": draw(amount(12_000)), + "member_age": draw(st.integers(min_value=15, max_value=61)), + "interest": draw(amount(40_000)), + "capital_gain": draw( + st.one_of(st.just(0), st.integers(min_value=-20_000, max_value=40_000)) + ), + "member_ss": draw(amount(20_000)), + "extra_interest": draw(st.integers(min_value=1, max_value=20_000)), + } + + @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.given(st.lists(case(), min_size=1, max_size=8)) + def test_properties(cases): + assert_properties(cases) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py index d2db70646cc..735e9a7bafc 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py @@ -14,6 +14,9 @@ class mt_elderly_homeowner_or_renter_credit_gross_household_income(Variable): # 2023 Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 9 # (renamed Schedule 2EC from 2024) "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=52", + # 2023 instructions, lines 1 and 8: the returns of each member of + # the household, and the wages of members who do not file + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51", # 2024 Schedule 2EC, line 17: income received by other members of # the household "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=48", @@ -24,9 +27,17 @@ def formula(person, period, parameters): period ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter sources = add(person, period, p.gross_income_sources) - # The sources count only the taxable portion of Social Security: a - # filer's in federal AGI, a dependent's in their own gross income. - # Add the untaxed portion so all SS is counted per + # Gross household income counts every member of the household + # (§ 15-30-2337(4)). A tax unit dependent's income is on their own + # return, not in adjusted_gross_income_person (irs_gross_income + # leaves dependents out), so count their gross income directly: + # gains but not losses, and the taxable part of their Social + # Security. + is_dependent = person("is_tax_unit_dependent", period) + dependent_income = is_dependent * person("dependent_gross_income", period) + # The income above counts only the taxable portion of Social + # Security: a filer's in federal AGI, a dependent's in their own + # gross income. Add the untaxed portion so all SS is counted per # § 15-30-2337(9)(a)(viii). social_security = person("social_security", period) taxable_social_security = add( @@ -42,4 +53,4 @@ def formula(person, period, parameters): # losses."). The return's losses are added once, to the head. head = person("is_tax_unit_head", period) losses = head * person.tax_unit("loss_ald", period) - return sources + untaxed_social_security + losses + return sources + dependent_income + untaxed_social_security + losses From 98d3742c354403a5cd3e1aabf69ec3a3769cd63d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 10:54:50 -0400 Subject: [PATCH 06/13] Limit the Montana elderly homeowner/renter credit to one claimant per household MCA 15-30-2341(1): "Only one claimant per household in a claim period ... is entitled to relief." Since gross household income now counts every household member, two eligible tax units in one household (for example two elderly siblings filing separately) each computed a credit on the same household income and both were paid. - mt_elderly_homeowner_or_renter_credit_pre_one_claimant holds the existing computation (Schedule 2EC lines 18-30) for every eligible tax unit. - mt_elderly_homeowner_or_renter_credit_selected_claimant (TaxUnit) ranks the eligible tax unit heads within each household by that credit with get_rank and selects the largest; ties keep member order, so exactly one tax unit per household with an eligible one is selected. - mt_elderly_homeowner_or_renter_credit pays only the selected tax unit. Six legacy credit cases that never set a state now place their households in Montana. Adds YAML cases (two eligible tax units, one eligible, a joint couple, a tie, separate households) and property tests against a Schedule 2EC reference. Co-Authored-By: Claude Opus 5.5 --- ...t-ehrc-one-claimant-per-household.fixed.md | 1 + ...mt_elderly_homeowner_or_renter_credit.yaml | 181 ++++++++++++ ...ner_or_renter_credit_pre_one_claimant.yaml | 37 +++ ...er_or_renter_credit_selected_claimant.yaml | 224 +++++++++++++++ .../test_mt_elderly_credit_one_claimant.py | 260 ++++++++++++++++++ .../mt_elderly_homeowner_or_renter_credit.py | 39 +-- ...r_or_renter_credit_net_household_income.py | 5 +- ...owner_or_renter_credit_pre_one_claimant.py | 50 ++++ ...wner_or_renter_credit_selected_claimant.py | 49 ++++ 9 files changed, 813 insertions(+), 33 deletions(-) create mode 100644 changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml create mode 100644 policyengine_us/tests/test_mt_elderly_credit_one_claimant.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py diff --git a/changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md b/changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md new file mode 100644 index 00000000000..2f6c0989d0b --- /dev/null +++ b/changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md @@ -0,0 +1 @@ +Limit the Montana elderly homeowner/renter credit to one claimant per household (MCA 15-30-2341(1)): when several tax units in a household qualify, only the one with the largest credit is paid. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index 6f2d13342d2..eed5f7075fc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -11,6 +11,10 @@ tax_units: tax_unit: members: [person1] + households: + household: + members: [person1] + state_code: MT output: mt_elderly_homeowner_or_renter_credit: 0 @@ -33,6 +37,10 @@ tax_units: tax_unit: members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT output: # min_((propertytax + rent*rate - (grossincome-standard)*reduction_rate), 1150) * percentage(grossincome) # 1500+ 9000*2*0.15 - (34000-12600)*0.05 = 3130 # 1150*100% @@ -57,6 +65,10 @@ tax_units: tax_unit: members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT output: # min_((propertytax + rent*rate - (grossincome-standard)*reduction_rate), 1000) * percentage(grossincome) # 1500+ 9000*2*0.15 - (34000-6300)*0.05 = 2815 # 1000*100% @@ -87,6 +99,10 @@ tax_units: tax_unit: members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT output: # min_((propertytax + rent*rate - (grossincome-standard)*reduction_rate), 1150) * percentage(grossincome) # 1000+ 9000*0.15 - (43000-12600)*0.05 = 830 # 830*10% = 83 @@ -111,6 +127,10 @@ tax_units: tax_unit: members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT output: # Each person's gross household income is under the $35,000 threshold # (multiplier 1.0 per-person), but the tax-unit sum is $44,000 which @@ -130,6 +150,10 @@ tax_units: tax_unit: members: [person1] + households: + household: + members: [person1] + state_code: MT output: mt_elderly_homeowner_or_renter_credit: 0 @@ -399,3 +423,160 @@ mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] mt_elderly_homeowner_or_renter_credit: [1_130, 0] + +- name: 2024 Schedule 2EC, two eligible siblings filing separately in one household; only the larger claim is paid + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 4_800 + person2: + age: 70 + taxable_pension_income: 15_000 + rent: 7_200 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Both siblings are 62 or older and file their own returns. Line 18 + # gross household income = 10_000 + 15_000 = 25_000 for each; line 20 = + # 25_000 - 12_600 = 12_400; line 21 = 0.05; line 22 = 620. + # person1: line 25 = 4_800 x 0.15 = 720; line 27 = 720 - 620 = 100. + # person2: line 25 = 7_200 x 0.15 = 1_080; line 27 = 1_080 - 620 = 460. + # Line 29 = 100% (under 35_000). "Only one claimant per household in a + # claim period ... is entitled to relief" (§ 15-30-2341(1)); the + # household claims through person2, whose credit is larger. Before the + # limit both were paid, 560 in total. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [100, 460] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + mt_elderly_homeowner_or_renter_credit: [0, 460] + mt_refundable_credits: [0, 460] + +- name: 2024 Schedule 2EC, one eligible tax unit and one younger household member who files separately + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 10_000 + rent: 4_800 + person2: + age: 58 + taxable_pension_income: 15_000 + rent: 7_200 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Same household as above, but person2 is under 62 and cannot be a + # claimant (§ 15-30-2338(1)(a)), even though their rent is larger. + # Line 22 = 620; person1's line 27 = 720 - 620 = 100. + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit: [100, 0] + +- name: 2025 Schedule 2EC, a joint couple who both qualify make one claim + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 15_000 + real_estate_taxes: 2_000 + person2: + age: 66 + taxable_pension_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # "If filing jointly and both spouses qualify for the credit, enter the + # spouse listed as the taxpayer on page 1 as the claimant" (2025 + # instructions). Line 18 = 25_000; line 22 = 12_400 x 0.05 = 620; + # line 27 = 2_000 - 620 = 1_380; line 28 = 1_150 cap; line 30 = 1_150, + # held by the tax unit head. + mt_elderly_homeowner_or_renter_credit_selected_claimant: true + mt_elderly_homeowner_or_renter_credit: [1_150, 0] + +- name: 2024 Schedule 2EC, two eligible tax units with equal credits; exactly one claims + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 68 + taxable_pension_income: 12_500 + rent: 6_000 + person2: + age: 72 + taxable_pension_income: 12_500 + rent: 6_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 25_000; line 22 = 620; each line 27 = 900 - 620 = 280. The + # household gets one 280 credit; the tie goes to the first tax unit in + # the household's member order. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [280, 280] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit: [280, 0] + +- name: 2024 Schedule 2EC, the one-claimant limit applies within each household, not across households + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 75 + taxable_pension_income: 10_000 + rent: 6_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household1: + members: [person1] + state_name: MT + household2: + members: [person2] + state_name: MT + output: + # Household 1: line 18 = 20_000; line 20 = 7_400; line 21 = 0.035; + # line 22 = 259; line 27 = 2_000 - 259 = 1_741; line 28 = 1_150. + # Household 2: line 18 = 10_000; line 20 = 0; line 22 = 0; + # line 27 = 6_000 x 0.15 = 900. Each household has its own claimant. + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, true] + mt_elderly_homeowner_or_renter_credit: [1_150, 900] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml new file mode 100644 index 00000000000..533e31da039 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml @@ -0,0 +1,37 @@ +- name: Case 1, each eligible tax unit's credit before the one-claimant limit. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 4_800 + person2: + age: 70 + taxable_pension_income: 15_000 + rent: 7_200 + person3: + age: 40 + taxable_interest_income: 5_000 + rent: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # 2024 Schedule 2EC. Line 18 = 10_000 + 15_000 + 5_000 = 30_000 for the + # whole household; line 20 = 17_400; line 21 = 0.05; line 22 = 870. + # person1: line 25 = 720; line 27 = max(720 - 870, 0) = 0. + # person2: line 25 = 1_080; line 27 = 1_080 - 870 = 210. + # person3 is under 62, so not a claimant (§ 15-30-2338(1)(a)). + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [0, 210, 0] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true, false] + mt_elderly_homeowner_or_renter_credit: [0, 210, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml new file mode 100644 index 00000000000..c25d6f4df78 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml @@ -0,0 +1,224 @@ +- name: Case 1, two eligible tax units in one household; the larger credit claims. + period: 2024 + input: + people: + person1: + age: 66 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + person2: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 460 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + +- name: Case 2, equal credits; the first tax unit in member order claims. + period: 2024 + input: + people: + person1: + age: 68 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 280 + person2: + age: 72 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 280 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + +- name: Case 3, an ineligible tax unit is never selected, whatever its amount. + period: 2024 + input: + people: + person1: + age: 58 + mt_elderly_homeowner_or_renter_credit_eligible: false + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person2: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + +- name: Case 4, no eligible tax unit in the household. + period: 2024 + input: + people: + person1: + age: 45 + mt_elderly_homeowner_or_renter_credit_eligible: false + person2: + age: 50 + mt_elderly_homeowner_or_renter_credit_eligible: false + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, false] + +- name: Case 5, three eligible tax units; the largest credit claims wherever it is listed. + period: 2024 + input: + people: + person1: + age: 64 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + person2: + age: 80 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person3: + age: 75 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 460 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true, false] + +- name: Case 6, the limit is per household; each household selects its own claimant. + period: 2024 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 1_150 + person2: + age: 75 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person3: + age: 66 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 300 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household1: + members: [person1] + state_code: MT + household2: + members: [person2, person3] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, true, false] + +- name: Case 7, a joint couple is one tax unit and one claim. + period: 2025 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 1_150 + person2: + age: 66 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 0 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: true + +- name: Case 8, a dependent in an eligible tax unit is not a separate claimant. + period: 2024 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 500 + person2: + age: 12 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 0 + person3: + age: 67 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 200 + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + +- name: Case 9, outside Montana no tax unit is selected. + period: 2024 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: false diff --git a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py new file mode 100644 index 00000000000..0d63e528d27 --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py @@ -0,0 +1,260 @@ +"""Property tests: one claimant per household for the Montana elderly +homeowner/renter credit. + +MCA 15-30-2341(1): "Only one claimant per household in a claim period under +the provisions of 15-30-2337 through 15-30-2341 is entitled to relief." The +2024 and 2025 Schedule 2EC attestation reads "I am the only member of my +household claiming this credit". Gross household income (line 18) covers +everyone in the household, so every tax unit in a household with a member +aged 62 or older computes its credit on the same net household income (line +22) and multiplier (line 29); the credits differ only by each tax unit's own +property tax (line 23) and rent (line 24). The model pays the tax unit whose +credit is largest. + +A reference written here from the schedule's lines is compared with the +model for random Montana households of one to three tax units (single or +joint, any mix of ages, pension, interest, Social Security, property tax and +rent; no earned income, so no refundable credit enters line 8): + +1. Each tax unit's credit before the limit matches the reference. +2. The household's credit is the largest of its tax units' credits, so it + never exceeds the $1,150 cap of a single claim. +3. At most one tax unit per household is paid. Exactly one is selected in a + household with an eligible tax unit, and none in a household without one. +4. A tax unit is paid either nothing or its whole credit, and the selected + one is paid its whole credit. +5. Reversing the order of the tax units in a household does not change the + household's credit. + +The model computes in single precision, so comparisons allow one cent or +eight float32 spacings at the household's total income, whichever is larger. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system + +YEAR = 2024 +TOLERANCE = 0.01 +P = system.parameters(YEAR).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter + + +def build_situation(households): + """Each household is a list of tax units; each tax unit is a dict with + "adults" (a list of dicts with age, pension, interest and social + security) and the head's property tax and rent. Every household is built + twice: as given, and with its tax units in reverse order.""" + people, tax_units, marital_units, spm_units, families, situation_households = ( + {}, + {}, + {}, + {}, + {}, + {}, + ) + for h, units in enumerate(households): + for order, ordered_units in (("fwd", units), ("rev", units[::-1])): + key = f"{order}{h}" + members = [] + for u, unit in enumerate(ordered_units): + unit_key = f"{key}_u{u}" + unit_members = [] + for a, adult in enumerate(unit["adults"]): + person = f"{unit_key}_p{a}" + people[person] = { + "age": {YEAR: adult["age"]}, + "taxable_pension_income": {YEAR: adult["pension"]}, + "taxable_interest_income": {YEAR: adult["interest"]}, + "social_security_retirement": {YEAR: adult["social_security"]}, + "is_tax_unit_dependent": {YEAR: False}, + } + unit_members.append(person) + # The first adult is the oldest, so the head: the property + # tax and rent are theirs. + head = unit_members[0] + people[head]["real_estate_taxes"] = {YEAR: unit["property_tax"]} + people[head]["rent"] = {YEAR: unit["rent"]} + tax_units[unit_key] = {"members": unit_members} + marital_units[unit_key] = {"members": unit_members} + members += unit_members + spm_units[key] = {"members": members} + families[key] = {"members": members} + situation_households[key] = { + "members": members, + "state_code": {YEAR: "MT"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": spm_units, + "families": families, + "households": situation_households, + } + + +def reference(units): + """Each tax unit's credit before the one-claimant limit, from the 2024 + Schedule 2EC lines.""" + # Line 18: the pension, interest and Social Security of everyone in the + # household. + gross = sum( + adult["pension"] + adult["interest"] + adult["social_security"] + for unit in units + for adult in unit["adults"] + ) + # Lines 19-22. + reduced = max(gross - P.net_household_income.standard_exclusion, 0) + net = reduced * float(P.net_household_income.reduction_rate.calc(reduced)) + # Line 29. + multiplier = float(P.multiplier.calc(gross)) + credits = [] + for unit in units: + eligible = max(adult["age"] for adult in unit["adults"]) >= P.age_threshold + # Lines 23-28. + housing = unit["property_tax"] + unit["rent"] * P.rent_equivalent_tax_rate + capped = min(max(housing - net, 0), P.cap) + credits.append(eligible * capped * multiplier) + return gross, credits + + +def tolerance(amount): + return max(TOLERANCE, 8 * float(np.spacing(np.float32(abs(amount))))) + + +def calculate(households): + """Per tax unit, in situation order: the credit before the limit, the + selection and the credit.""" + simulation = Simulation(situation=build_situation(households)) + + def by_tax_unit(variable): + return np.asarray(simulation.calculate(variable, YEAR, map_to="tax_unit")) + + return { + "pre_one_claimant": by_tax_unit( + "mt_elderly_homeowner_or_renter_credit_pre_one_claimant" + ), + "selected": np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit_selected_claimant", YEAR + ) + ), + "credit": by_tax_unit("mt_elderly_homeowner_or_renter_credit"), + } + + +def assert_properties(households): + result = calculate(households) + start = 0 + for units in households: + n = len(units) + gross, expected = reference(units) + tol = tolerance(gross) + totals = {} + for order, ordered_expected in (("fwd", expected), ("rev", expected[::-1])): + pre = result["pre_one_claimant"][start : start + n] + selected = result["selected"][start : start + n] + credit = result["credit"][start : start + n] + start += n + # 1. Each tax unit's credit before the limit. + assert pre == pytest.approx(ordered_expected, abs=tol), units + # 2. The household is paid its largest credit, within the cap. + totals[order] = credit.sum() + assert totals[order] == pytest.approx(max(expected), abs=tol), units + assert -tol <= totals[order] <= P.cap + tol, units + # 3. At most one payment; one selection where any tax unit is + # eligible. + assert (credit > tol).sum() <= 1, units + any_eligible = any( + max(adult["age"] for adult in unit["adults"]) >= P.age_threshold + for unit in units + ) + assert selected.sum() == int(any_eligible), units + # 4. Nothing or the whole credit; the selected unit gets it all. + assert np.all((np.abs(credit) <= tol) | (np.abs(credit - pre) <= tol)), ( + units + ) + assert credit[selected] == pytest.approx(pre[selected], abs=tol), units + # 5. The order of the tax units does not matter. + assert totals["fwd"] == pytest.approx(totals["rev"], abs=tol), units + + +def adult(age, pension=0, interest=0, social_security=0): + return { + "age": age, + "pension": pension, + "interest": interest, + "social_security": social_security, + } + + +def test_worked_example(): + """2024 Schedule 2EC: two siblings, 66 and 70, filing separately in one + household with pensions of 10,000 and 15,000 and rent of 4,800 and + 7,200. Line 18 = 25,000; line 22 = 12,400 x 0.05 = 620. Line 27 is + 720 - 620 = 100 for the first and 1,080 - 620 = 460 for the second. + Only the second is paid; before the limit the household received 560.""" + households = [ + [ + {"adults": [adult(66, pension=10_000)], "property_tax": 0, "rent": 4_800}, + {"adults": [adult(70, pension=15_000)], "property_tax": 0, "rent": 7_200}, + ] + ] + result = calculate(households) + # Forward order, then reversed. + assert result["pre_one_claimant"] == pytest.approx( + [100, 460, 460, 100], abs=TOLERANCE + ) + assert result["selected"].tolist() == [False, True, True, False] + assert result["credit"] == pytest.approx([0, 460, 460, 0], abs=TOLERANCE) + assert_properties(households) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + @st.composite + def person(draw): + return adult( + draw(st.integers(min_value=40, max_value=90)), + pension=draw(amount(25_000)), + interest=draw(amount(15_000)), + social_security=draw(amount(20_000)), + ) + + @st.composite + def tax_unit(draw): + adults = draw(st.lists(person(), min_size=1, max_size=2)) + # Put the oldest first so that the head holds the housing costs. + adults.sort(key=lambda a: -a["age"]) + if len(adults) == 2 and adults[0]["age"] == adults[1]["age"]: + adults[1]["age"] -= 1 + return { + "adults": adults, + "property_tax": draw(amount(4_000)), + "rent": draw(amount(12_000)), + } + + @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.given( + st.lists( + st.lists(tax_unit(), min_size=1, max_size=3), + min_size=1, + max_size=4, + ) + ) + def test_properties(households): + assert_properties(households) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py index f2cbf8e8162..b0f123f4552 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py @@ -9,41 +9,18 @@ class mt_elderly_homeowner_or_renter_credit(Variable): definition_period = YEAR defined_for = "mt_elderly_homeowner_or_renter_credit_eligible" reference = ( - "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0410/0150-0300-0230-0410.html", # 2024 Form 2, Schedule 2EC "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", ) def formula(person, period, parameters): - p = parameters( - period - ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter - # § 15-30-2337(4) defines gross household income as "all income - # received by all individuals of a household", including members - # outside the claimant's return (2024 Schedule 2EC line 17), so the - # multiplier phase-out applies to the household's total income rather - # than each person's share. - gross_household_income = add( - person.household, - period, - ["mt_elderly_homeowner_or_renter_credit_gross_household_income"], + # Only one claimant per household is entitled to relief + # (§ 15-30-2341(1)). + credit = person( + "mt_elderly_homeowner_or_renter_credit_pre_one_claimant", period ) - # Get net_household_income and allocate it to the head - head = person("is_tax_unit_head", period) - net_household_income = add( - person.tax_unit, - period, - ["mt_elderly_homeowner_or_renter_credit_net_household_income"], + selected = person.tax_unit( + "mt_elderly_homeowner_or_renter_credit_selected_claimant", period ) - # Credit Computation - property_tax = add(person.tax_unit, period, ["real_estate_taxes"]) - rent = add(person.tax_unit, period, ["rent"]) - countable_rent = rent * p.rent_equivalent_tax_rate - countable_rent_and_property_tax = property_tax + countable_rent - uncapped_credit_unit = max_( - countable_rent_and_property_tax - net_household_income, 0 - ) - uncapped_credit = uncapped_credit_unit * head - capped_credit = min_(uncapped_credit, p.cap) - multiplier = p.multiplier.calc(gross_household_income) - return capped_credit * multiplier + return credit * selected diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py index a46ccf6f5c6..cebec7893d8 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py @@ -18,8 +18,9 @@ def formula(person, period, parameters): p = parameters( period ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter - # Only one claim is allowed per household - # married taxpayer who are living apart may qualify for only one credit per year + # Only one claimant per household is entitled to relief + # (§ 15-30-2341(1)); mt_elderly_homeowner_or_renter_credit_selected_claimant + # picks that claimant. standard_exclusion = p.net_household_income.standard_exclusion # Gross household income counts every member of the household, # including those outside the claimant's return (§ 15-30-2337(4); diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py new file mode 100644 index 00000000000..12486d784eb --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py @@ -0,0 +1,50 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_pre_one_claimant(Variable): + value_type = float + entity = Person + label = "Montana Elderly Homeowner/Renter Credit before the one-claimant limit" + unit = USD + definition_period = YEAR + defined_for = "mt_elderly_homeowner_or_renter_credit_eligible" + reference = ( + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0400/0150-0300-0230-0400.html", + # 2024 Form 2, Schedule 2EC + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter + # § 15-30-2337(4) defines gross household income as "all income + # received by all individuals of a household", including members + # outside the claimant's return (2024 Schedule 2EC line 17), so the + # multiplier phase-out applies to the household's total income rather + # than each person's share. + gross_household_income = add( + person.household, + period, + ["mt_elderly_homeowner_or_renter_credit_gross_household_income"], + ) + # Get net_household_income and allocate it to the head + head = person("is_tax_unit_head", period) + net_household_income = add( + person.tax_unit, + period, + ["mt_elderly_homeowner_or_renter_credit_net_household_income"], + ) + # Credit Computation + property_tax = add(person.tax_unit, period, ["real_estate_taxes"]) + rent = add(person.tax_unit, period, ["rent"]) + countable_rent = rent * p.rent_equivalent_tax_rate + countable_rent_and_property_tax = property_tax + countable_rent + uncapped_credit_unit = max_( + countable_rent_and_property_tax - net_household_income, 0 + ) + uncapped_credit = uncapped_credit_unit * head + capped_credit = min_(uncapped_credit, p.cap) + multiplier = p.multiplier.calc(gross_household_income) + return capped_credit * multiplier diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py new file mode 100644 index 00000000000..8b83c731903 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py @@ -0,0 +1,49 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_selected_claimant(Variable): + value_type = bool + entity = TaxUnit + label = "Selected claimant for the Montana Elderly Homeowner/Renter Credit" + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # § 15-30-2341(1): one claimant per household + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0410/0150-0300-0230-0410.html", + # § 15-30-2337(7): household + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + # 2025 Schedule 2EC attestation: "I am the only member of my + # household claiming this credit" + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + # 2025 instructions: the attestation, the household definition and + # the joint filers' claimant + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=45", + # 2025 instructions, lines 23 and 24: the claimant's own property + # tax and rent + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47", + ) + + def formula(tax_unit, period, parameters): + # "Only one claimant per household in a claim period ... is entitled + # to relief" (§ 15-30-2341(1)). The law does not say which member + # claims. Every eligible tax unit in a household has the same gross + # and net household income, so their credits differ only by each + # one's own property tax billed and rent paid (Schedule 2EC line 24 + # is "the rent that you paid"; property in another's name "can + # qualify as rent only"). We select the tax unit whose credit is + # largest: the member who bears the most housing cost, and the claim + # a household would choose. Ties keep the household's member order, + # since get_rank sorts stably, so each household with an eligible + # tax unit has exactly one selected. A joint return is one tax unit + # and one claim; the 2025 instructions name the spouse listed as the + # taxpayer as the claimant when both qualify, and the model holds + # that credit on the tax unit head. + person = tax_unit.members + head = person("is_tax_unit_head", period) + eligible = person("mt_elderly_homeowner_or_renter_credit_eligible", period) + credit = person( + "mt_elderly_homeowner_or_renter_credit_pre_one_claimant", period + ) + candidate = head & eligible + rank = person.get_rank(person.household, -credit, condition=candidate) + return tax_unit.any(candidate & (rank == 0)) From 20c68cf1258a4c4d6a4aefadea7c495d4df56d2b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 13:25:17 -0400 Subject: [PATCH 07/13] Cite ARM 42.4.302 and make the head-filter test bite - Cite ARM 42.4.302, the rule implementing 15-30-2341, in the selected claimant's references; note that neither it nor the statute says which member claims. - Restore the living-apart note in the net household income comment with its source (ARM 42.4.302(3)(a)), and say in the selection comment that a separated spouse in another tax unit is not linked, so that limit is not modeled. - Selected-claimant Case 8 now fails if non-heads are ranked: a dependent's input amount larger than the other tax unit's no longer decides the selection. Co-Authored-By: Claude Opus 5.5 --- ...meowner_or_renter_credit_selected_claimant.yaml | 10 ++++++---- ...eowner_or_renter_credit_net_household_income.py | 3 ++- ...homeowner_or_renter_credit_selected_claimant.py | 14 +++++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml index c25d6f4df78..31b6c0d4192 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml @@ -177,18 +177,20 @@ output: mt_elderly_homeowner_or_renter_credit_selected_claimant: true -- name: Case 8, a dependent in an eligible tax unit is not a separate claimant. +- name: Case 8, only tax unit heads are ranked; another member's amount does not count. period: 2024 input: people: person1: age: 70 mt_elderly_homeowner_or_renter_credit_eligible: true - mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 500 + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 person2: + # A dependent: the model holds a tax unit's credit on its head, so + # this amount, set here only to test the ranking, is ignored. age: 12 mt_elderly_homeowner_or_renter_credit_eligible: true - mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 0 + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 person3: age: 67 mt_elderly_homeowner_or_renter_credit_eligible: true @@ -203,7 +205,7 @@ members: [person1, person2, person3] state_code: MT output: - mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] - name: Case 9, outside Montana no tax unit is selected. period: 2024 diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py index cebec7893d8..56053085070 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py @@ -20,7 +20,8 @@ def formula(person, period, parameters): ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter # Only one claimant per household is entitled to relief # (§ 15-30-2341(1)); mt_elderly_homeowner_or_renter_credit_selected_claimant - # picks that claimant. + # picks the claimant. Married couples living apart may receive only + # one credit per year (ARM 42.4.302(3)(a)). standard_exclusion = p.net_household_income.standard_exclusion # Gross household income counts every member of the household, # including those outside the claimant's return (§ 15-30-2337(4); diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py index 8b83c731903..82058dcbfa6 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py @@ -12,6 +12,10 @@ class mt_elderly_homeowner_or_renter_credit_selected_claimant(Variable): "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0410/0150-0300-0230-0410.html", # § 15-30-2337(7): household "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + # ARM 42.4.302 (implementing § 15-30-2341): property tax billed to + # another counts as rent; married couples living apart receive only + # one credit per year + "https://rules.mt.gov/gateway/RuleNo.asp?RN=42%2E4%2E302", # 2025 Schedule 2EC attestation: "I am the only member of my # household claiming this credit" "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", @@ -25,8 +29,9 @@ class mt_elderly_homeowner_or_renter_credit_selected_claimant(Variable): def formula(tax_unit, period, parameters): # "Only one claimant per household in a claim period ... is entitled - # to relief" (§ 15-30-2341(1)). The law does not say which member - # claims. Every eligible tax unit in a household has the same gross + # to relief" (§ 15-30-2341(1)). Neither the statute nor ARM 42.4.302 + # says which member claims. Every eligible tax unit in a household has + # the same gross # and net household income, so their credits differ only by each # one's own property tax billed and rent paid (Schedule 2EC line 24 # is "the rent that you paid"; property in another's name "can @@ -37,7 +42,10 @@ def formula(tax_unit, period, parameters): # tax unit has exactly one selected. A joint return is one tax unit # and one claim; the 2025 instructions name the spouse listed as the # taxpayer as the claimant when both qualify, and the model holds - # that credit on the tax unit head. + # that credit on the tax unit head. ARM 42.4.302(3)(a) also allows + # married couples living apart only one credit a year; a separated + # spouse in another tax unit is not linked to the other spouse, so + # that limit is not modeled. person = tax_unit.members head = person("is_tax_unit_head", period) eligible = person("mt_elderly_homeowner_or_renter_credit_eligible", period) From ad487399b17e8eb7d4627cfc15cc30d62fb4b1af Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 16:46:26 -0400 Subject: [PATCH 08/13] Describe the claimant ranking and cost reading accurately; pin examples Independent review (Opus) asked for text fixes only: - The selection ranks by Schedule 2EC line 26 (property tax billed plus 15% of rent, before the cap), not by total housing cost; say so. - Counting each tax unit's own property tax and rent follows the form's wording; the statute defines both for the homestead (MCA 15-30-2337(5), (10)). Name that reading as not modeled (queued for a ruling as decision d1035). - Uniqueness of the selection comes from get_rank's ranks; its stable sort only breaks ties. - Cite ARM 42.4.302 at Cornell, which shows the rule text. The Hypothesis test is now derandomized and pins a tie at the cap and an owner against a renter who pays more but has the smaller line 26. Co-Authored-By: Claude Opus 5.5 --- .../test_mt_elderly_credit_one_claimant.py | 41 +++++++++++++++++- ...wner_or_renter_credit_selected_claimant.py | 42 ++++++++++--------- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py index 0d63e528d27..d1dc874a300 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py +++ b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py @@ -248,7 +248,9 @@ def tax_unit(draw): "rent": draw(amount(12_000)), } - @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=15, deadline=None, derandomize=True, suppress_health_check=SLOW + ) @hypothesis.given( st.lists( st.lists(tax_unit(), min_size=1, max_size=3), @@ -256,5 +258,42 @@ def tax_unit(draw): max_size=4, ) ) + # Both tax units reach the $1,150 cap (line 18 = 20,000; line 22 = 259; + # line 27 = 2,741 and 1,741): one claim of 1,150. + @hypothesis.example( + [ + [ + { + "adults": [adult(70, pension=10_000)], + "property_tax": 3_000, + "rent": 0, + }, + { + "adults": [adult(66, pension=10_000)], + "property_tax": 2_000, + "rent": 0, + }, + ] + ] + ) + # A renter paying 6,000 (line 26 = 900) and an owner billed 1,000 (line + # 26 = 1,000): the owner's 741 is paid, not the renter's 641, although + # the renter pays more for housing. + @hypothesis.example( + [ + [ + { + "adults": [adult(70, pension=10_000)], + "property_tax": 0, + "rent": 6_000, + }, + { + "adults": [adult(66, pension=10_000)], + "property_tax": 1_000, + "rent": 0, + }, + ] + ] + ) def test_properties(households): assert_properties(households) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py index 82058dcbfa6..80ed81d5f1e 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py @@ -10,20 +10,21 @@ class mt_elderly_homeowner_or_renter_credit_selected_claimant(Variable): reference = ( # § 15-30-2341(1): one claimant per household "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0410/0150-0300-0230-0410.html", - # § 15-30-2337(7): household + # § 15-30-2337(5), (7), (10): gross rent, household, property tax + # billed "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", # ARM 42.4.302 (implementing § 15-30-2341): property tax billed to # another counts as rent; married couples living apart receive only # one credit per year - "https://rules.mt.gov/gateway/RuleNo.asp?RN=42%2E4%2E302", + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", # 2025 Schedule 2EC attestation: "I am the only member of my # household claiming this credit" "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", # 2025 instructions: the attestation, the household definition and # the joint filers' claimant "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=45", - # 2025 instructions, lines 23 and 24: the claimant's own property - # tax and rent + # 2025 instructions, lines 23 and 24: "the property tax you were + # billed" and "the rent that you paid" "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47", ) @@ -31,21 +32,24 @@ def formula(tax_unit, period, parameters): # "Only one claimant per household in a claim period ... is entitled # to relief" (§ 15-30-2341(1)). Neither the statute nor ARM 42.4.302 # says which member claims. Every eligible tax unit in a household has - # the same gross - # and net household income, so their credits differ only by each - # one's own property tax billed and rent paid (Schedule 2EC line 24 - # is "the rent that you paid"; property in another's name "can - # qualify as rent only"). We select the tax unit whose credit is - # largest: the member who bears the most housing cost, and the claim - # a household would choose. Ties keep the household's member order, - # since get_rank sorts stably, so each household with an eligible - # tax unit has exactly one selected. A joint return is one tax unit - # and one claim; the 2025 instructions name the spouse listed as the - # taxpayer as the claimant when both qualify, and the model holds - # that credit on the tax unit head. ARM 42.4.302(3)(a) also allows - # married couples living apart only one credit a year; a separated - # spouse in another tax unit is not linked to the other spouse, so - # that limit is not modeled. + # the same gross and net household income, so their credits differ + # only by Schedule 2EC line 26: property tax billed plus 15% of rent, + # before the $1,150 cap. The model counts each tax unit's own + # property tax and rent, following the form's wording ("the property + # tax you were billed", "the rent that you paid"; property in + # another's name "can qualify as rent only"). The statute instead + # defines both for the homestead (§ 15-30-2337(5), (10)), which would + # give the one claimant the household's whole property tax and rent; + # that reading is not modeled. We select the tax unit whose credit is + # largest, the claim a household would choose. get_rank ranks the + # candidates in each household 0, 1, 2, ..., so exactly one has rank + # 0; its stable sort gives a tie to the first in member order. A + # joint return is one tax unit and one claim; the 2025 instructions + # name the spouse listed as the taxpayer as the claimant when both + # qualify, and the model holds that credit on the tax unit head. + # ARM 42.4.302(3)(a) also allows married couples living apart only + # one credit a year; a separated spouse in another tax unit is not + # linked to the other spouse, so that limit is not modeled. person = tax_unit.members head = person("is_tax_unit_head", period) eligible = person("mt_elderly_homeowner_or_renter_credit_eligible", period) From 11b47a5b49b8740dc5d125a8e583934ec28389cc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 18:56:48 -0400 Subject: [PATCH 09/13] Attribute Schedule 2EC quotes to the form's line labels, not the instructions Co-Authored-By: Claude Opus 5.5 --- ...wner_or_renter_credit_selected_claimant.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py index 80ed81d5f1e..c28c01f485d 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py @@ -17,14 +17,16 @@ class mt_elderly_homeowner_or_renter_credit_selected_claimant(Variable): # another counts as rent; married couples living apart receive only # one credit per year "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", - # 2025 Schedule 2EC attestation: "I am the only member of my - # household claiming this credit" + # 2025 Schedule 2EC: the attestation "I am the only member of my + # household claiming this credit"; line 23 "the property tax you were + # billed"; line 24 "the rent that you paid" "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", # 2025 instructions: the attestation, the household definition and # the joint filers' claimant "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=45", - # 2025 instructions, lines 23 and 24: "the property tax you were - # billed" and "the rent that you paid" + # 2025 instructions, lines 23 and 24: property in another's name "can + # qualify as rent only"; "Your rent is only the amount of money that + # you paid to occupy your home" "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47", ) @@ -35,15 +37,16 @@ def formula(tax_unit, period, parameters): # the same gross and net household income, so their credits differ # only by Schedule 2EC line 26: property tax billed plus 15% of rent, # before the $1,150 cap. The model counts each tax unit's own - # property tax and rent, following the form's wording ("the property - # tax you were billed", "the rent that you paid"; property in - # another's name "can qualify as rent only"). The statute instead + # property tax and rent, following Schedule 2EC's line labels ("the + # property tax you were billed", "the rent that you paid") and its + # instructions (property in another's name "can qualify as rent + # only"). The statute instead # defines both for the homestead (§ 15-30-2337(5), (10)), which would # give the one claimant the household's whole property tax and rent; # that reading is not modeled. We select the tax unit whose credit is # largest, the claim a household would choose. get_rank ranks the - # candidates in each household 0, 1, 2, ..., so exactly one has rank - # 0; its stable sort gives a tie to the first in member order. A + # candidates in each household 0, 1, 2, ..., so a household with any + # candidate has exactly one at rank 0; its stable sort gives a tie to the first in member order. A # joint return is one tax unit and one claim; the 2025 instructions # name the spouse listed as the taxpayer as the claimant when both # qualify, and the model holds that credit on the tax unit head. From 10c7727dcf5c0e6af85fb658a22c203962ace3da Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:38:45 -0400 Subject: [PATCH 10/13] Preserve dependent AGI adjustments for Montana elderly credit --- ...x-mt-ehrc-household-member-income.fixed.md | 2 +- ...mt_elderly_homeowner_or_renter_credit.yaml | 38 ++++ .../deductions/mt_tax_unit_itemizes.yaml | 38 ++++ ...est_mt_elderly_credit_household_members.py | 201 +++++++++++++++--- ...or_renter_credit_gross_household_income.py | 20 +- 5 files changed, 263 insertions(+), 36 deletions(-) diff --git a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md index 0b6584283d6..6704770f799 100644 --- a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md +++ b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md @@ -1 +1 @@ -Count household members' income in the Montana elderly homeowner/renter credit: a tax unit dependent's gross income, and the income of household members who file their own returns. +Count household members' income in the Montana elderly homeowner/renter credit, preserving dependents' permitted person-level federal AGI adjustments and excluding losses, as well as including the income of members who file their own returns. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index eed5f7075fc..a06f73e04c1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -460,6 +460,11 @@ mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] mt_elderly_homeowner_or_renter_credit: [0, 460] mt_refundable_credits: [0, 460] + taxsim_state_property_tax_credit: [0, 460] + # 2024 Form 2, PDF p. 1: federal standard deduction 16,550 plus + # old-age subtraction 5,500 exceeds either pension. Tax before + # refundable credits is zero, so only person2 receives a refund. + mt_income_tax: [0, -460] - name: 2024 Schedule 2EC, one eligible tax unit and one younger household member who files separately period: 2024 @@ -580,3 +585,36 @@ # line 27 = 6_000 x 0.15 = 900. Each household has its own claimant. mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, true] mt_elderly_homeowner_or_renter_credit: [1_150, 900] + +- name: 2023 dependent self-employment income preserves the deductible half of SE tax + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + rent: 12_000 + child: + age: 18 + is_tax_unit_dependent: true + self_employment_income: 10_000 + tax_units: + claimant_unit: + members: [claimant, child] + households: + household: + members: [claimant, child] + state_code: MT + output: + # 2023 Schedule SE, PDF p. 1, lines 4a and 10-13: + # https://www.irs.gov/pub/irs-prior/f1040sse--2023.pdf#page=1 + # 10,000 * .9235 * (.124 + .029) / 2 = 706.4775 deductible SE tax. + # 2023 MT instructions, PDF pp. 51-52, lines 1 and 9: use each + # member's federal AGI and add back losses, preserving other adjustments. + # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 + # Income = 20,000 + 10,000 - 706.4775 = 29,293.5225. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 9_293.5225] + # Credit = 12,000 * .15 - (29,293.5225 - 12,600) * .05 = 965.323875. + mt_elderly_homeowner_or_renter_credit: [965.323875, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml index 6b5b7e020f8..a9033e68da4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml @@ -15,3 +15,41 @@ mt_standard_deduction_indiv: 10_000 output: mt_tax_unit_itemizes: false + +- name: Combined-AGI care phaseout keeps itemized deductions below the standard deduction + period: 2022 + input: + people: + head: + age: 40 + mt_agi_indiv: 20_000 + real_estate_taxes: 4_650 + spouse: + age: 40 + mt_agi_indiv: 0 + child: + age: 5 + tax_units: + tax_unit: + members: [head, spouse, child] + filing_status: JOINT + tax_unit_childcare_expenses: 3_000 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + # 2022 Form 2441-M, PDF p. 1, lines 2-7: min(3,000, 2,400) + # - .5 * (20,000 - 18,000) = 1,400; half to each spouse = 700. + # https://revenue.mt.gov/files/Forms/Child-and-Dependent-Care-Expense-Deduction-Form-2441-M/2022_Child_and_Dependent_Care_Expense_Deduction_Form_2441-M.pdf#page=1 + mt_child_dependent_care_expense_deduction: [700, 700, 0] + # 2022 Form 2 instructions, PDF p. 35: standard deductions are + # max(min(.2 * 20,000, 5,090), 2,260) = 4,000 and the 2,260 floor. + # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=35 + # Itemized total = 4,650 + 700 + 700 = 6,050 < 4,000 + 2,260 = 6,260. + # Using each spouse's AGI would give the spouse 1,200 and total 6,550, + # incorrectly switching the couple to itemization. + mt_itemized_deductions_indiv: [5_350, 700, 0] + mt_standard_deduction_indiv: [4_000, 2_260, 0] + mt_tax_unit_itemizes: false + mt_deductions_indiv: [4_000, 2_260, 0] diff --git a/policyengine_us/tests/test_mt_elderly_credit_household_members.py b/policyengine_us/tests/test_mt_elderly_credit_household_members.py index d1c1fff15ef..b123404bdcc 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_household_members.py +++ b/policyengine_us/tests/test_mt_elderly_credit_household_members.py @@ -10,16 +10,19 @@ and puts the income of "other members of your household" on line 17. The model reaches the same quantity by two paths. A member who is the -claimant's tax unit dependent is counted through dependent_gross_income and -dependent_taxable_social_security; a member who files their own return is -counted through federal AGI, the loss add-back and taxable_social_security. +claimant's tax unit dependent is counted through their income and permitted +adjustments; a member who files their own return is counted through federal +AGI, the loss add-back and taxable_social_security. For a claimant aged 62 or older (taxable pension, Social Security, property -tax, rent) and one other adult in the household (taxable interest, a -long-term capital gain or loss, Social Security): +tax, rent) and one other member in the household (taxable interest, a +long-term capital gain or loss, Social Security, self-employment profit and +permitted non-loss adjustments): 1. The household's gross household income is the claimant's pension and - Social Security plus the other member's interest, positive capital gain - and Social Security, whichever path counts the other member. + Social Security plus the other member's interest, positive capital gain, + self-employment profit and Social Security, less permitted non-loss + adjustments, whichever path counts the other member. Capital losses + cannot reduce this amount. 2. So the claimant's credit is the same in both arrangements, and the other member's own tax unit, with no one aged 62 or older, claims nothing. 3. The credit is between zero and the cap, and zero when gross household @@ -27,10 +30,15 @@ 4. More interest for the other member never raises the credit: gross household income rises, so net household income (line 22) cannot fall and the credit multiplier (line 29) cannot rise. +5. Early-withdrawal penalties and self-employed health/pension expenses + reduce household income by their deductible amounts, even when those + adjustments make the member's contribution negative. -No one has earned income, so no refundable credit enters line 8. The model -computes in single precision, so comparisons allow one cent or eight float32 -spacings at the household's total income, whichever is larger. +Earned-income cases keep the other member under 25 with no dependents on +their own return, so neither arrangement receives refundable earned-income +or child tax credits. The claimant has no earned income. The model computes +in single precision, so comparisons allow one cent or eight float32 spacings +at the household's total income, whichever is larger. """ import numpy as np @@ -42,12 +50,14 @@ CAP = 1_150 INCOME_LIMIT = 45_000 TOLERANCE = 0.01 +ARRANGEMENTS = "ABCD" def build_situation(cases): - """Three Montana households per case: (A) the other member is the + """Four Montana households per case: (A) the other member is the claimant's dependent, (B) the other member files their own return and - (C) as A with the other member's extra interest added.""" + (C) as A with the other member's extra interest added, and (D) as A + without early-withdrawal, self-employed health or pension expenses.""" people, tax_units, marital_units, spm_units, families, households = ( {}, {}, @@ -57,7 +67,7 @@ def build_situation(cases): {}, ) for i, c in enumerate(cases): - for arrangement in "ABC": + for arrangement in ARRANGEMENTS: key = f"{arrangement}{i}" claimant, member = f"claimant_{key}", f"member_{key}" interest = c["interest"] + ( @@ -75,6 +85,16 @@ def build_situation(cases): "taxable_interest_income": {YEAR: interest}, "long_term_capital_gains": {YEAR: c["capital_gain"]}, "social_security_survivors": {YEAR: c["member_ss"]}, + "self_employment_income": {YEAR: c["self_employment_income"]}, + "early_withdrawal_penalty": { + YEAR: 0 if arrangement == "D" else c["early_withdrawal_penalty"] + }, + "self_employed_health_insurance_premiums": { + YEAR: 0 if arrangement == "D" else c["health_premiums"] + }, + "self_employed_pension_contributions": { + YEAR: 0 if arrangement == "D" else c["pension_contributions"] + }, } if arrangement == "B": tax_units[f"tax_unit_{key}"] = {"members": [claimant]} @@ -124,22 +144,49 @@ def calculate(cases): simulation.calculate("mt_elderly_homeowner_or_renter_credit", YEAR) ) dependent = np.asarray(simulation.calculate("is_tax_unit_dependent", YEAR)) - # Households and people are in situation order: A0, B0, C0, A1, ...; + other_refundable_credits = np.asarray( + simulation.calculate( + "mt_refundable_credits_before_renter_credit", YEAR, map_to="household" + ) + ) + # Households and people are in situation order: A0, B0, C0, D0, A1, ...; # the member follows the claimant. - n = len(cases) + n, arrangements = len(cases), len(ARRANGEMENTS) return { - "gross_household_income": gross_household_income.reshape(n, 3).T, - "credit": credit.reshape(n, 3).T, - "member_credit": person_credit[1::2].reshape(n, 3).T, - "member_is_dependent": dependent[1::2].reshape(n, 3).T, + "gross_household_income": gross_household_income.reshape(n, arrangements).T, + "credit": credit.reshape(n, arrangements).T, + "member_credit": person_credit[1::2].reshape(n, arrangements).T, + "member_is_dependent": dependent[1::2].reshape(n, arrangements).T, + "other_refundable_credits": other_refundable_credits.reshape(n, arrangements).T, } -def expected_gross_household_income(c, extra_interest=0): - """2024 Schedule 2EC line 18: lines 5 and 6 for the claimant, and line - 17 for the other member's interest, gains and Social Security.""" +def elective_adjustments(c): + # Generated health premiums and pension contributions stay below the + # member's eligible self-employment earnings, so all are deductible. + return ( + c["early_withdrawal_penalty"] + + c["health_premiums"] + + c["pension_contributions"] + ) + + +def expected_gross_household_income(c): + """All members' federal AGI without losses, plus nontaxable income: + MCA 15-30-2337(9); 2023 Form 2 instructions PDF pp. 51-52. + https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 + """ claimant = c["pension"] + c["claimant_ss"] - member = c["interest"] + extra_interest + max(0, c["capital_gain"]) + member = c["interest"] + max(0, c["capital_gain"]) + c["self_employment_income"] + # Schedule SE lines 4a, 4c, 10-13: taxable earnings are 92.35% of + # profit; no SE tax below $400. Generated profits stay below the + # Social Security wage base, and no one has wages. + # https://www.irs.gov/pub/irs-prior/f1040sse--2024.pdf#page=1 + taxable_earnings = c["self_employment_income"] * 0.9235 + half_se_tax = ( + taxable_earnings * (0.124 + 0.029) * 0.5 if taxable_earnings >= 400 else 0 + ) + member -= half_se_tax + elective_adjustments(c) return claimant + member + c["member_ss"] @@ -149,33 +196,43 @@ def tolerance(amount): def assert_properties(cases): result = calculate(cases) - a, b, c_ = 0, 1, 2 - # The other member is a dependent in A and C and their own filer in B. + a, b, c_, d = 0, 1, 2, 3 + # Equivalence assumes these income definitions contain no refundable + # credits whose eligibility changes when the member files separately. + assert np.all(result["other_refundable_credits"] == 0), cases + # The other member is a dependent in A, C and D and their own filer in B. assert result["member_is_dependent"].tolist() == [ [True] * len(cases), [False] * len(cases), [True] * len(cases), + [True] * len(cases), ] for i, c in enumerate(cases): expected = expected_gross_household_income(c) - tol = tolerance(expected + c["extra_interest"]) + expenses = elective_adjustments(c) + tol = tolerance(expected + c["extra_interest"] + expenses) ghi = result["gross_household_income"] credit = result["credit"] # 1. Both paths give the Schedule 2EC total. assert ghi[a, i] == pytest.approx(expected, abs=tol), c assert ghi[b, i] == pytest.approx(expected, abs=tol), c assert ghi[c_, i] == pytest.approx(expected + c["extra_interest"], abs=tol), c + assert ghi[d, i] == pytest.approx(expected + expenses, abs=tol), c # 2. The claimant's credit does not depend on the arrangement, and # the other member claims nothing. assert credit[a, i] == pytest.approx(credit[b, i], abs=tol), c assert np.all(result["member_credit"][:, i] == 0), c - for arrangement in (a, b, c_): + for arrangement in (a, b, c_, d): # 3. Bounds and the income limit. assert -tol <= credit[arrangement, i] <= CAP + tol, c if ghi[arrangement, i] >= INCOME_LIMIT + tol: assert credit[arrangement, i] == 0, c # 4. More income never raises the credit. assert credit[c_, i] <= credit[a, i] + tol, c + # 5. Non-loss adjustments lower income by the full deductible + # amount, including when the member has negative AGI without losses. + assert ghi[d, i] - ghi[a, i] == pytest.approx(expenses, abs=tol), c + assert credit[d, i] <= credit[a, i] + tol, c def test_worked_example(): @@ -194,13 +251,19 @@ def test_worked_example(): "interest": 10_000, "capital_gain": 0, "member_ss": 0, + "self_employment_income": 0, + "early_withdrawal_penalty": 0, + "health_premiums": 0, + "pension_contributions": 0, "extra_interest": 5_000, } result = calculate([case]) assert result["gross_household_income"][:, 0] == pytest.approx( - [30_000, 30_000, 35_000], abs=TOLERANCE + [30_000, 30_000, 35_000, 30_000], abs=TOLERANCE + ) + assert result["credit"][:, 0] == pytest.approx( + [1_130, 1_130, 352, 1_130], abs=TOLERANCE ) - assert result["credit"][:, 0] == pytest.approx([1_130, 1_130, 352], abs=TOLERANCE) assert_properties([case]) @@ -216,26 +279,102 @@ def test_worked_example(): SLOW = [hypothesis.HealthCheck.too_slow] def amount(high): + if high == 0: + return st.just(0) return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) @st.composite def case(draw): + member_age = draw(st.integers(min_value=15, max_value=61)) + self_employment_income = draw(amount(20_000)) if member_age <= 24 else 0 return { "claimant_age": draw(st.integers(min_value=62, max_value=90)), "pension": draw(amount(40_000)), "claimant_ss": draw(amount(30_000)), "property_tax": draw(st.integers(min_value=0, max_value=4_000)), "rent": draw(amount(12_000)), - "member_age": draw(st.integers(min_value=15, max_value=61)), + "member_age": member_age, "interest": draw(amount(40_000)), "capital_gain": draw( st.one_of(st.just(0), st.integers(min_value=-20_000, max_value=40_000)) ), "member_ss": draw(amount(20_000)), + "self_employment_income": self_employment_income, + "early_withdrawal_penalty": draw(amount(40_000)), + # Keep these expenses within earned income and conservative + # plan limits, so both filer arrangements permit the deduction. + "health_premiums": draw(amount(self_employment_income // 4)), + "pension_contributions": draw(amount(self_employment_income // 10)), "extra_interest": draw(st.integers(min_value=1, max_value=20_000)), } - @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=15, derandomize=True, deadline=None, suppress_health_check=SLOW + ) + @hypothesis.example( + [ + # P2: 10,000 * .9235 * .153 / 2 = 706.4775; + # gross household income = 20,000 + 10,000 - 706.4775. + { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 0, + "rent": 12_000, + "member_age": 18, + "interest": 0, + "capital_gain": 0, + "member_ss": 0, + "self_employment_income": 10_000, + "early_withdrawal_penalty": 0, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": 1_000, + }, + # The capital loss is excluded. 3,000 * .9235 * .153 / 2 + # = 211.94325; the other adjustments total 5,700, leaving + # member income 3,500 - 211.94325 - 5,700 = -2,411.94325. + { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 0, + "rent": 12_000, + "member_age": 24, + "interest": 500, + "capital_gain": -3_000, + "member_ss": 0, + "self_employment_income": 3_000, + "early_withdrawal_penalty": 5_000, + "health_premiums": 500, + "pension_contributions": 200, + "extra_interest": 1_000, + }, + ] + ) + @hypothesis.example( + [ + # Taxable SE earnings: 433 * .9235 = 399.8755 (no tax), + # 434 * .9235 = 400.799 (half-tax deduction 30.6611235). + { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 0, + "rent": 12_000, + "member_age": 18, + "interest": 0, + "capital_gain": 0, + "member_ss": 0, + "self_employment_income": profit, + "early_withdrawal_penalty": 0, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": 1_000, + } + for profit in (433, 434) + ] + ) @hypothesis.given(st.lists(case(), min_size=1, max_size=8)) def test_properties(cases): assert_properties(cases) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py index 735e9a7bafc..21a84de4bb4 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.irs_gross_income.social_security.dependent_taxable_ss_magi import ( + PERSON_ABOVE_THE_LINE_DEDUCTIONS, +) class mt_elderly_homeowner_or_renter_credit_gross_household_income(Variable): @@ -30,11 +33,20 @@ def formula(person, period, parameters): # Gross household income counts every member of the household # (§ 15-30-2337(4)). A tax unit dependent's income is on their own # return, not in adjusted_gross_income_person (irs_gross_income - # leaves dependents out), so count their gross income directly: - # gains but not losses, and the taxable part of their Social - # Security. + # leaves dependents out). Start with their gross income without + # losses, then retain the permitted non-loss AGI adjustments + # attributable to that person (2023 instructions, lines 1 and 9). + # Do not floor an individual's AGI contribution at zero: an + # adjustment such as an early-withdrawal penalty can exceed income. is_dependent = person("is_tax_unit_dependent", period) - dependent_income = is_dependent * person("dependent_gross_income", period) + deductions = [ + PERSON_ABOVE_THE_LINE_DEDUCTIONS[deduction] + for deduction in parameters(period).gov.irs.ald.deductions + if deduction in PERSON_ABOVE_THE_LINE_DEDUCTIONS + ] + dependent_income = is_dependent * ( + person("dependent_gross_income", period) - add(person, period, deductions) + ) # The income above counts only the taxable portion of Social # Security: a filer's in federal AGI, a dependent's in their own # gross income. Add the untaxed portion so all SS is counted per From 5b965e139afb99751f26d4f13aef50c507fc712a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 14:08:14 -0400 Subject: [PATCH 11/13] Apply Montana elderly credit multiplier to negative household income --- ...x-mt-ehrc-household-member-income.fixed.md | 1 + ...est_mt_elderly_credit_household_members.py | 41 +++++++++++++++++++ ...owner_or_renter_credit_pre_one_claimant.py | 7 +++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md index 6704770f799..c7e8d0d90f9 100644 --- a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md +++ b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md @@ -1 +1,2 @@ Count household members' income in the Montana elderly homeowner/renter credit, preserving dependents' permitted person-level federal AGI adjustments and excluding losses, as well as including the income of members who file their own returns. +Apply the full credit multiplier when permitted adjustments make household income negative. diff --git a/policyengine_us/tests/test_mt_elderly_credit_household_members.py b/policyengine_us/tests/test_mt_elderly_credit_household_members.py index b123404bdcc..65be11469ee 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_household_members.py +++ b/policyengine_us/tests/test_mt_elderly_credit_household_members.py @@ -267,6 +267,46 @@ def test_worked_example(): assert_properties([case]) +def negative_household_cases(): + return [ + { + "claimant_age": claimant_age, + "pension": 0, + "claimant_ss": 347, + "property_tax": 319, + "rent": 0, + "member_age": 43, + "interest": interest, + "capital_gain": 0, + "member_ss": member_ss, + "self_employment_income": 0, + "early_withdrawal_penalty": penalty, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": extra_interest, + } + for claimant_age, interest, member_ss, penalty, extra_interest in ( + (62, 4_420, 894, 5_986, 377), + (86, 0, 0, 894, 1), + ) + ] + + +def test_negative_household_income_keeps_full_multiplier(): + # 347 + 4,420 + 894 - 5,986 = -325; adding 377 gives 52; + # removing the penalty gives 5,661. In the second case, 347 - 894 + # = -547, or -546 with extra interest, or 347 without the penalty. + # Every total is below 12,600, so net household contribution is zero. + # Line 29 gives 100% below 35,000, so the 319 property tax is paid: + # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=49 + result = calculate(negative_household_cases()) + assert result["gross_household_income"] == pytest.approx( + np.array([[-325, -547], [-325, -547], [52, -546], [5_661, 347]]), + abs=TOLERANCE, + ) + assert result["credit"] == pytest.approx(np.full((4, 2), 319), abs=TOLERANCE) + + try: import hypothesis import hypothesis.strategies as st @@ -311,6 +351,7 @@ def case(draw): @hypothesis.settings( max_examples=15, derandomize=True, deadline=None, suppress_health_check=SLOW ) + @hypothesis.example(negative_household_cases()) @hypothesis.example( [ # P2: 10,000 * .9235 * .153 / 2 = 706.4775; diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py index 12486d784eb..aa8cd4ba1ff 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py @@ -13,6 +13,8 @@ class mt_elderly_homeowner_or_renter_credit_pre_one_claimant(Variable): "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0400/0150-0300-0230-0400.html", # 2024 Form 2, Schedule 2EC "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + # 2024 instructions, line 29 Credit Multiplier Table + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=49", ) def formula(person, period, parameters): @@ -46,5 +48,8 @@ def formula(person, period, parameters): ) uncapped_credit = uncapped_credit_unit * head capped_credit = min_(uncapped_credit, p.cap) - multiplier = p.multiplier.calc(gross_household_income) + # The table gives 100% for all income less than $35,000, including + # negative AGI after permitted adjustments. Preserve signed income + # and floor only the lookup input at its first threshold. + multiplier = p.multiplier.calc(max_(0, gross_household_income)) return capped_credit * multiplier From 261e65b1ba3eec8bd4e356c50a98f4734b4f72e3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 14:27:09 -0400 Subject: [PATCH 12/13] Supply explicit filer roles in Montana income property tests --- ...est_mt_elderly_credit_household_members.py | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/test_mt_elderly_credit_household_members.py b/policyengine_us/tests/test_mt_elderly_credit_household_members.py index 65be11469ee..c05da31f775 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_household_members.py +++ b/policyengine_us/tests/test_mt_elderly_credit_household_members.py @@ -101,11 +101,13 @@ def build_situation(cases): tax_units[f"tax_unit_{key}_member"] = {"members": [member]} else: tax_units[f"tax_unit_{key}"] = {"members": [claimant, member]} - # Set for everyone: an input for some people gives the others - # its default (false), not its formula. Without it the model - # takes an adult in the claimant's tax unit to be the spouse. - people[claimant]["is_tax_unit_dependent"] = {YEAR: False} - people[member]["is_tax_unit_dependent"] = {YEAR: arrangement != "B"} + # Complete roles distinguish adult dependents from spouses + # and keep a minor's separate return headed by that minor; + # age-based inference only considers adults as filers. + people[claimant]["tax_unit_role_input"] = {YEAR: "HEAD"} + people[member]["tax_unit_role_input"] = { + YEAR: "HEAD" if arrangement == "B" else "DEPENDENT" + } marital_units[f"marital_unit_{key}"] = {"members": [claimant]} marital_units[f"marital_unit_{key}_member"] = {"members": [member]} spm_units[f"spm_unit_{key}"] = {"members": [claimant, member]} @@ -352,6 +354,28 @@ def case(draw): max_examples=15, derandomize=True, deadline=None, suppress_health_check=SLOW ) @hypothesis.example(negative_household_cases()) + @hypothesis.example( + [ + # A minor can head their own return. With no gross income, + # a deductible penalty of 1 gives AGI -1 in both arrangements. + { + "claimant_age": 62, + "pension": 0, + "claimant_ss": 0, + "property_tax": 0, + "rent": 0, + "member_age": 15, + "interest": 0, + "capital_gain": 0, + "member_ss": 0, + "self_employment_income": 0, + "early_withdrawal_penalty": 1, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": 1, + } + ] + ) @hypothesis.example( [ # P2: 10,000 * .9235 * .153 / 2 = 706.4775; From bfcf454cc5a31acfd77eba1c1710974054219b68 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:16:26 -0400 Subject: [PATCH 13/13] Drop changelog entries already released from #9915 The separate-filing allocation fragment duplicated the line released in 2.38.17, and the household-member income fragment repeated the dependent and separate-filer income corrections released there too. Keep one line per change this PR makes: the one-claimant limit and the full multiplier for negative household income. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-mt-ehrc-household-member-income.fixed.md | 3 +-- changelog.d/fix-mt-separate-filing-allocations.fixed.md | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 changelog.d/fix-mt-separate-filing-allocations.fixed.md diff --git a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md index c7e8d0d90f9..1406d26b72a 100644 --- a/changelog.d/fix-mt-ehrc-household-member-income.fixed.md +++ b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md @@ -1,2 +1 @@ -Count household members' income in the Montana elderly homeowner/renter credit, preserving dependents' permitted person-level federal AGI adjustments and excluding losses, as well as including the income of members who file their own returns. -Apply the full credit multiplier when permitted adjustments make household income negative. +Apply the full Montana elderly homeowner/renter credit multiplier when permitted adjustments make household income negative. diff --git a/changelog.d/fix-mt-separate-filing-allocations.fixed.md b/changelog.d/fix-mt-separate-filing-allocations.fixed.md deleted file mode 100644 index c7d3be16f20..00000000000 --- a/changelog.d/fix-mt-separate-filing-allocations.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Montana's federal income tax deduction for spouses filing separately on the same form now attributes federal tax to the spouse who earned the income and caps each spouse at 5,000; the child and dependent care expense deduction phases out on the couple's combined Montana AGI and is halved only for spouses filing separately on the same form; and the elderly homeowner/renter credit counts income without regard to losses.