From 52ffe10226cd26b3ff24e48ce7f9bc555924dd07 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:08:30 -0400 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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 b2096889598043e97afbc33f3843f28defdfae47 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:46:30 -0400 Subject: [PATCH 13/14] Count property tax billed in another's name as rent in the Montana elderly credit ARM 42.4.302(2)(b) lets an eligible claimant use property taxes billed "as rent if the property occupied by the claimant is in a name other than the claimant"; the Form 2 instructions (line 23, since tax year 2011) say such taxes "can qualify as rent only". The model counted only each tax unit's own property tax and rent, so an elderly person living in a home billed to another household member got $0. - New mt_elderly_homeowner_or_renter_credit_property_tax_billed (Schedule 2EC line 23): the property tax billed to the tax unit's head and spouse. A dependent's bill is in a name other than the claimant's. - New mt_elderly_homeowner_or_renter_credit_gross_rent (line 24): rent paid, or, for a tax unit billed no property tax, the larger of that and the household's property tax. Line 25 then takes 15% of it. - New parameter property_tax_in_another_name_as_rent: false for 2009-2010, whose instructions applied "rent only" just to trusts; true from 2011. - The pre-limit credit reads both; the one-claimant selection is unchanged. Co-Authored-By: Claude Opus 5.5 --- ...x-mt-ehrc-property-in-others-name.fixed.md | 1 + .../property_tax_in_another_name_as_rent.yaml | 24 ++ ...mt_elderly_homeowner_or_renter_credit.yaml | 168 ++++++++++ ...homeowner_or_renter_credit_gross_rent.yaml | 286 ++++++++++++++++++ ...ner_or_renter_credit_pre_one_claimant.yaml | 70 +++++ ..._or_renter_credit_property_tax_billed.yaml | 104 +++++++ .../test_mt_elderly_credit_one_claimant.py | 121 +++++++- ...y_homeowner_or_renter_credit_gross_rent.py | 59 ++++ ...owner_or_renter_credit_pre_one_claimant.py | 13 +- ...er_or_renter_credit_property_tax_billed.py | 32 ++ ...wner_or_renter_credit_selected_claimant.py | 32 +- 11 files changed, 889 insertions(+), 21 deletions(-) create mode 100644 changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md create mode 100644 policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.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_gross_rent.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_property_tax_billed.yaml create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py diff --git a/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md b/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md new file mode 100644 index 00000000000..64fd3cddd82 --- /dev/null +++ b/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md @@ -0,0 +1 @@ +Count property tax billed for a Montana elderly homeowner/renter credit claimant's home in another household member's name as rent (ARM 42.4.302(2)(b)), and count only the claimant's own (head and spouse) property tax as property tax billed. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml new file mode 100644 index 00000000000..20cc8ea2f81 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml @@ -0,0 +1,24 @@ +description: Montana counts the property tax billed for a home in another person's name as the claimant's rent under the elderly homeowner or renter credit, if this is true. +values: + # The 2009 and 2010 instructions let only property tax billed to an + # irrevocable or family trust "qualify as rent only"; the 2011 + # instructions first extend this to any property "in a name other than + # your own", the wording of ARM 42.4.302(2)(b). + 2009-01-01: false + 2011-01-01: true + +metadata: + unit: bool + period: year + label: Montana elderly homeowner or renter credit property tax in another name as rent + reference: + - title: ARM 42.4.302(2)(b), Computation of Elderly Homeowner/Renter Tax Credit + href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302 + - title: 2010 Montana Form 2 Instructions, Form 2EC line 6 (property tax billed to a trust qualifies as rent only) + href: https://taxsim.nber.org/historical_state_tax_forms/MT/2010/2010-2-Booklet.pdf#page=42 + - title: 2011 Montana Form 2 Instructions, Form 2EC line 6 (property in a name other than your own) + href: https://taxsim.nber.org/historical_state_tax_forms/MT/2011/Form_2_Booklet_2011.pdf-1.pdf#page=54 + - title: 2024 Montana Form 2 Instructions, Schedule 2EC line 23 + 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 + - title: 2025 Montana Form 2 Instructions, Schedule 2EC line 23 + href: 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 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 77815f66850..a11062e7c10 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 @@ -842,3 +842,171 @@ mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [50, 410] mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] mt_elderly_homeowner_or_renter_credit: [0, 410] + +- name: 2024 Schedule 2EC, a parent living in an adult child's home claims the child's property tax as rent + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + person2: + age: 45 + taxable_interest_income: 6_000 + rent: 0 + real_estate_taxes: 4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The home is in the child's name. "If the property occupied by you is + # in a name other than your own, the property taxes billed for that + # property can qualify as rent only" (2025 instructions, line 23; ARM + # 42.4.302(2)(b)). + # Line 18 = 14_000 + 6_000 = 20_000; line 20 = 20_000 - 12_600 = 7_400; + # line 21 = 0.035; line 22 = 259. + # Parent: line 23 = 0; line 24 = 4_000 (the child's bill, as rent); + # line 25 = 600; line 26 = 600; line 27 = 600 - 259 = 341; line 28 = + # 341; line 29 = 100% (under 35_000); line 30 = 341. Before this rule + # the parent's line 24 was 0 and the credit 0. + # The child is 45, so not a claimant (§ 15-30-2338(1)(a)). + mt_elderly_homeowner_or_renter_credit_net_household_income: [259, 0] + mt_elderly_homeowner_or_renter_credit_gross_rent: [4_000, 0] + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [341, 0] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit: [341, 0] + mt_refundable_credits: [341, 0] + +- name: 2024 Schedule 2EC, a parent who pays the child less rent than the child's bill counts the bill + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 2_400 + person2: + age: 45 + taxable_interest_income: 6_000 + rent: 0 + real_estate_taxes: 4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 22 = 259 as above. Line 24 = the larger of the rent paid + # (2_400) and the child's bill (4_000) = 4_000; line 25 = 600; line 27 = + # 341. Counting rent paid alone would give 360 - 259 = 101. + mt_elderly_homeowner_or_renter_credit_gross_rent: [4_000, 0] + mt_elderly_homeowner_or_renter_credit: [341, 0] + +- name: 2024 Schedule 2EC, a parent who pays the child more rent than the child's bill counts the rent, not both + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 6_000 + person2: + age: 45 + taxable_interest_income: 6_000 + rent: 0 + real_estate_taxes: 4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 22 = 259. Line 24 = max(6_000, 4_000) = 6_000; line 25 = 900; + # line 27 = 900 - 259 = 641. Adding the bill to the rent (10_000) would + # give 1_500 - 259 = 1_241, capped at 1_150. + mt_elderly_homeowner_or_renter_credit_gross_rent: [6_000, 0] + mt_elderly_homeowner_or_renter_credit: [641, 0] + +- name: 2024 Schedule 2EC, two elderly siblings and one owns; the owner's larger credit is the one claim + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 0 + person2: + age: 70 + taxable_pension_income: 10_000 + rent: 0 + real_estate_taxes: 2_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 20_000; line 20 = 7_400; line 21 = 0.035; line 22 = 259. + # person1 (home in person2's name): line 24 = 2_000; line 25 = 300; + # line 27 = 300 - 259 = 41. + # person2: line 23 = 2_000; line 27 = 2_000 - 259 = 1_741; line 28 = + # 1_150 cap. + # One claimant per household (§ 15-30-2341(1)): person2 claims 1_150. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [41, 1_150] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + mt_elderly_homeowner_or_renter_credit: [0, 1_150] + +- name: 2024 Schedule 2EC, a home in a dependent's name counts as the claimant's rent, not property tax billed + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + is_tax_unit_dependent: false + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 4_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 14_000; line 20 = 1_400; line 21 = 0 (under 2_000); line + # 22 = 0. The home is in the dependent's name, not the claimant's: line + # 23 = 0; line 24 = 4_000; line 25 = 600; line 27 = 600; line 30 = 600. + # Counting the dependent's bill on line 23 would give 4_000, capped at + # 1_150. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + mt_elderly_homeowner_or_renter_credit_gross_rent: 4_000 + mt_elderly_homeowner_or_renter_credit: [600, 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_rent.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml new file mode 100644 index 00000000000..a68c4aa6ecd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml @@ -0,0 +1,286 @@ +- name: Case 1, rent paid, with no property tax billed in the household. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 6_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Schedule 2EC line 24: "the rent that you paid". + mt_elderly_homeowner_or_renter_credit_gross_rent: 6_000 + +- name: Case 2, a parent living in an adult child's home counts the child's property tax as rent. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The parent is billed nothing: the home is "in a name other than the + # claimant", so the child's 3_000 bill goes on the parent's line 24 as + # rent (ARM 42.4.302(2)(b)). The child is billed their own property tax + # (line 23) and pays no rent. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [0, 3_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 3, a claimant billed their own property tax does not count another member's bill. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + real_estate_taxes: 500 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The home is in person1's name too, so ARM 42.4.302(2)(b) does not + # apply: line 23 = 500 and line 24 = 0. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [500, 3_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + +- name: Case 4, rent paid below the other member's bill; the bill counts. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 2_400 + person2: + age: 50 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 24 = max(2_400, 3_000): the bill is an alternative measure of the + # rent for the same home, not an addition to it. + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 5, rent paid above the other member's bill; the rent counts. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 4_800 + person2: + age: 50 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 24 = max(4_800, 3_000), not 4_800 + 3_000. + mt_elderly_homeowner_or_renter_credit_gross_rent: [4_800, 0] + +- name: Case 6, a home in a dependent's name counts as rent. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 0 + is_tax_unit_dependent: false + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The dependent is not the claimant, so their bill "can qualify as rent + # only": line 23 = 0 and line 24 = 2_000. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + mt_elderly_homeowner_or_renter_credit_gross_rent: 2_000 + +- name: Case 7, a home co-owned by two other members counts the whole bill. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 1_000 + person3: + age: 48 + rent: 0 + real_estate_taxes: 1_500 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # "The property taxes billed for that property": 1_000 + 1_500. + mt_elderly_homeowner_or_renter_credit_gross_rent: [2_500, 0, 0] + +- name: Case 8, a joint couple billed through the spouse does not count another member's bill. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + person2: + age: 66 + rent: 0 + real_estate_taxes: 1_800 + person3: + age: 40 + rent: 0 + real_estate_taxes: 2_000 + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # A joint return is one claimant, billed 1_800 through the spouse. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [1_800, 2_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + +- name: Case 9, in 2010 only property billed to a trust counted as rent. + period: 2010 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + 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_gross_rent: [0, 0] + +- name: Case 10, from 2011 property in another person's name counts as rent. + period: 2011 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + 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_gross_rent: [3_000, 0] + +- name: Case 11, outside Montana. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 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_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 index 533e31da039..fcdb8c37865 100644 --- 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 @@ -35,3 +35,73 @@ 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] + +- name: Case 2, a tax unit billed no property tax counts another member's bill as rent. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + person2: + age: 72 + taxable_pension_income: 4_000 + rent: 0 + real_estate_taxes: 900 + person3: + age: 45 + taxable_interest_income: 2_000 + rent: 0 + 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 = 14_000 + 4_000 + 2_000 = 20_000; line 20 + # = 7_400; line 21 = 0.035; line 22 = 259. + # person1 (home in person2's name): line 24 = 900; line 25 = 135; line + # 27 = max(135 - 259, 0) = 0. + # person2: line 23 = 900; line 27 = 900 - 259 = 641. + # person3 is billed nothing either, so their line 24 is also 900, but + # they are under 62 and not a claimant. + mt_elderly_homeowner_or_renter_credit_gross_rent: [900, 0, 900] + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [0, 641, 0] + +- name: Case 3, the home's bill as rent makes the only eligible tax unit's credit positive. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 64 + taxable_pension_income: 9_000 + rent: 0 + person2: + age: 60 + taxable_pension_income: 3_000 + rent: 0 + real_estate_taxes: 2_500 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 12_000; line 20 = max(12_000 - 12_600, 0) = 0; line 22 = 0. + # person1: line 24 = 2_500; line 25 = 375; line 27 = 375. + # person2 is 60, so not a claimant, though the home is in their name. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [375, 0] + mt_elderly_homeowner_or_renter_credit: [375, 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_property_tax_billed.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml new file mode 100644 index 00000000000..755928a772a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml @@ -0,0 +1,104 @@ +- name: Case 1, a single owner's property tax billed. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Schedule 2EC line 23. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_000 + +- name: Case 2, a joint couple's bills are one claimant's. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + real_estate_taxes: 1_200 + person2: + age: 66 + real_estate_taxes: 800 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # A joint return makes one claim: line 23 = 1_200 + 800. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_000 + +- name: Case 3, a home in a dependent's name is not billed to the claimant. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + is_tax_unit_dependent: false + person2: + age: 50 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The property is "in a name other than the claimant", so its tax "can + # qualify as rent only" (ARM 42.4.302(2)(b)): line 23 = 0. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + +- name: Case 4, each tax unit counts only the bill in its own name. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + person2: + age: 45 + real_estate_taxes: 3_000 + 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_property_tax_billed: [0, 3_000] + +- name: Case 5, outside Montana. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 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 d1dc874a300..93430829674 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py +++ b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py @@ -7,9 +7,12 @@ 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. +22) and multiplier (line 29); the credits differ only by each tax unit's +property tax billed (line 23) and rent (line 24). A tax unit billed no +property tax lives in a home in another member's name, whose property tax +"can qualify as rent only" (ARM 42.4.302(2)(b)): its line 24 is the larger +of the rent it paid and the property tax billed to the household's other +tax units. 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 @@ -26,6 +29,15 @@ 5. Reversing the order of the tax units in a household does not change the household's credit. +A second test raises the property tax billed to one tax unit and checks: + +6. No other tax unit's credit before the limit falls, and neither does that + tax unit's own credit if it was already billed property tax. (A tax unit + newly billed property tax can lose credit: it stops counting the other + members' bill as rent and counts only its own bill.) +7. The household's credit does not fall unless the raised tax unit was billed + nothing before. + The model computes in single precision, so comparisons allow one cent or eight float32 spacings at the household's total income, whichever is larger. """ @@ -110,11 +122,18 @@ def reference(units): net = reduced * float(P.net_household_income.reduction_rate.calc(reduced)) # Line 29. multiplier = float(P.multiplier.calc(gross)) + household_property_tax = sum(unit["property_tax"] for unit in units) credits = [] for unit in units: eligible = max(adult["age"] for adult in unit["adults"]) >= P.age_threshold + # Line 24: a tax unit billed no property tax may use the property tax + # billed to the household's other tax units as rent, in place of the + # rent it paid if that is larger (ARM 42.4.302(2)(b)). + rent = unit["rent"] + if unit["property_tax"] == 0 and P.property_tax_in_another_name_as_rent: + rent = max(rent, household_property_tax) # Lines 23-28. - housing = unit["property_tax"] + unit["rent"] * P.rent_equivalent_tax_rate + housing = unit["property_tax"] + rent * P.rent_equivalent_tax_rate capped = min(max(housing - net, 0), P.cap) credits.append(eligible * capped * multiplier) return gross, credits @@ -212,6 +231,71 @@ def test_worked_example(): assert_properties(households) +PARENT_IN_CHILDS_HOME = [ + {"adults": [adult(80, pension=14_000)], "property_tax": 0, "rent": 0}, + {"adults": [adult(45, interest=6_000)], "property_tax": 4_000, "rent": 0}, +] + + +def test_property_tax_in_another_name_worked_example(): + """2024 Schedule 2EC: a parent, 80, with a 14,000 pension lives in the + home of an adult child, 45, who has 6,000 of interest and is billed 4,000 + of property tax; each files their own return. Line 18 = 20,000; line 22 + = 7,400 x 0.035 = 259. The home is in the child's name, so its property + tax "can qualify as rent only" on the parent's schedule (ARM + 42.4.302(2)(b)): line 24 = 4,000, line 25 = 600, line 27 = 341. The child + is under 62 and cannot claim.""" + households = [PARENT_IN_CHILDS_HOME] + result = calculate(households) + assert result["pre_one_claimant"] == pytest.approx([341, 0, 0, 341], abs=TOLERANCE) + assert result["selected"].tolist() == [True, False, False, True] + assert result["credit"] == pytest.approx([341, 0, 0, 341], abs=TOLERANCE) + assert_properties(households) + + +def raise_property_tax(units, index, increase): + """The household with `increase` more property tax billed to the head of + tax unit `index`.""" + raised = list(units) + raised[index] = { + **units[index], + "property_tax": units[index]["property_tax"] + increase, + } + return raised + + +def assert_raising_a_bill(units, index, increase): + """Invariants 6 and 7.""" + result = calculate([units, raise_property_tax(units, index, increase)]) + n = len(units) + tol = tolerance(reference(units)[0]) + # build_situation lays out each household forward, then reversed; compare + # the forward copies. + before = result["pre_one_claimant"][:n] + after = result["pre_one_claimant"][2 * n : 3 * n] + already_billed = units[index]["property_tax"] > 0 + for j in range(n): + if j != index or already_billed: + assert after[j] >= before[j] - tol, (units, index, increase) + if already_billed: + household_before = result["credit"][:n].sum() + household_after = result["credit"][2 * n : 3 * n].sum() + assert household_after >= household_before - tol, (units, index, increase) + + +def test_newly_billed_tax_unit_can_lose_credit(): + """The exception in invariant 6, which follows the form's reading of line + 23 ("the property tax you were billed"). Billing the parent 100 of + property tax makes the home theirs too: line 23 = 100 and line 24 = 0, + so line 27 = max(100 - 259, 0) = 0 instead of 341. Line 22 is unchanged + at 259. The child's bill still counts for no one but the child.""" + result = calculate( + [PARENT_IN_CHILDS_HOME, raise_property_tax(PARENT_IN_CHILDS_HOME, 0, 100)] + ) + assert result["pre_one_claimant"][[0, 4]] == pytest.approx([341, 0], abs=TOLERANCE) + assert_raising_a_bill(PARENT_IN_CHILDS_HOME, 1, 500) + + try: import hypothesis import hypothesis.strategies as st @@ -295,5 +379,34 @@ def tax_unit(draw): ] ] ) + # A parent in an adult child's home: the child's bill is the parent's + # rent (line 24 = 4,000; line 27 = 600 - 259 = 341). + @hypothesis.example([PARENT_IN_CHILDS_HOME]) + # The same parent paying the child 2,400 of rent: line 24 is the larger + # bill, 4,000, not 2,400 + 4,000. + @hypothesis.example( + [ + [ + {**PARENT_IN_CHILDS_HOME[0], "rent": 2_400}, + PARENT_IN_CHILDS_HOME[1], + ] + ] + ) def test_properties(households): assert_properties(households) + + @st.composite + def raised_bill(draw): + units = draw(st.lists(tax_unit(), min_size=1, max_size=3)) + index = draw(st.integers(min_value=0, max_value=len(units) - 1)) + increase = draw(st.integers(min_value=1, max_value=4_000)) + return units, index, increase + + @hypothesis.settings( + max_examples=10, deadline=None, derandomize=True, suppress_health_check=SLOW + ) + @hypothesis.given(raised_bill()) + # Raising the child's bill raises the parent's rent and credit. + @hypothesis.example((PARENT_IN_CHILDS_HOME, 1, 500)) + def test_raising_a_bill(case): + assert_raising_a_bill(*case) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py new file mode 100644 index 00000000000..4975e86b61a --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py @@ -0,0 +1,59 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_gross_rent(Variable): + value_type = float + entity = TaxUnit + label = "Montana gross rent for the elderly homeowner/renter credit" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # § 15-30-2337(5): gross rent; § 15-30-2340(2): a claimant who rents + # the homestead + "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", + # ARM 42.4.302(2)(b): property taxes billed may be used "as rent if + # the property occupied by the claimant is in a name other than the + # claimant" + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # 2025 Schedule 2EC lines 24 and 25 + "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, lines 23 and 24: "If the property occupied by + # you is in a name other than your own, the property taxes billed for + # that property can qualify as rent only." + "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): + p = parameters( + period + ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter + # Schedule 2EC line 24: "the rent that you paid". + rent = add(tax_unit, period, ["rent"]) + # A claimant billed no property tax lives in a home in another + # member's name. Its property tax "can qualify as rent only" (2025 + # instructions, line 23; ARM 42.4.302(2)(b)): it goes on line 24, not + # line 23, so line 25 counts 15% of it. The rule offers that bill as + # a measure of the claimant's rent for the same home, not an + # addition to rent paid: read as an addition, every renter could add + # the landlord's property tax to their rent. So line 24 is the larger + # of the rent paid and the property tax billed to the household's + # other members, dependents included. With the claimant billed + # nothing, that is the household's whole property tax. The + # household's other members are the only owners the model can see. + claimant_property_tax = tax_unit( + "mt_elderly_homeowner_or_renter_credit_property_tax_billed", period + ) + in_another_name = claimant_property_tax <= 0 + person = tax_unit.members + head = person("is_tax_unit_head", period) + household_property_tax = tax_unit.sum( + add(person.household, period, ["real_estate_taxes"]) * head + ) + property_tax_as_rent = where( + in_another_name & p.property_tax_in_another_name_as_rent, + household_property_tax, + 0, + ) + return max_(rent, property_tax_as_rent) 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 aa8cd4ba1ff..04e7d41e713 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 @@ -11,6 +11,9 @@ class mt_elderly_homeowner_or_renter_credit_pre_one_claimant(Variable): 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", + # ARM 42.4.302(1): property tax billed on the owned part plus the + # rent-equivalent tax on the rented part + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", # 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 @@ -38,9 +41,13 @@ def formula(person, period, parameters): 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"]) + # Credit Computation: Schedule 2EC lines 23 to 26 + property_tax = person.tax_unit( + "mt_elderly_homeowner_or_renter_credit_property_tax_billed", period + ) + rent = person.tax_unit( + "mt_elderly_homeowner_or_renter_credit_gross_rent", period + ) countable_rent = rent * p.rent_equivalent_tax_rate countable_rent_and_property_tax = property_tax + countable_rent uncapped_credit_unit = max_( diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py new file mode 100644 index 00000000000..a9dc5b156c8 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_property_tax_billed(Variable): + value_type = float + entity = TaxUnit + label = "Montana property tax billed for the elderly homeowner/renter credit" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # § 15-30-2337(10): property tax billed; § 15-30-2340(1): a claimant + # who owns the homestead + "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", + # ARM 42.4.302(2)(b): property in a name other than the claimant + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # 2025 Schedule 2EC line 23: "the property tax you were billed" + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + "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): + # Schedule 2EC line 23 is "the property tax you were billed". A joint + # return makes one claim, so the claimant is the head and spouse. + # Property in anyone else's name, including a dependent's, "can + # qualify as rent only" (ARM 42.4.302(2)(b)); see + # mt_elderly_homeowner_or_renter_credit_gross_rent. + person = tax_unit.members + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + property_tax = person("real_estate_taxes", period) + return tax_unit.sum(property_tax * head_or_spouse) 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 c28c01f485d..c779c8db410 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 @@ -36,20 +36,24 @@ def formula(tax_unit, period, parameters): # 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 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 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 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. + # before the $1,150 cap. The model counts the property tax billed to + # each tax unit's head and spouse and the rent the tax unit paid, + # following Schedule 2EC's line labels ("the property tax you were + # billed", "the rent that you paid"). A tax unit billed no property + # tax lives in a home in another member's name, whose property tax + # "can qualify as rent only" (2025 instructions; ARM 42.4.302(2)(b)): + # it counts the household's property tax as rent, if that is larger + # (see mt_elderly_homeowner_or_renter_credit_gross_rent). 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 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. # 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. From 3d487c5d4a941e21e1deb723667e803048aa6cc1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:36:44 -0400 Subject: [PATCH 14/14] Apply ARM 42.4.302's rent rule in every year; keep co-owned homes on line 23 Review fixes for the property-tax-as-rent change: - The rule predates 2011. ARM 42.4.302(5), in force from 8/20/2004 (Wayback captures of mtrules.org, 2008 and 2010), read "Property taxes billed are allowable as rent if the property occupied by an eligible claimant is in a name other than the claimant"; the 2017 restructuring made it (2)(b). The property_tax_in_another_name_as_rent parameter, false for 2009-2010, is removed, and the 2010/2011 cases become one 2009 case. - A home co-owned by the claimant and a dependent stays on line 23 with both bills, as before; only a home wholly in others' names, a dependent's included, moves to line 24 as rent. - State the actual basis for max(rent paid, bill): the rule's "may use" is a choice between two measures of one home's cost, and ARM 42.4.302(3)(a) and (3)(c)(v) count one housing cost or another "but not both". - New cases: a joint couple billed nothing counts the other member's bill once (kills a head-or-spouse double count that only Hypothesis caught), and a home co-owned with a dependent. - Cite the line 23 instructions from PDF p. 46; describe the bill-as-rent path as the model's treatment in comments and the test docstring. Co-Authored-By: Claude Opus 5.5 --- ...x-mt-ehrc-property-in-others-name.fixed.md | 2 +- .../property_tax_in_another_name_as_rent.yaml | 24 ------- ...mt_elderly_homeowner_or_renter_credit.yaml | 33 ++++++++++ ...homeowner_or_renter_credit_gross_rent.yaml | 63 +++++++++++++++---- ..._or_renter_credit_property_tax_billed.yaml | 25 ++++++++ .../test_mt_elderly_credit_one_claimant.py | 17 ++--- ...y_homeowner_or_renter_credit_gross_rent.py | 43 +++++++------ ...er_or_renter_credit_property_tax_billed.py | 18 ++++-- ...wner_or_renter_credit_selected_claimant.py | 14 ++--- 9 files changed, 162 insertions(+), 77 deletions(-) delete mode 100644 policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml diff --git a/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md b/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md index 64fd3cddd82..749992a2659 100644 --- a/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md +++ b/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md @@ -1 +1 @@ -Count property tax billed for a Montana elderly homeowner/renter credit claimant's home in another household member's name as rent (ARM 42.4.302(2)(b)), and count only the claimant's own (head and spouse) property tax as property tax billed. +Count the property tax billed for a Montana elderly homeowner/renter credit claimant's home that is wholly in another household member's name, a dependent's included, as rent (ARM 42.4.302(2)(b)). diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml deleted file mode 100644 index 20cc8ea2f81..00000000000 --- a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/property_tax_in_another_name_as_rent.yaml +++ /dev/null @@ -1,24 +0,0 @@ -description: Montana counts the property tax billed for a home in another person's name as the claimant's rent under the elderly homeowner or renter credit, if this is true. -values: - # The 2009 and 2010 instructions let only property tax billed to an - # irrevocable or family trust "qualify as rent only"; the 2011 - # instructions first extend this to any property "in a name other than - # your own", the wording of ARM 42.4.302(2)(b). - 2009-01-01: false - 2011-01-01: true - -metadata: - unit: bool - period: year - label: Montana elderly homeowner or renter credit property tax in another name as rent - reference: - - title: ARM 42.4.302(2)(b), Computation of Elderly Homeowner/Renter Tax Credit - href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302 - - title: 2010 Montana Form 2 Instructions, Form 2EC line 6 (property tax billed to a trust qualifies as rent only) - href: https://taxsim.nber.org/historical_state_tax_forms/MT/2010/2010-2-Booklet.pdf#page=42 - - title: 2011 Montana Form 2 Instructions, Form 2EC line 6 (property in a name other than your own) - href: https://taxsim.nber.org/historical_state_tax_forms/MT/2011/Form_2_Booklet_2011.pdf-1.pdf#page=54 - - title: 2024 Montana Form 2 Instructions, Schedule 2EC line 23 - 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 - - title: 2025 Montana Form 2 Instructions, Schedule 2EC line 23 - href: 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 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 a11062e7c10..13486feebea 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 @@ -1010,3 +1010,36 @@ mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 mt_elderly_homeowner_or_renter_credit_gross_rent: 4_000 mt_elderly_homeowner_or_renter_credit: [600, 0] + +- name: 2024 Schedule 2EC, a home co-owned with a dependent keeps both bills on line 23 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + is_tax_unit_dependent: false + real_estate_taxes: 500 + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The claimant is billed too, so the home is in the claimant's name and + # ARM 42.4.302(2)(b) does not apply. Line 18 = 14_000; line 20 = 1_400; + # line 21 = 0; line 22 = 0. Line 23 = 500 + 2_000 = 2_500; line 24 = 0; + # line 27 = 2_500; line 28 = 1_150 cap; line 30 = 1_150, as before + # ARM 42.4.302(2)(b) was modeled. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_500 + mt_elderly_homeowner_or_renter_credit_gross_rent: 0 + mt_elderly_homeowner_or_renter_credit: [1_150, 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_rent.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml index a68c4aa6ecd..0edc1f5d79a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml @@ -213,8 +213,8 @@ mt_elderly_homeowner_or_renter_credit_property_tax_billed: [1_800, 2_000] mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] -- name: Case 9, in 2010 only property billed to a trust counted as rent. - period: 2010 +- name: Case 9, in 2009 ARM 42.4.302(5) already counted property in another's name as rent. + period: 2009 absolute_error_margin: 0.01 input: people: @@ -235,10 +235,14 @@ members: [person1, person2] state_code: MT output: - mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + # ARM 42.4.302(5), effective 8/20/2004 to 11/10/2017: "Property taxes + # billed are allowable as rent if the property occupied by an eligible + # claimant is in a name other than the claimant." It became (2)(b) in + # 2017. + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] -- name: Case 10, from 2011 property in another person's name counts as rent. - period: 2011 +- name: Case 10, outside Montana. + period: 2024 absolute_error_margin: 0.01 input: people: @@ -257,30 +261,63 @@ households: household: members: [person1, person2] - state_code: MT + state_code: ID output: - mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] -- name: Case 11, outside Montana. +- name: Case 11, a joint couple billed nothing counts the other member's bill once. period: 2024 absolute_error_margin: 0.01 input: people: person1: - age: 80 + age: 70 rent: 0 person2: + age: 66 + rent: 0 + person3: age: 45 rent: 0 real_estate_taxes: 3_000 tax_units: tax_unit1: - members: [person1] + members: [person1, person2] tax_unit2: - members: [person2] + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # One claim for the joint return: line 24 = 3_000, not 6_000. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [0, 3_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 12, a home co-owned with a dependent stays on line 23. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 0 + is_tax_unit_dependent: false + real_estate_taxes: 500 + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] households: household: members: [person1, person2] - state_code: ID + state_code: MT output: - mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + # The home is in the claimant's name too, so ARM 42.4.302(2)(b) does not + # apply: the tax unit's bills, 500 + 2_000, are line 23, and line 24 is 0. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_500 + mt_elderly_homeowner_or_renter_credit_gross_rent: 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_property_tax_billed.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml index 755928a772a..f490d893e65 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml @@ -102,3 +102,28 @@ state_code: ID output: mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + +- name: Case 6, a home co-owned with a dependent counts both bills. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + is_tax_unit_dependent: false + real_estate_taxes: 500 + person2: + age: 50 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The claimant is billed, so the home is in the claimant's name and the + # tax unit's bills are line 23: 500 + 2_000. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_500 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 93430829674..c77da08ba2e 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py +++ b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py @@ -9,15 +9,18 @@ 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 property tax billed (line 23) and rent (line 24). A tax unit billed no -property tax lives in a home in another member's name, whose property tax -"can qualify as rent only" (ARM 42.4.302(2)(b)): its line 24 is the larger -of the rent it paid and the property tax billed to the household's other -tax units. The model pays the tax unit whose credit is largest. +property tax is treated as living in a home in another member's name, whose +property tax "can qualify as rent only" (ARM 42.4.302(2)(b)): its line 24 is +the larger of the rent it paid and the property tax billed to the +household's other members. 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): +rent; no earned income, so no refundable credit enters line 8). The +generator has no dependents, so the household's other members are its other +tax units; the YAML tests cover a home in a dependent's name: 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 @@ -127,10 +130,10 @@ def reference(units): for unit in units: eligible = max(adult["age"] for adult in unit["adults"]) >= P.age_threshold # Line 24: a tax unit billed no property tax may use the property tax - # billed to the household's other tax units as rent, in place of the + # billed to the household's other members as rent, in place of the # rent it paid if that is larger (ARM 42.4.302(2)(b)). rent = unit["rent"] - if unit["property_tax"] == 0 and P.property_tax_in_another_name_as_rent: + if unit["property_tax"] == 0: rent = max(rent, household_property_tax) # Lines 23-28. housing = unit["property_tax"] + rent * P.rent_equivalent_tax_rate diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py index 4975e86b61a..7a2cd0f939c 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py @@ -15,8 +15,14 @@ class mt_elderly_homeowner_or_renter_credit_gross_rent(Variable): "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0400/0150-0300-0230-0400.html", # ARM 42.4.302(2)(b): property taxes billed may be used "as rent if # the property occupied by the claimant is in a name other than the - # claimant" + # claimant"; (3)(a) and (3)(c)(v): two measures of housing cost for + # one period count "either ... or ..., but not both" "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # ARM 42.4.302(5) before its 2017 restructuring, in force from + # 8/20/2004: "Property taxes billed are allowable as rent if the + # property occupied by an eligible claimant is in a name other than + # the claimant." + "https://web.archive.org/web/20080624004125/http://www.mtrules.org/gateway/RuleNo.asp?RN=42.4.302", # 2025 Schedule 2EC lines 24 and 25 "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, lines 23 and 24: "If the property occupied by @@ -26,34 +32,33 @@ class mt_elderly_homeowner_or_renter_credit_gross_rent(Variable): ) def formula(tax_unit, period, parameters): - p = parameters( - period - ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter # Schedule 2EC line 24: "the rent that you paid". rent = add(tax_unit, period, ["rent"]) - # A claimant billed no property tax lives in a home in another - # member's name. Its property tax "can qualify as rent only" (2025 - # instructions, line 23; ARM 42.4.302(2)(b)): it goes on line 24, not - # line 23, so line 25 counts 15% of it. The rule offers that bill as - # a measure of the claimant's rent for the same home, not an - # addition to rent paid: read as an addition, every renter could add - # the landlord's property tax to their rent. So line 24 is the larger - # of the rent paid and the property tax billed to the household's - # other members, dependents included. With the claimant billed - # nothing, that is the household's whole property tax. The - # household's other members are the only owners the model can see. + # The model treats a tax unit billed no property tax as living in a + # home in another household member's name, a dependent's included. + # Its property tax "can qualify as rent only" (2025 instructions, + # line 23; ARM 42.4.302(2)(b)): it goes on line 24, not line 23, so + # line 25 counts 15% of it. With the claimant billed nothing, "the + # property taxes billed for that property" are the household's whole + # property tax. + # The claimant "may use" the bill as rent: a choice between two + # measures of what it costs to occupy one home, not an addition to + # the rent paid. The rule's facility provisions likewise let a + # claimant report one housing cost or another for a period, "but not + # both" (ARM 42.4.302(3)(a), (3)(c)(v)). So line 24 is the larger of + # the rent paid and the bill. + # Owners outside the household, such as a landlord, are not visible + # here, and real_estate_taxes is not limited to the residence and + # one acre. claimant_property_tax = tax_unit( "mt_elderly_homeowner_or_renter_credit_property_tax_billed", period ) - in_another_name = claimant_property_tax <= 0 person = tax_unit.members head = person("is_tax_unit_head", period) household_property_tax = tax_unit.sum( add(person.household, period, ["real_estate_taxes"]) * head ) property_tax_as_rent = where( - in_another_name & p.property_tax_in_another_name_as_rent, - household_property_tax, - 0, + claimant_property_tax <= 0, household_property_tax, 0 ) return max_(rent, property_tax_as_rent) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py index a9dc5b156c8..d961cdec535 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py @@ -17,16 +17,22 @@ class mt_elderly_homeowner_or_renter_credit_property_tax_billed(Variable): "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", # 2025 Schedule 2EC line 23: "the property tax you were billed" "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", - "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", + # PDF pages 46-47: line 23, and property "in a name other than your + # own" + "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=46", ) def formula(tax_unit, period, parameters): # Schedule 2EC line 23 is "the property tax you were billed". A joint # return makes one claim, so the claimant is the head and spouse. - # Property in anyone else's name, including a dependent's, "can - # qualify as rent only" (ARM 42.4.302(2)(b)); see - # mt_elderly_homeowner_or_renter_credit_gross_rent. + # When either is billed property tax, the home is in the claimant's + # name and the tax unit's bills count here, including a dependent + # co-owner's. When neither is, the home is in a name other than the + # claimant's, a dependent's included, and its property tax "can + # qualify as rent only" (ARM 42.4.302(2)(b)): line 23 is 0 and + # mt_elderly_homeowner_or_renter_credit_gross_rent counts it. person = tax_unit.members - head_or_spouse = person("is_tax_unit_head_or_spouse", period) property_tax = person("real_estate_taxes", period) - return tax_unit.sum(property_tax * head_or_spouse) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + claimant_billed = tax_unit.sum(property_tax * head_or_spouse) > 0 + return where(claimant_billed, tax_unit.sum(property_tax), 0) 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 c779c8db410..8be14dd130a 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 @@ -36,13 +36,13 @@ def formula(tax_unit, period, parameters): # 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 Schedule 2EC line 26: property tax billed plus 15% of rent, - # before the $1,150 cap. The model counts the property tax billed to - # each tax unit's head and spouse and the rent the tax unit paid, - # following Schedule 2EC's line labels ("the property tax you were - # billed", "the rent that you paid"). A tax unit billed no property - # tax lives in a home in another member's name, whose property tax - # "can qualify as rent only" (2025 instructions; ARM 42.4.302(2)(b)): - # it counts the household's property tax as rent, if that is larger + # before the $1,150 cap. The model counts each tax unit's own + # property tax and rent, following Schedule 2EC's line labels ("the + # property tax you were billed", "the rent that you paid"). A tax + # unit whose head and spouse are billed no property tax is treated as + # living in a home in another member's name, whose property tax "can + # qualify as rent only" (2025 instructions; ARM 42.4.302(2)(b)): it + # counts the household's property tax as rent, if that is larger # (see mt_elderly_homeowner_or_renter_credit_gross_rent). The statute # instead defines both for the homestead (§ 15-30-2337(5), (10)), # which would give the one claimant the household's whole property