From 52ffe10226cd26b3ff24e48ce7f9bc555924dd07 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:08:30 -0400 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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 620e791e35f4baf2d2d0f1ddc7a8fd6bd2b7c810 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 13:30:21 -0400 Subject: [PATCH 06/11] Count refundable credits received in Montana elderly credit income ARM 42.4.301(2) counts "federal refundable tax credits received" and "any state refundable tax credits received, including elderly homeowner/renter credit refunds" in gross household income, and Schedule 2EC line 8 (2023 line 7) combines the refundable credits received by all household members. The model counted only the Montana EITC. - Count the return's federal refundable credits (income_tax_refundable_credits) from 2017, once per tax unit, on the head. - Add an input for this credit received during the year (last year's claim); reading last year's credit would rerun the prior-year model for every record. - Count the rebate for the prior property tax year from 2024 (2024 and 2025 Schedule 2EC line 8), read at period.last_year. - Date the Montana property tax rebate: 675 for 2022-2023, 400 for 2024 (Ch. 767, L. 2025), none after. Co-Authored-By: Claude Opus 5.5 --- .../fix-mt-ehrc-refundable-credits.fixed.md | 1 + .../gross_income_sources.yaml | 55 ++++- .../credits/rebate/property/amount.yaml | 14 +- .../mt_property_tax_rebate_in_aggregate.yaml | 42 ++++ ...mt_elderly_homeowner_or_renter_credit.yaml | 184 +++++++++++++++- ...ter_credit_federal_refundable_credits.yaml | 95 +++++++++ ..._renter_credit_gross_household_income.yaml | 175 +++++++++++++++ ...r_credit_property_tax_rebate_received.yaml | 108 ++++++++++ .../rebate/mt_property_tax_rebate.yaml | 26 +++ ...st_mt_elderly_credit_refundable_credits.py | 201 ++++++++++++++++++ ...enter_credit_federal_refundable_credits.py | 27 +++ ...ter_credit_property_tax_rebate_received.py | 29 +++ ...rly_homeowner_or_renter_credit_received.py | 27 +++ 13 files changed, 979 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fix-mt-ehrc-refundable-credits.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_federal_refundable_credits.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_rebate_received.yaml create mode 100644 policyengine_us/tests/test_mt_elderly_credit_refundable_credits.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_federal_refundable_credits.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_rebate_received.py create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_received.py diff --git a/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md b/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md new file mode 100644 index 00000000000..be83e89c23e --- /dev/null +++ b/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md @@ -0,0 +1 @@ +Count federal refundable credits, the elderly homeowner/renter credit received during the year, and the prior year's property tax rebate in Montana's elderly homeowner/renter credit gross household income; date the Montana property tax rebate ($400 for tax year 2024, none after). 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..24a0d58c981 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,6 +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: ARM 42.4.301(2) as in effect 8/20/2004 to 11/9/2017 ("elderly homeowner/renter credit refunds received") + href: https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/788508cc-8418-41c8-926d-3dfb11af3fd6 + - title: ARM 42.4.301(2) as in effect 11/10/2017 to 9/6/2024 ("federal refundable credits received" and "any state refundable credits received") + href: https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/297c7a8d-f6f4-4b70-a7b7-fe1cedee53e7 + - title: ARM 42.4.301(2) as in effect from 9/7/2024 ("federal refundable tax credits received" and "any state refundable tax credits received") + href: https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/768c8344-24ee-4950-9d5e-9d8fc97ec0f7 + - title: 2017 Montana Form 2EC instructions, Gross Household Income Worksheet, item 4 ("Any federal or state refundable credits including the 2EC credit") + href: https://taxsim.nber.org/historical_state_tax_forms/MT/2017/Form%202%202017%20Booklet.pdf#page=65 + - title: 2023 Montana Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 7, refundable credits received + 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 8, refundable credits received + 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=47 + - title: 2025 Montana Form 2 instructions, Schedule 2EC, line 8, refundable credits received + 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=45 values: 2009-01-01: @@ -27,8 +41,45 @@ values: - child_support_received # § 15-30-2337(9)(a)(iv) – support money - strike_benefits # § 15-30-2337(9)(a)(v) – nontaxable strike benefits - general_assistance # § 15-30-2337(9)(a)(vi) – cash public assistance - - mt_refundable_credits_before_renter_credit # Line 7: Refundable credits + # Refundable credits received (2023 schedule line 7, Schedule 2EC line 8 + # from 2024): Montana's refundable credits other than this credit, + # and this credit as received during the year (ARM 42.4.301(2)(c)). + - mt_refundable_credits_before_renter_credit + - mt_elderly_homeowner_or_renter_credit_received + # ARM 42.4.301(2)(b), amended effective 11/10/2017, counts "federal + # refundable credits received"; the 2017 Form 2EC worksheet counts "any + # federal or state refundable credits". + 2017-01-01: + - adjusted_gross_income_person + - tax_exempt_interest_income + - tax_exempt_ira_distributions + - tax_exempt_pension_income + - veterans_benefits + - child_support_received + - strike_benefits + - general_assistance + - mt_refundable_credits_before_renter_credit + - mt_elderly_homeowner_or_renter_credit_received + - mt_elderly_homeowner_or_renter_credit_federal_refundable_credits + # 2024 Schedule 2EC line 8 adds "the 2023 Montana property tax rebate" + # (the 2025 schedule: "the 2024 Montana property tax rebate"), the rebate + # for the prior property tax year, claimed during this year. The 2023 + # schedule (line 7) does not list the rebate for 2022 property taxes. + 2024-01-01: + - adjusted_gross_income_person + - tax_exempt_interest_income + - tax_exempt_ira_distributions + - tax_exempt_pension_income + - veterans_benefits + - child_support_received + - strike_benefits + - general_assistance + - mt_refundable_credits_before_renter_credit + - mt_elderly_homeowner_or_renter_credit_received + - mt_elderly_homeowner_or_renter_credit_federal_refundable_credits + - mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received # Social Security under § 15-30-2337(9)(a)(viii) is added separately in # mt_elderly_homeowner_or_renter_credit_gross_household_income.py because the # untaxed portion is computed as social_security - taxable_social_security. -# The renter credit itself is excluded to avoid a circular reference. +# This year's elderly homeowner/renter credit is excluded to avoid a circular +# reference; the credit received during the year counts instead. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/rebate/property/amount.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/rebate/property/amount.yaml index 4ec487b6cb1..f7c074a2c6d 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/credits/rebate/property/amount.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/credits/rebate/property/amount.yaml @@ -1,7 +1,13 @@ description: Montana provides the following property tax rebate amount. values: + # Tax years 2022 and 2023: $500 (HB 222, Ch. 47, L. 2023) plus the $175 + # bonus (HB 816, Ch. 764, L. 2023). 2022-01-01: 675 - # The property tax rebate was only offered in the 2022 and 2023 tax years + # Tax year 2024: $400 (SB 542, Ch. 767, L. 2025, sec. 3). + 2024-01-01: 400 + # No rebate has been enacted for tax year 2025 or later; the tax year 2024 + # rebate terminates June 30, 2026 (Ch. 767, L. 2025, sec. 29(2)). + 2025-01-01: 0 metadata: period: year unit: currency-USD @@ -16,3 +22,9 @@ metadata: href: https://docs.legmt.gov/download-ticket?ticketId=ff6faf83-515f-4472-a079-c41a4a75e827#page=1 - title: 2024 Montana Form 2 Instructions 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=3 + - title: Montana Department of Revenue, Income tax and property tax rebate report, May 2024 (maximum rebate $675 in each of tax years 2022 and 2023) + href: https://archive.legmt.gov/content/Committees/Interim/2023-2024/Revenue/Meetings/May-2024/5.1-DOR-rebate-report.pdf#page=13 + - title: 2025 Montana Session Laws, Chapter 767 (SB 542), sec. 3(1), property tax rebate for tax year 2024 + href: https://archive.legmt.gov/content/Sessions/69th/Contractor_index/CH0767.pdf#page=2 + - title: 2025 Montana Session Laws, Chapter 767 (SB 542), sec. 29(2), termination of the tax year 2024 rebate + href: https://archive.legmt.gov/content/Sessions/69th/Contractor_index/CH0767.pdf#page=44 diff --git a/policyengine_us/tests/policy/baseline/gov/states/household/state_property_tax_credits/mt_property_tax_rebate_in_aggregate.yaml b/policyengine_us/tests/policy/baseline/gov/states/household/state_property_tax_credits/mt_property_tax_rebate_in_aggregate.yaml index 3abf3612f58..3c15dbf23d6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/household/state_property_tax_credits/mt_property_tax_rebate_in_aggregate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/household/state_property_tax_credits/mt_property_tax_rebate_in_aggregate.yaml @@ -24,3 +24,45 @@ # contributor to the aggregate is the $675 property tax rebate. mt_property_tax_rebate: 675 taxsim_state_property_tax_credit: 675 + +- name: The tax year 2024 MT property tax rebate of 400 flows into the aggregate + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 50_000 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + # Ch. 767, L. 2025 (SB 542), sec. 3(1): $400 for tax year 2024. Before + # the rebate amount was dated, the model carried 675 into 2024. + mt_property_tax_rebate: 400 + taxsim_state_property_tax_credit: 400 + +- name: No MT property tax rebate in the aggregate for tax year 2025 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 50_000 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + mt_property_tax_rebate: 0 + taxsim_state_property_tax_credit: 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.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..5f41944970c 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 @@ -200,8 +200,16 @@ members: [person1] state_code: MT output: - # (863.08 - (gross_hh - 6,300) x 0.05) x 1.0, capped at $1,000 = $133. - mt_elderly_homeowner_or_renter_credit: 133.33 + # (863.08 - (gross_hh - 6,300) x 0.05) x 1.0, capped at $1,000. + # 2021 instructions, line 7: "Refundable credits include the federal and + # Montana Earned Income Tax Credit, the federal Child Tax Credits, + # Economic Impact Payments". Federal EITC (ARPA, no qualifying child) + # 1_502 - 15.3% x (20_892.60 - 11_610) = 81.76; Montana EITC 3% = 2.45; + # Economic Impact Payment 1_400. Line 10 = 20_892.60 + 1_484.21 = + # 22_376.81; line 12 = 16_076.81; line 14 = 803.84; line 19 = 863.08 - + # 803.84 = 59.24. The earlier expected value, 133.33, counted only the + # Montana EITC. + mt_elderly_homeowner_or_renter_credit: 59.24 - name: MT EHRC uses the $12,600 exclusion from 2022 (HB 191) period: 2022 @@ -399,3 +407,175 @@ 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 worked case, a household member's federal EITC counts as a refundable credit received + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 40 + employment_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 son lives in the household and files his own + # return. Line 5 pensions 20_000. Line 8, refundable credits received by + # all household members: his federal EITC 632 (Rev. Proc. 2023-34, no + # qualifying child, wages between 8_260 and 10_330) and Montana EITC + # 10% x 632 = 63.20, 695.20 in all. Line 17 his wages 10_000. Line 18 + # gross household income 30_695.20; line 20 = 30_695.20 - 12_600 = + # 18_095.20; line 21 = 0.05; line 22 = 904.76; line 27 = 2_000 - 904.76 + # = 1_095.24; line 28 = 1_095.24 (under 1_150); line 29 = 100%; line 30 = + # 1_095.24. Counting only his Montana EITC gave 1_126.84. + mt_elderly_homeowner_or_renter_credit_net_household_income: [904.76, 0] + mt_elderly_homeowner_or_renter_credit: [1_095.24, 0] + +- name: 2024 Schedule 2EC worked case, a parent's federal EITC and refundable child tax credit count + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 30 + employment_income: 10_000 + person3: + age: 5 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # The claimant's daughter and grandchild live in the household; the + # daughter files her own return. Line 8: federal EITC 34% x 10_000 = + # 3_400, refundable part of the federal child tax credit + # min(1_700, 15% x (10_000 - 2_500)) = 1_125, Montana EITC 340; 4_865 in + # all. Line 17 her wages 10_000. Line 18 = 20_000 + 4_865 + 10_000 = + # 34_865; line 20 = 22_265; line 22 = 22_265 x 0.05 = 1_113.25; line 27 = + # 2_000 - 1_113.25 = 886.75; line 29 = 100% (under 35_000); line 30 = + # 886.75. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 14_865, 0] + mt_elderly_homeowner_or_renter_credit_net_household_income: [1_113.25, 0, 0] + mt_elderly_homeowner_or_renter_credit: [886.75, 0, 0] + +- name: 2024 Schedule 2EC worked case, federal credits move gross household income into the 30 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: 30 + employment_income: 12_000 + person3: + age: 5 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # Line 8: federal EITC 34% x 12_000 = 4_080 (under the 12_390 earned + # income amount), refundable child tax credit + # min(1_700, 15% x 9_500) = 1_425, Montana EITC 408; 5_913 in all. + # Line 18 = 20_000 + 5_913 + 12_000 = 37_913; line 20 = 25_313; + # line 22 = 1_265.65; line 27 = 734.35; line 29 = 30% (37_501 to 40_000); + # line 30 = 734.35 x 0.3 = 220.31. Counting only the Montana EITC, line + # 18 was 32_408 and the credit 1_009.60. + mt_elderly_homeowner_or_renter_credit: [220.31, 0, 0] + +- name: 2025 Schedule 2EC worked case, the credit and the property tax rebate received in 2025 count + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + taxable_pension_income: 20_000 + real_estate_taxes: + 2024: 1_000 + 2025: 1_000 + mt_elderly_homeowner_or_renter_credit_received: 1_150 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2024: MT + 2025: MT + output: + # Line 5 pensions 20_000. Line 8: "your elderly homeowner/renter credit + # received in 2025", 1_150 (the 2024 credit), and "the 2024 Montana + # property tax rebate", min(400, 1_000) = 400; 1_550 in all. Line 18 = + # 21_550; line 20 = 8_950; line 21 = 0.039 (8_000 to 9_000); line 22 = + # 349.05; line 27 = 1_000 - 349.05 = 650.95; line 30 = 650.95. Without + # line 8, line 22 would be 7_400 x 0.035 = 259 and the credit 741. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 21_550 + mt_elderly_homeowner_or_renter_credit_net_household_income: 349.05 + mt_elderly_homeowner_or_renter_credit: 650.95 + +- name: 2023 Elderly Homeowner/Renter Credit Schedule worked case, a household member's federal EITC counts on line 7 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: + 2022: 2_000 + 2023: 2_000 + person2: + age: 40 + employment_income: 10_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: + 2022: MT + 2023: MT + output: + # Line 1 federal AGI of all household members 30_000. Line 7, the + # refundable credits received by the members of the household: the + # son's federal EITC, Rev. Proc. 2022-38, no qualifying child, + # 600 - 7.65% x (10_000 - 9_800) = 584.70, and Montana EITC 3%, 17.54; + # 602.24 in all. The 675 rebate for 2022 property taxes is not on the + # 2023 line 7 list. Line 10 = 30_602.24; line 12 = 18_002.24; line 13 = + # 0.05; line 14 = 900.11; line 19 = 2_000 - 900.11 = 1_099.89; line 20 + # = 1_099.89; line 21 = 100%; line 22 = 1_099.89. + mt_elderly_homeowner_or_renter_credit_net_household_income: [900.11, 0] + mt_elderly_homeowner_or_renter_credit: [1_099.89, 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_federal_refundable_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_federal_refundable_credits.yaml new file mode 100644 index 00000000000..5f2f32ff9fe --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_federal_refundable_credits.yaml @@ -0,0 +1,95 @@ +- name: Case 1, a single parent's federal EITC and refundable CTC are counted once, on the head. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 10_000 + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Rev. Proc. 2023-34, one qualifying child: 34% x 10_000 = 3_400 (under + # the 12_390 earned income amount). Refundable CTC: no tax before credits + # (10_000 is under the standard deduction), so + # min(1_700, 15% x (10_000 - 2_500)) = 1_125. + eitc: 3_400 + refundable_ctc: 1_125 + # 3_400 + 1_125 = 4_525, on the head only. + mt_elderly_homeowner_or_renter_credit_federal_refundable_credits: [4_525, 0] + +- name: Case 2, a joint return's federal refundable credits go to the head, not the spouse or child. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + employment_income: 15_000 + person2: + age: 33 + person3: + age: 4 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Rev. Proc. 2023-34, one child: 15_000 is above the 12_390 earned income + # amount and under the 29_640 joint phaseout threshold, so the maximum + # 4_213. Refundable CTC: min(1_700, 15% x (15_000 - 2_500) = 1_875) + # = 1_700. Total 5_913. + mt_elderly_homeowner_or_renter_credit_federal_refundable_credits: [5_913, 0, 0] + +- name: Case 3, the 2021 Economic Impact Payment counts with the federal EITC. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + employment_income: 20_892.60 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # 2021 Elderly Homeowner/Renter Credit Schedule instructions, line 7: + # "Refundable credits include the federal and Montana Earned Income Tax + # Credit, the federal Child Tax Credits, Economic Impact Payments". + # 2021 childless EITC (ARPA): 1_502 - 15.3% x (20_892.60 - 11_610) + # = 81.76. Recovery rebate credit (third-round payment): 1_400. + eitc: 81.76 + mt_elderly_homeowner_or_renter_credit_federal_refundable_credits: 1_481.76 + +- name: Case 4, zero outside Montana. + period: 2024 + input: + people: + person1: + age: 30 + employment_income: 10_000 + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_federal_refundable_credits: [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_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 88dbe3c9d4e..cd19d78fdb1 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 @@ -160,3 +160,178 @@ # 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] + +- name: A household member's federal and Montana EITC count once, on their own return + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 40 + employment_income: 10_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # 2024 Schedule 2EC line 8: "Combine all the refundable credits received + # by all household members in 2024. Refundable credits include the + # federal and Montana earned income tax credits". Rev. Proc. 2023-34, no + # qualifying child: 10_000 is above the 8_260 earned income amount and + # under the 10_330 phaseout threshold, so the maximum 632; the Montana + # EITC is 10% of it, 63.20. The member's 10_000 + 632 + 63.20. + mt_elderly_homeowner_or_renter_credit_federal_refundable_credits: [0, 632] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_695.20] + +- name: A parent's federal EITC and refundable child tax credit count in gross household income + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 30 + employment_income: 10_000 + person3: + age: 5 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # 2024 Schedule 2EC line 8 counts "the refundable portion of the federal + # child tax credit". Federal EITC 34% x 10_000 = 3_400; refundable CTC + # min(1_700, 15% x 7_500) = 1_125; Montana EITC 340. The parent: + # 10_000 + 3_400 + 1_125 + 340 = 14_865. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 14_865, 0] + +- name: The elderly homeowner/renter credit received during the year counts + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + mt_elderly_homeowner_or_renter_credit_received: 1_150 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # 2024 Schedule 2EC line 8: "including your elderly homeowner/renter + # credit received in 2024". 20_000 + 1_150. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 21_150 + +- name: The rebate for 2023 property taxes, received in 2024, counts + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: + 2023: 2_000 + 2024: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2023: MT + 2024: MT + output: + # 2024 Schedule 2EC line 8 lists "the 2023 Montana property tax rebate": + # min(675, 2_000) = 675. 20_000 + 675. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_675 + +- name: The rebate for 2022 property taxes does not count on the 2023 schedule + period: 2023 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: + 2022: 2_000 + 2023: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2022: MT + 2023: MT + output: + # The 2023 instructions' line 7 list (federal and Montana EITC, federal + # child tax credits, unlocking public land credit, elderly + # homeowner/renter credit) has no property tax rebate, though the 675 + # rebate for 2022 property taxes was claimed in August-October 2023. + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 675 + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 + +- name: Federal refundable credits count from 2017 + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 10_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # ARM 42.4.301(2)(b), effective 11/10/2017: "federal refundable credits + # received"; 2017 Form 2EC worksheet item 4: "Any federal or state + # refundable credits". Rev. Proc. 2016-55, no qualifying child: + # 510 - 7.65% x (10_000 - 8_340) = 383.01. + eitc: 383.01 + mt_elderly_homeowner_or_renter_credit_gross_household_income: 10_383.01 + +- name: Federal refundable credits do not count before 2017 + period: 2016 + input: + people: + person1: + age: 40 + employment_income: 10_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # The 2004-2017 ARM 42.4.301(2)(b) counted federal refunds only "to the + # extent that the amount recovered reduced the claimant's Montana income + # tax in a prior year". + mt_elderly_homeowner_or_renter_credit_gross_household_income: 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_property_tax_rebate_received.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_rebate_received.yaml new file mode 100644 index 00000000000..38304aebb07 --- /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_rebate_received.yaml @@ -0,0 +1,108 @@ +- name: Case 1, the 2023 rebate is received in 2024 and goes to the head. + period: 2024 + input: + people: + person1: + age: 70 + real_estate_taxes: + 2023: 2_000 + 2024: 2_100 + person2: + age: 68 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: + 2023: MT + 2024: MT + output: + # 2024 Schedule 2EC line 8: "the 2023 Montana property tax rebate", + # min(675, 2_000) = 675. + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: [675, 0] + +- name: Case 2, the 2023 rebate is the 2023 property tax when that is less than 675. + period: 2024 + input: + people: + person1: + age: 70 + real_estate_taxes: + 2023: 500 + 2024: 900 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2023: MT + 2024: MT + output: + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 500 + +- name: Case 3, the 2024 rebate of 400 is received in 2025. + period: 2025 + input: + people: + person1: + age: 70 + real_estate_taxes: + 2024: 2_000 + 2025: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2024: MT + 2025: MT + output: + # 2025 Schedule 2EC line 8: "the 2024 Montana property tax rebate"; + # Ch. 767, L. 2025, sec. 3(1): $400 or the property taxes paid, if less. + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 400 + +- name: Case 4, there is no rebate for 2025 property taxes to receive in 2026. + period: 2026 + input: + people: + person1: + age: 70 + real_estate_taxes: + 2025: 2_000 + 2026: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2025: MT + 2026: MT + output: + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 0 + +- name: Case 5, zero when only this year's property tax is known. + period: 2024 + input: + people: + person1: + age: 70 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Inputs never carry backwards, so last year's property tax and rebate + # are 0. + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/rebate/mt_property_tax_rebate.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/rebate/mt_property_tax_rebate.yaml index b2c717f534e..ea970786fd0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/rebate/mt_property_tax_rebate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/rebate/mt_property_tax_rebate.yaml @@ -21,3 +21,29 @@ state_code: MT output: mt_property_tax_rebate: 675 + +- name: The tax year 2024 rebate is 400 + period: 2024 + input: + real_estate_taxes: 2_000 + state_code: MT + output: + # Ch. 767, L. 2025 (SB 542), sec. 3(1): "$400 or the amount of total + # property taxes paid, whichever is less, for tax year 2024". + mt_property_tax_rebate: 400 + +- name: The tax year 2024 rebate is the property tax when that is less than 400 + period: 2024 + input: + real_estate_taxes: 300 + state_code: MT + output: + mt_property_tax_rebate: 300 + +- name: No rebate for tax year 2025 + period: 2025 + input: + real_estate_taxes: 2_000 + state_code: MT + output: + mt_property_tax_rebate: 0 diff --git a/policyengine_us/tests/test_mt_elderly_credit_refundable_credits.py b/policyengine_us/tests/test_mt_elderly_credit_refundable_credits.py new file mode 100644 index 00000000000..a946da6f053 --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_refundable_credits.py @@ -0,0 +1,201 @@ +"""Property tests: refundable credits in the Montana elderly homeowner/renter +credit's gross household income. + +ARM 42.4.301(2)(b)-(c) counts "federal refundable tax credits received" and +"any state refundable tax credits received" in gross household income, and +the 2024 Schedule 2EC line 8 combines "all the refundable credits received by +all household members". The model sums a person-level gross household income +over the household, so each tax unit's federal refundable credits are put on +its head once. These tests check that allocation for households of a claimant +aged 62 or older and a younger member who files their own return with up to +two children, so that the member can have a federal EITC and a refundable +child tax credit. + +YAML cases cannot compare two simulations or draw household structures, so +this file does both. Each example runs the households twice: as given (W) and +with the federal refundable credits in gross household income set to zero +(Z). Properties, for every household: + +1. Counted once: the household sum of the person-level federal credits + equals the sum of income_tax_refundable_credits over its tax units, read + from the tax-unit array. +2. Accounting: gross household income in W exceeds Z by exactly those + credits. +3. More income never raises the credit: the credit in W is at most Z's. +4. The credit is between zero and the cap, and zero when gross household + income is $45,000 or more. +5. Montana tax changes only through the credit: Montana refundable credits + other than this one are the same in W and Z, and the household's Montana + income tax rises by exactly the fall in its credit. + +The model computes in single precision, so comparisons allow one cent or +eight float32 spacings at the amount compared, 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 +FEDERAL = "mt_elderly_homeowner_or_renter_credit_federal_refundable_credits" +GROSS = "mt_elderly_homeowner_or_renter_credit_gross_household_income" +CREDIT = "mt_elderly_homeowner_or_renter_credit" + + +def build_situation(cases, zero_federal_credits): + """One Montana household per case: the claimant files alone; the member + files with their children. Also returns each person's and tax unit's + case index.""" + people, tax_units, households, case_of = {}, {}, {}, {} + for i, c in enumerate(cases): + claimant, member = f"claimant_{i}", f"member_{i}" + children = [f"child_{i}_{j}" for j in range(len(c["child_ages"]))] + people[claimant] = { + "age": {YEAR: c["claimant_age"]}, + "taxable_pension_income": {YEAR: c["pension"]}, + "real_estate_taxes": {YEAR: c["property_tax"]}, + "rent": {YEAR: c["rent"]}, + } + people[member] = { + "age": {YEAR: c["member_age"]}, + "employment_income": {YEAR: c["wages"]}, + } + for child, age in zip(children, c["child_ages"]): + people[child] = {"age": {YEAR: age}} + tax_units[f"claimant_unit_{i}"] = {"members": [claimant]} + tax_units[f"member_unit_{i}"] = {"members": [member, *children]} + for name in [claimant, member, *children, *list(tax_units)[-2:]]: + case_of[name] = i + households[f"household_{i}"] = { + "members": [claimant, member, *children], + "state_code": {YEAR: "MT"}, + } + if zero_federal_credits: + # Set for everyone: an input for some people gives the others its + # default, not its formula. + for person in people.values(): + person[FEDERAL] = {YEAR: 0} + situation = {"people": people, "tax_units": tax_units, "households": households} + return situation, case_of + + +def calculate(cases, zero_federal_credits): + """Household totals of person- and tax-unit-level variables, summed by + entity id rather than array position.""" + situation, case_of = build_situation(cases, zero_federal_credits) + simulation = Simulation(situation=situation) + + def by_household(variable, entity): + values = np.asarray(simulation.calculate(variable, YEAR), dtype=float) + totals = np.zeros(len(cases)) + for name, value in zip(simulation.populations[entity].ids, values): + totals[case_of[name]] += value + return totals + + return { + "federal_person": by_household(FEDERAL, "person"), + "federal_tax_unit": by_household("income_tax_refundable_credits", "tax_unit"), + "gross": by_household(GROSS, "person"), + "credit": by_household(CREDIT, "person"), + "mt_other_credits": by_household( + "mt_refundable_credits_before_renter_credit", "person" + ), + "mt_income_tax": by_household("mt_income_tax", "tax_unit"), + } + + +def tolerance(amount): + return max(TOLERANCE, 8 * float(np.spacing(np.float32(abs(amount))))) + + +def assert_properties(cases): + w = calculate(cases, zero_federal_credits=False) + z = calculate(cases, zero_federal_credits=True) + for i, c in enumerate(cases): + tol = tolerance(w["gross"][i]) + federal = w["federal_tax_unit"][i] + # 1. Each tax unit's federal refundable credits count once. + assert w["federal_person"][i] == pytest.approx(federal, abs=tol), c + assert z["federal_person"][i] == 0, c + # 2. They are the whole difference in gross household income. + assert w["gross"][i] - z["gross"][i] == pytest.approx(federal, abs=tol), c + # 3. More income never raises the credit. + assert w["credit"][i] <= z["credit"][i] + tol, c + for result in (w, z): + # 4. Bounds and the income limit. + assert -tol <= result["credit"][i] <= CAP + tol, c + if result["gross"][i] >= INCOME_LIMIT + tol: + assert result["credit"][i] == 0, c + # 5. Montana tax moves only through this credit. + assert w["mt_other_credits"][i] == pytest.approx( + z["mt_other_credits"][i], abs=TOLERANCE + ), c + assert w["mt_income_tax"][i] - z["mt_income_tax"][i] == pytest.approx( + z["credit"][i] - w["credit"][i], abs=tolerance(z["credit"][i]) + ), c + + +def test_worked_example(): + """2024 Schedule 2EC: the claimant's line 5 pension 20,000; the + daughter's wages 10,000 (line 17) and her line 8 credits: federal EITC + 34% x 10,000 = 3,400, refundable child tax credit min(1,700, 15% x + 7,500) = 1,125 and Montana EITC 340. Line 18 = 34,865; line 22 = + (34,865 - 12,600) x 0.05 = 1,113.25; line 27 = 2,000 - 1,113.25 = + 886.75; line 30 = 886.75. Without the federal credits, line 18 = + 30,340, line 22 = 887 and line 30 = 1,113.""" + case = { + "claimant_age": 70, + "pension": 20_000, + "property_tax": 2_000, + "rent": 0, + "member_age": 30, + "wages": 10_000, + "child_ages": [5], + } + w = calculate([case], zero_federal_credits=False) + z = calculate([case], zero_federal_credits=True) + assert w["federal_tax_unit"][0] == pytest.approx(4_525, abs=TOLERANCE) + assert w["gross"][0] == pytest.approx(34_865, abs=TOLERANCE) + assert w["credit"][0] == pytest.approx(886.75, abs=TOLERANCE) + assert z["gross"][0] == pytest.approx(30_340, abs=TOLERANCE) + assert z["credit"][0] == pytest.approx(1_113, 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 two Simulations; 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)), + "property_tax": draw(st.integers(min_value=0, max_value=4_000)), + "rent": draw(amount(12_000)), + "member_age": draw(st.integers(min_value=19, max_value=61)), + "wages": draw(amount(30_000)), + "child_ages": draw( + st.lists(st.integers(min_value=0, max_value=16), max_size=2) + ), + } + + @hypothesis.settings(max_examples=10, deadline=None, suppress_health_check=SLOW) + @hypothesis.given(st.lists(case(), min_size=1, max_size=6)) + 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_federal_refundable_credits.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_federal_refundable_credits.py new file mode 100644 index 00000000000..25342808b61 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_federal_refundable_credits.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_federal_refundable_credits(Variable): + value_type = float + entity = Person + label = "Federal refundable credits in Montana elderly homeowner/renter credit gross household income" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # ARM 42.4.301(2)(b): "federal refundable tax credits received" + "https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/768c8344-24ee-4950-9d5e-9d8fc97ec0f7", + # 2023 instructions, line 7: "the federal and Montana Earned Income Tax + # Credit, the federal Child Tax Credits" + "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 8: "the federal and Montana earned income tax + # credits, the refundable portion of the federal child tax credit" + "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=47", + ) + + def formula(person, period, parameters): + # Federal refundable credits are claimed on the tax unit's return. + # Gross household income sums this person-level amount over the + # household, so count each return's credits once, on its head. + head = person("is_tax_unit_head", period) + return head * person.tax_unit("income_tax_refundable_credits", period) 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_rebate_received.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received.py new file mode 100644 index 00000000000..895bc87c5eb --- /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_rebate_received.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received(Variable): + value_type = float + entity = Person + label = "Montana property tax rebate received, in elderly homeowner/renter credit gross household income" + documentation = ( + "The Montana property tax rebate for the prior property tax year, " + "claimed from August 15 to October 1 of this year, allocated to the " + "tax unit head." + ) + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # 2024 Schedule 2EC, line 8: "the 2023 Montana property tax rebate" + "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=47", + # 2025 Schedule 2EC, line 8: "the 2024 Montana property tax rebate" + "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", + ) + + def formula(person, period, parameters): + # The rebate for tax year 2023 property taxes was claimed from + # August 15 to October 1, 2024, so it counts on the 2024 schedule. It + # is the rebate computed on last year's property taxes, which is 0 + # when the inputs cover only this year. + head = person("is_tax_unit_head", period) + return head * person.tax_unit("mt_property_tax_rebate", period.last_year) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_received.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_received.py new file mode 100644 index 00000000000..5bfbabeb8b0 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_received.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_received(Variable): + value_type = float + entity = Person + label = "Montana elderly homeowner/renter credit received" + documentation = ( + "Montana elderly homeowner/renter credit refunds received during the " + "year, normally the credit claimed on the prior year's return. It " + "counts in the current year's gross household income." + ) + unit = USD + definition_period = YEAR + reference = ( + # ARM 42.4.301(2)(c): "any state refundable tax credits received, + # including elderly homeowner/renter credit refunds" + "https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/768c8344-24ee-4950-9d5e-9d8fc97ec0f7", + # 2024 Schedule 2EC, line 8: "including your elderly homeowner/renter + # credit received in 2024" + "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=47", + ) + # An input rather than this credit read at period.last_year. This year's + # credit cannot count without a circular reference. Reading last year's + # credit would rerun the whole prior-year tax computation, every member's + # federal refundable credits included, for each record, and it reads 0 + # whenever the inputs cover only one year. From 6e42f36584b1f997b656493c1f211abdc9aaab6d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:57:00 -0400 Subject: [PATCH 07/11] Preserve dependent adjustments in Montana elderly credit income --- ...fix-mt-ehrc-dependent-adjustments.fixed.md | 1 + ...or_renter_credit_dependent_deductions.yaml | 195 +++++++++++++++++ ..._mt_elderly_credit_dependent_deductions.py | 203 ++++++++++++++++++ ...or_renter_credit_gross_household_income.py | 41 ++-- 4 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 changelog.d/fix-mt-ehrc-dependent-adjustments.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_dependent_deductions.yaml create mode 100644 policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py diff --git a/changelog.d/fix-mt-ehrc-dependent-adjustments.fixed.md b/changelog.d/fix-mt-ehrc-dependent-adjustments.fixed.md new file mode 100644 index 00000000000..b1437bd7a98 --- /dev/null +++ b/changelog.d/fix-mt-ehrc-dependent-adjustments.fixed.md @@ -0,0 +1 @@ +Montana elderly homeowner/renter credit household income includes dependents' own allowable non-loss adjustments to federal gross income. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_dependent_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_dependent_deductions.yaml new file mode 100644 index 00000000000..a0bd2ad43a7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_dependent_deductions.yaml @@ -0,0 +1,195 @@ +- name: Dependent Schedule C income keeps the dependent's half-SE-tax deduction + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + dependent: + age: 16 + is_tax_unit_dependent: true + self_employment_income: 15_000 + tax_units: + tax_unit: + members: [claimant, dependent] + households: + household: + members: [claimant, dependent] + state_name: MT + output: + # 2023 Schedule SE p.1: 15,000 * .9235 * .153 / 2 = 1,059.71625. + # 2023 MT instructions p.51, line 1: include each member's federal AGI. + # Dependent AGI without losses = 15,000 - 1,059.71625 = 13,940.28375; + # GHI = 20,000 + 13,940.28375 = 33,940.28375. + self_employment_tax_ald_person: [0, 1_059.71625] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 13_940.28375] + # 2,000 - (33,940.28375 - 12,600) * .05 = 932.9858125. + # GHI is below 35,000, so the credit multiplier is 100%. + mt_elderly_homeowner_or_renter_credit: [932.9858125, 0] + +- name: The statutory AGI definition also preserves dependent deductions in 2024 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + dependent: + age: 16 + is_tax_unit_dependent: true + self_employment_income: 15_000 + tax_units: + tax_unit: + members: [claimant, dependent] + households: + household: + members: [claimant, dependent] + state_name: MT + output: + # HB191 section 1(9), PDF p.2: federal AGI without losses remains the + # income definition. 15,000 - 15,000 * .9235 * .153 / 2 = 13,940.28375. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 13_940.28375] + # 2,000 - (20,000 + 13,940.28375 - 12,600) * .05 = 932.9858125. + mt_elderly_homeowner_or_renter_credit: [932.9858125, 0] + +- name: Own personal deductions count once and parental HSA deductions stay with the parent + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + mt_refundable_credits_before_renter_credit: 0 + dependent: + age: 16 + is_tax_unit_dependent: true + employment_income: 10_000 + self_employment_income: 15_000 + traditional_ira_contributions: 1_000 + early_withdrawal_penalty: 200 + self_employed_health_insurance_premiums: 500 + self_employed_pension_contributions: 600 + long_term_capital_gains: -3_000 + rental_income: -2_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [claimant, dependent] + health_savings_account_ald: 1_000 + income_tax_refundable_credits: 0 + households: + household: + members: [claimant, dependent] + state_name: MT + output: + # Parent AGI: 20,000 - own HSA deduction 1,000 = 19,000. + # Dependent: 10,000 + 15,000 - 1,059.71625 - 1,000 - 200 - 500 - 600 + # = 21,640.28375. Capital/rental losses do not reduce MT income. + # The parent's HSA deduction must not be subtracted from the child again. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [19_000, 21_640.28375] + +- name: Parent-owned exclusions recorded on a dependent are not deducted twice + period: 2024 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + mt_refundable_credits_before_renter_credit: 0 + dependent: + age: 16 + is_tax_unit_dependent: true + employment_income: 10_000 + qualified_adoption_assistance_expense: 500 + us_bonds_for_higher_ed: 700 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [claimant, dependent] + income_tax_refundable_credits: 0 + households: + household: + members: [claimant, dependent] + state_name: MT + output: + # The established filer_amounts_recorded_on_dependents convention puts + # 500 + 700 = 1,200 on the parent's return: 20,000 - 1,200 = 18,800. + # Neither is the dependent's own deduction: their income stays 10,000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [18_800, 10_000] + +- name: Dependent educator and IRA deductions retain their statutory annual caps + period: 2023 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + mt_refundable_credits_before_renter_credit: 0 + dependent: + age: 19 + is_tax_unit_dependent: true + employment_income: 10_000 + traditional_ira_contributions: 10_000 + educator_expense: 1_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [claimant, dependent] + income_tax_refundable_credits: 0 + households: + household: + members: [claimant, dependent] + state_name: MT + output: + # https://www.irs.gov/pub/irs-prior/p590a--2023.pdf#page=11: + # IRA deduction = min(10,000 contributions, 10,000 compensation, + # 6,500 annual limit) = 6,500; supplied actual excess is not deductible. + traditional_ira_deduction: [0, 6_500] + # https://www.irs.gov/pub/irs-prior/i1040gi--2023.pdf#page=89: + # Eligible educator deduction = min(1,000 expenses, 300 cap) = 300. + educator_expense_ald_person: [0, 300] + # Dependent AGI without losses = 10,000 - 6,500 - 300 = 3,200. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 3_200] + +- name: Own compensation caps the IRA deduction without flooring valid negative AGI + period: 2024 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + mt_refundable_credits_before_renter_credit: 0 + dependent: + age: 19 + is_tax_unit_dependent: true + employment_income: 2_000 + traditional_ira_contributions: 10_000 + educator_expense: 1_000 + mt_refundable_credits_before_renter_credit: 0 + tax_units: + tax_unit: + members: [claimant, dependent] + income_tax_refundable_credits: 0 + households: + household: + members: [claimant, dependent] + state_name: MT + output: + # Pub.590-A (2023) pp.2,11: 2024 annual limit is 7,000; compensation + # is only 2,000, so min(10,000, 2,000, 7,000) = 2,000 is deductible. + traditional_ira_deduction: [0, 2_000] + # The $300 educator deduction is valid separately from compensation: + # 2,000 - 2,000 - 300 = -300. This is an adjustment, not a loss. + educator_expense_ald_person: [0, 300] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, -300] diff --git a/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py b/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py new file mode 100644 index 00000000000..2ae61858b49 --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py @@ -0,0 +1,203 @@ +"""Own valid adjustments reduce Montana household income once, losses never do. + +2023 Form 2 instructions p.51 line 1 uses each household member's federal +AGI. HB191 section 1(9), PDF p.2, retains federal AGI without regard to loss. +These properties compare a dependent with a member on a separate return, +holding receipts fixed at zero to isolate their non-loss deductions. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings, strategies as st + +from policyengine_us import Simulation + + +def build_situation(cases, year): + people, tax_units, marital_units, spm_units, families, households = ( + {}, + {}, + {}, + {}, + {}, + {}, + ) + for index, case in enumerate(cases): + # A: dependent; B: separate filer; C: no losses; D: extra own penalty. + for arrangement in "ABCD": + key = f"{arrangement}{index}" + claimant, member = f"claimant_{key}", f"member_{key}" + people[claimant] = { + "age": {year: 70}, + "taxable_pension_income": {year: case["pension"]}, + "early_withdrawal_penalty": {year: case["parent_penalty"]}, + "is_tax_unit_dependent": {year: False}, + "mt_refundable_credits_before_renter_credit": {year: 0}, + "mt_elderly_homeowner_or_renter_credit_received": {year: 0}, + "mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received": { + year: 0 + }, + } + people[member] = { + "age": {year: 19}, + "is_tax_unit_dependent": {year: arrangement != "B"}, + "employment_income": {year: case["wages"]}, + "self_employment_income": {year: case["self_employment"]}, + "taxable_interest_income": {year: case["interest"]}, + "traditional_ira_contributions": {year: case["ira"]}, + "educator_expense": {year: case["educator"]}, + "early_withdrawal_penalty": { + year: case["penalty"] + + (case["extra_penalty"] if arrangement == "D" else 0) + }, + "self_employed_health_insurance_premiums": {year: case["health"]}, + "self_employed_pension_contributions": { + year: case["pension_contribution"] + }, + "long_term_capital_gains": { + year: -case["capital_loss"] if arrangement != "C" else 0 + }, + "rental_income": { + year: -case["rental_loss"] if arrangement != "C" else 0 + }, + "mt_refundable_credits_before_renter_credit": {year: 0}, + "mt_elderly_homeowner_or_renter_credit_received": {year: 0}, + "mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received": { + year: 0 + }, + } + tax_units[f"tax_unit_{key}"] = { + "members": [claimant] if arrangement == "B" else [claimant, member], + "health_savings_account_ald": {year: case["parent_hsa"]}, + "income_tax_refundable_credits": {year: 0}, + } + if arrangement == "B": + tax_units[f"member_tax_unit_{key}"] = { + "members": [member], + "health_savings_account_ald": {year: 0}, + "income_tax_refundable_credits": {year: 0}, + } + marital_units[f"claimant_marital_unit_{key}"] = {"members": [claimant]} + marital_units[f"member_marital_unit_{key}"] = {"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 assert_properties(cases, year): + simulation = Simulation(situation=build_situation(cases, year)) + income = np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit_gross_household_income", year + ) + ).reshape(len(cases), 4, 2) + for index, case in enumerate(cases): + # Schedule SE p.1: net earnings x .9235 x (.124 + .029) / 2. + # Generated positive profits exceed the $400 net-earnings threshold + # and remain below the Social Security wage base. + self_employment_deduction = case["self_employment"] * 0.9235 * 0.153 / 2 + # Pub.590-A (2023) p.11: the deduction cannot exceed contributions, + # compensation, or the annual limit ($6,500 in 2023; $7,000 in + # 2024–25). These single members' MAGI is below the phase-out start. + # Compensation excludes the half-SE-tax and SE retirement deduction. + compensation = case["wages"] + max( + 0, + case["self_employment"] + - self_employment_deduction + - case["pension_contribution"], + ) + ira_deduction = min(case["ira"], compensation, 6_500 if year == 2023 else 7_000) + # 2023 Form1040 instructions p.89: the per-educator cap is $300; + # the cap remains $300 in the other generated years. + educator_deduction = min(case["educator"], 300) + own_deductions = ( + self_employment_deduction + + case["health"] + + case["pension_contribution"] + + ira_deduction + + educator_deduction + + case["penalty"] + ) + parent = case["pension"] - case["parent_hsa"] - case["parent_penalty"] + member = ( + case["wages"] + case["self_employment"] + case["interest"] - own_deductions + ) + tol = max(0.01, 8 * float(np.spacing(np.float32(abs(parent + member))))) + # Each person's non-loss deductions are used once, on that person. + assert income[index, :, 0] == pytest.approx([parent] * 4, abs=tol) + assert income[index, :3, 1] == pytest.approx([member] * 3, abs=tol) + # The filing arrangement leaves total household income unchanged. + assert income[index, 0].sum() == pytest.approx(income[index, 1].sum(), abs=tol) + # Removing capital/rental losses never changes this statutory total. + assert income[index, 0].sum() == pytest.approx(income[index, 2].sum(), abs=tol) + # Another dollar of the dependent's valid penalty is deducted once. + assert income[index, 3, 1] == pytest.approx( + member - case["extra_penalty"], abs=tol + ) + + +def test_worked_deductions_and_losses(): + case = { + "pension": 20_000, + "parent_hsa": 1_000, + "parent_penalty": 50, + "wages": 10_000, + "self_employment": 15_000, + "interest": 500, + "ira": 1_000, + "educator": 100, + "penalty": 200, + "health": 500, + "pension_contribution": 600, + "capital_loss": 3_000, + "rental_loss": 2_000, + "extra_penalty": 300, + } + # Parent = 20,000 - 1,000 - 50 = 18,950. Member = 25,500 - 1,059.71625 + # - 1,000 - 100 - 200 - 500 - 600 = 22,040.28375; extra penalty -> + # 21,740.28375. Neither of the two losses affects these amounts. + assert_properties([case], 2023) + + +@st.composite +def case_strategy(draw): + return { + "pension": draw(st.integers(10_000, 35_000)), + "parent_hsa": draw(st.integers(0, 3_000)), + "parent_penalty": draw(st.integers(0, 500)), + "wages": draw(st.integers(10_000, 20_000)), + "self_employment": draw(st.integers(1_000, 15_000)), + "interest": draw(st.integers(0, 10_000)), + "ira": draw(st.integers(0, 10_000)), + "educator": draw(st.integers(0, 1_000)), + "penalty": draw(st.integers(0, 1_000)), + "health": draw(st.integers(0, 500)), + "pension_contribution": draw(st.integers(0, 500)), + "capital_loss": draw(st.integers(0, 20_000)), + "rental_loss": draw(st.integers(0, 20_000)), + "extra_penalty": draw(st.integers(1, 2_000)), + } + + +@settings( + max_examples=15, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + cases=st.lists(case_strategy(), min_size=1, max_size=4), + year=st.sampled_from([2023, 2024, 2025]), +) +def test_own_deductions_and_losses_are_independent_of_filing_arrangement(cases, year): + assert_properties(cases, year) 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..c6f1065035f 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): @@ -11,15 +14,15 @@ class mt_elderly_homeowner_or_renter_credit_gross_household_income(Variable): 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 - # (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 + # 2023 Form 2 instructions, PDF pages 51-52: each household member's + # return (line 1), nonfilers' wages (line 8), no losses (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=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", + # The statutory federal-AGI definition remains after the 2024 form + # began displaying separate income categories: HB191 § 1(9), p.2. + "https://archive.legmt.gov/bills/2021/HB0199/HB0191_X.pdf#page=2", ) def formula(person, period, parameters): @@ -27,14 +30,28 @@ def formula(person, period, parameters): period ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter sources = add(person, period, p.gross_income_sources) - # 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. + # Gross household income counts every member's federal AGI without + # losses (§ 15-30-2337(4), (9)(a)). The dependent's gross income is + # omitted from this return's AGI, but their own valid non-loss + # adjustments still belong on their own return (2023 instructions, + # line 1). Do not subtract the parent's TaxUnit-level deductions. + ald = parameters(period).gov.irs.ald + deduction_variables = [ + PERSON_ABOVE_THE_LINE_DEDUCTIONS[name] + for name in ald.deductions + if name in PERSON_ABOVE_THE_LINE_DEDUCTIONS + and name not in ald.filer_amounts_recorded_on_dependents + ] + # The shared mapping uses capped/eligible person-level deductions, + # including the dependent's own IRA deduction. It excludes loss_ald: + # business/capital losses never reduce Montana household income. + # Parent-owned adoption/bond exclusions recorded on dependents + # remain with the filer under the existing convention. + own_deductions = add(person, period, deduction_variables) is_dependent = person("is_tax_unit_dependent", period) - dependent_income = is_dependent * person("dependent_gross_income", period) + dependent_income = is_dependent * ( + person("dependent_gross_income", period) - own_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 fff85a5ef3411b0a87b197fc14aefc2ee2d8b31b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:57:53 -0400 Subject: [PATCH 08/11] Use actual Montana property tax rebate receipts in household income --- .../fix-mt-ehrc-refundable-credits.fixed.md | 2 +- .../gross_income_sources.yaml | 12 +- ...mt_elderly_homeowner_or_renter_credit.yaml | 5 +- ..._renter_credit_gross_household_income.yaml | 13 +- ...r_credit_property_tax_rebate_received.yaml | 43 ++++++- .../test_mt_elderly_credit_rebate_receipts.py | 117 ++++++++++++++++++ ...ter_credit_property_tax_rebate_received.py | 19 ++- 7 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py diff --git a/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md b/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md index be83e89c23e..67ba4eb92f0 100644 --- a/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md +++ b/changelog.d/fix-mt-ehrc-refundable-credits.fixed.md @@ -1 +1 @@ -Count federal refundable credits, the elderly homeowner/renter credit received during the year, and the prior year's property tax rebate in Montana's elderly homeowner/renter credit gross household income; date the Montana property tax rebate ($400 for tax year 2024, none after). +Count federal refundable credits, the elderly homeowner/renter credit received during the year, and actual property tax rebate receipts (including in 2023) in Montana's elderly homeowner/renter credit gross household income; date the Montana property tax rebate ($400 for tax year 2024, none after). 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 24a0d58c981..7c3c2184cdf 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 @@ -4,6 +4,8 @@ metadata: period: year label: Montana elderly homeowner or renter credit gross income sources reference: + - title: HB191 (2021), section 1(9), all nontaxable income + href: https://archive.legmt.gov/bills/2021/HB0199/HB0191_X.pdf#page=2 - title: Montana 2022 Form2 Individual Income Tax Instructions, Page47, Gross Household Income href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47 - title: 2022 Montana Code Annotated, Title 15, Chapter 30, Part 23, 15-30-2337 (9) @@ -61,11 +63,11 @@ values: - mt_refundable_credits_before_renter_credit - mt_elderly_homeowner_or_renter_credit_received - mt_elderly_homeowner_or_renter_credit_federal_refundable_credits - # 2024 Schedule 2EC line 8 adds "the 2023 Montana property tax rebate" - # (the 2025 schedule: "the 2024 Montana property tax rebate"), the rebate - # for the prior property tax year, claimed during this year. The 2023 - # schedule (line 7) does not list the rebate for 2022 property taxes. - 2024-01-01: + # Actual rebate receipts count under the statutory nontaxable-income + # definition and ARM 42.4.301(2)(c). The 2023 form's illustrative list + # establishes no exemption for 2022 rebates received in 2023. The + # 2024/2025 schedules explicitly name the prior property-year rebates. + 2023-01-01: - adjusted_gross_income_person - tax_exempt_interest_income - tax_exempt_ira_distributions 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 5f41944970c..472a0218dd0 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 @@ -523,6 +523,7 @@ 2024: 1_000 2025: 1_000 mt_elderly_homeowner_or_renter_credit_received: 1_150 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 400 tax_units: tax_unit: members: [person1] @@ -573,8 +574,8 @@ # refundable credits received by the members of the household: the # son's federal EITC, Rev. Proc. 2022-38, no qualifying child, # 600 - 7.65% x (10_000 - 9_800) = 584.70, and Montana EITC 3%, 17.54; - # 602.24 in all. The 675 rebate for 2022 property taxes is not on the - # 2023 line 7 list. Line 10 = 30_602.24; line 12 = 18_002.24; line 13 = + # 602.24 in all. No actual property-rebate receipt is supplied, so + # none counts. Line 10 = 30_602.24; line 12 = 18_002.24; line 13 = # 0.05; line 14 = 900.11; line 19 = 2_000 - 900.11 = 1_099.89; line 20 # = 1_099.89; line 21 = 100%; line 22 = 1_099.89. mt_elderly_homeowner_or_renter_credit_net_household_income: [900.11, 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 cd19d78fdb1..ec00f8c2fad 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 @@ -250,6 +250,7 @@ real_estate_taxes: 2023: 2_000 2024: 2_000 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 675 tax_units: tax_unit: members: [person1] @@ -264,7 +265,7 @@ # min(675, 2_000) = 675. 20_000 + 675. mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_675 -- name: The rebate for 2022 property taxes does not count on the 2023 schedule +- name: Actual rebate cash received in 2023 counts under the broad income definition period: 2023 input: people: @@ -274,6 +275,7 @@ real_estate_taxes: 2022: 2_000 2023: 2_000 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 675 tax_units: tax_unit: members: [person1] @@ -284,12 +286,11 @@ 2022: MT 2023: MT output: - # The 2023 instructions' line 7 list (federal and Montana EITC, federal - # child tax credits, unlocking public land credit, elderly - # homeowner/renter credit) has no property tax rebate, though the 675 - # rebate for 2022 property taxes was claimed in August-October 2023. + # HB191 section1(9), PDFp2: all nontaxable income; ARM42.4.301(2)(c): + # state refundable credits received. The illustrative 2023 list does + # not exempt the rebate. Pension 20_000 + actual cash 675 = 20_675. mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 675 - mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_675 - name: Federal refundable credits count from 2017 period: 2017 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_rebate_received.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_rebate_received.yaml index 38304aebb07..dc2ddf161cf 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_rebate_received.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_rebate_received.yaml @@ -1,4 +1,4 @@ -- name: Case 1, the 2023 rebate is received in 2024 and goes to the head. +- name: Case 1, the head reports actual rebate cash received in 2024. period: 2024 input: people: @@ -7,6 +7,7 @@ real_estate_taxes: 2023: 2_000 2024: 2_100 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 675 person2: age: 68 tax_units: @@ -23,7 +24,7 @@ # min(675, 2_000) = 675. mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: [675, 0] -- name: Case 2, the 2023 rebate is the 2023 property tax when that is less than 675. +- name: Case 2, the person reports a smaller actual rebate receipt. period: 2024 input: people: @@ -32,6 +33,7 @@ real_estate_taxes: 2023: 500 2024: 900 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 500 tax_units: tax_unit: members: [person1] @@ -53,6 +55,7 @@ real_estate_taxes: 2024: 2_000 2025: 2_000 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 400 tax_units: tax_unit: members: [person1] @@ -67,7 +70,7 @@ # Ch. 767, L. 2025, sec. 3(1): $400 or the property taxes paid, if less. mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 400 -- name: Case 4, there is no rebate for 2025 property taxes to receive in 2026. +- name: Case 4, no actual cash receipt reported in 2026 defaults to zero. period: 2026 input: people: @@ -88,7 +91,7 @@ output: mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 0 -- name: Case 5, zero when only this year's property tax is known. +- name: Case 5, no reported actual receipt defaults to zero. period: 2024 input: people: @@ -103,6 +106,34 @@ members: [person1] state_code: MT output: - # Inputs never carry backwards, so last year's property tax and rebate - # are 0. + # No actual cash receipt was reported; default zero. mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 0 + +- name: Case 6, a prior tax bill does not establish rebate eligibility or receipt. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: + 2024: 1_000 + 2025: 1_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: + 2024: MT + 2025: MT + output: + # Reviewer: six months owning/occupying in 2024 fails SB542 section1(3), + # PDFp2 (minimum seven months); section3(5), PDFp3 also requires a claim. + # No actual receipt: 1_000 - (20_000 - 12_600) * .035 = 741. + # Inferring 400 from the old tax bill incorrectly gave 727. + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 0 + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 + mt_elderly_homeowner_or_renter_credit: 741 diff --git a/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py b/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py new file mode 100644 index 00000000000..9f44d62671b --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py @@ -0,0 +1,117 @@ +"""Actual cash receipts enter Montana household income once, without inference. + +SB542 sections 1(3),3(5) require qualifying occupancy and a claim, so a prior +property-tax bill alone cannot establish a receipt. Statutory gross household +income includes all nontaxable income (HB191 section1(9), PDFp2), including +rebate cash in2023 despite omission from that year's illustrative form list. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings, strategies as st + +from policyengine_us import Simulation + + +def build_situation(cases, year, with_receipts): + people, tax_units, households = {}, {}, {} + for i, c in enumerate(cases): + # No receipt input is supplied anywhere in A/B's simulation. Core + # defaults omitted person rows once any row supplies an input, so C + # must be in another simulation to exercise absence of that input. + for arrangement in "C" if with_receipts else "AB": + key = f"{arrangement}{i}" + members = [f"claimant{key}", f"member{key}"] + for j, name in enumerate(members): + people[name] = { + "age": {year: 70 if j == 0 else 40}, + "taxable_pension_income": {year: c["pension"] if j == 0 else 0}, + "is_tax_unit_dependent": {year: False}, + "real_estate_taxes": { + year: c["property_tax"] if j == 0 else 0, + year - 1: c["prior_tax"] + if arrangement == "A" and j == 0 + else 0, + }, + "mt_refundable_credits_before_renter_credit": {year: 0}, + "mt_elderly_homeowner_or_renter_credit_received": {year: 0}, + } + if arrangement == "C": + people[name][ + "mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received" + ] = {year: c["receipts"][j]} + tax_units[f"{name}_return"] = { + "members": [name], + "income_tax_refundable_credits": {year: 0}, + } + households[f"household{key}"] = { + "members": members, + "state_code": {year: "MT", year - 1: "MT"}, + } + return {"people": people, "tax_units": tax_units, "households": households} + + +def calculate(cases, year, with_receipts): + sim = Simulation(situation=build_situation(cases, year, with_receipts)) + shape = (len(cases), 1 if with_receipts else 2) + return { + name: np.asarray(sim.calculate(variable, year, map_to="household")).reshape( + shape + ) + for name, variable in { + "income": "mt_elderly_homeowner_or_renter_credit_gross_household_income", + "credit": "mt_elderly_homeowner_or_renter_credit", + "receipts": "mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received", + }.items() + } + + +def assert_receipts(cases, year): + absent = calculate(cases, year, with_receipts=False) + supplied = calculate(cases, year, with_receipts=True) + for i, c in enumerate(cases): + cash = sum(c["receipts"]) + assert absent["receipts"][i] == pytest.approx([0, 0], abs=0.01) + assert supplied["receipts"][i, 0] == pytest.approx(cash, abs=0.01) + assert absent["income"][i] == pytest.approx([c["pension"]] * 2, abs=0.01) + assert supplied["income"][i, 0] == pytest.approx(c["pension"] + cash, abs=0.01) + assert absent["credit"][i, 0] == pytest.approx(absent["credit"][i, 1], abs=0.01) + assert supplied["credit"][i, 0] <= absent["credit"][i, 0] + 0.01 + return absent["credit"], supplied["credit"] + + +def test_reviewer_unreceived_rebate(): + # No receipt: 1000-(20000-12600)*.035=741. The old assumed400 gave727. + absent, supplied = assert_receipts( + [ + { + "pension": 20000, + "property_tax": 1000, + "prior_tax": 1000, + "receipts": [400, 0], + } + ], + 2025, + ) + assert absent[0] == pytest.approx([741, 741], abs=0.01) + assert supplied[0, 0] == pytest.approx(727, abs=0.01) + + +@settings(max_examples=10, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given( + cases=st.lists( + st.fixed_dictionaries( + { + "pension": st.integers(0, 45000), + "property_tax": st.integers(0, 4000), + "prior_tax": st.integers(0, 4000), + "receipts": st.lists(st.integers(0, 675), min_size=2, max_size=2), + } + ), + min_size=1, + max_size=4, + ), + year=st.sampled_from([2023, 2024, 2025]), +) +def test_actual_receipts_count_once_and_prior_tax_does_not_imply_receipt(cases, year): + assert_receipts(cases, year) 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_rebate_received.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received.py index 895bc87c5eb..d359822c279 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received.py @@ -6,24 +6,23 @@ class mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received(Variabl entity = Person label = "Montana property tax rebate received, in elderly homeowner/renter credit gross household income" documentation = ( - "The Montana property tax rebate for the prior property tax year, " - "claimed from August 15 to October 1 of this year, allocated to the " - "tax unit head." + "Montana property tax rebate cash actually received by this person " + "during the year. Defaults to zero: prior-year property taxes or a " + "calculated entitlement do not establish eligibility, a claim, or receipt." ) unit = USD definition_period = YEAR defined_for = StateCode.MT reference = ( + # SB542 eligibility and claim requirements, PDF pages 2-3. + "https://archive.legmt.gov/content/Sessions/69th/Contractor_index/CH0767.pdf#page=2", + "https://archive.legmt.gov/bills/2021/HB0199/HB0191_X.pdf#page=2", # 2024 Schedule 2EC, line 8: "the 2023 Montana property tax rebate" "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=47", # 2025 Schedule 2EC, line 8: "the 2024 Montana property tax rebate" "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", ) - def formula(person, period, parameters): - # The rebate for tax year 2023 property taxes was claimed from - # August 15 to October 1, 2024, so it counts on the 2024 schedule. It - # is the rebate computed on last year's property taxes, which is 0 - # when the inputs cover only this year. - head = person("is_tax_unit_head", period) - return head * person.tax_unit("mt_property_tax_rebate", period.last_year) + # Actual receipt input: SB542 sections 1(3) and 3(5) require qualifying + # ownership/occupancy, paid taxes, and a claim. The model's separate + # mt_property_tax_rebate amount calculation omits those requirements. From b14afe7e4fbdb5c24d163e297b6ff72da8ce7152 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:58:12 -0400 Subject: [PATCH 09/11] Cover Montana allocation invariants and childcare itemization boundary --- ...ix-mt-federal-tax-zero-allocation.fixed.md | 1 + .../mt_childcare_itemization_boundary.yaml | 88 ++++++++ ...mt_federal_income_tax_deduction_indiv.yaml | 32 ++- ...federal_income_tax_deduction_allocation.py | 199 ++++++++++++++++++ .../mt_federal_income_tax_deduction_share.py | 8 +- 5 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fix-mt-federal-tax-zero-allocation.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml create mode 100644 policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py diff --git a/changelog.d/fix-mt-federal-tax-zero-allocation.fixed.md b/changelog.d/fix-mt-federal-tax-zero-allocation.fixed.md new file mode 100644 index 00000000000..ca01c310d03 --- /dev/null +++ b/changelog.d/fix-mt-federal-tax-zero-allocation.fixed.md @@ -0,0 +1 @@ +Preserve zero Montana federal income tax deduction allocation when neither filer has positive federal adjusted gross income. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml new file mode 100644 index 00000000000..f6f0e87b609 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml @@ -0,0 +1,88 @@ +# 2022 Form 2441-M, lines 2-7 and separate-spouse allocation (PDF p. 1): +# 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 +# Form 2, Itemized Deductions Schedule lines 8 and 14, and Standard Deduction +# Worksheet lines 1-6 (PDF p. 7): +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7 +# Instructions for Form 2 line 15 require both spouses to use the same method +# (PDF p. 16); the model retains the standard deduction when totals tie: +# 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=16 + +- name: Combined-AGI care deduction leaves standard deductions at the itemization boundary + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_tax_unit_dependent: false + mt_agi_indiv: 20_000 + home_mortgage_interest: 4_860 + spouse: + age: 39 + is_tax_unit_dependent: false + mt_agi_indiv: 0 + child: + age: 5 + is_tax_unit_dependent: true + mt_agi_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse, child] + filing_status: JOINT + state_filing_status_if_married_filing_separately_on_same_return: SEPARATE + tax_unit_childcare_expenses: 3_000 + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + # Care = min(3_000, 2_400) - 0.5 * (20_000 + 0 - 18_000) + # = 1_400; each spouse gets 700. Itemized total = 4_860 + 700 + 700 + # = 6_260. Standard = max(20% * 20_000, 2_260) + 2_260 = 6_260. + # The unchanged tie rule selects standard deductions. Phasing out on + # each spouse's own AGI would incorrectly give 700 + 1_200 care and + # itemized total 6_760, crossing the choice boundary. + mt_child_dependent_care_expense_deduction: [700, 700, 0] + mt_itemized_deductions_indiv: [5_560, 700, 0] + mt_standard_deduction_indiv: [4_000, 2_260, 0] + mt_tax_unit_itemizes: false + mt_deductions_indiv: [4_000, 2_260, 0] + +- name: One more dollar of mortgage interest crosses the derived childcare itemization boundary + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_tax_unit_dependent: false + mt_agi_indiv: 20_000 + home_mortgage_interest: 4_861 + spouse: + age: 39 + is_tax_unit_dependent: false + mt_agi_indiv: 0 + child: + age: 5 + is_tax_unit_dependent: true + mt_agi_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse, child] + filing_status: JOINT + state_filing_status_if_married_filing_separately_on_same_return: SEPARATE + tax_unit_childcare_expenses: 3_000 + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + # The same 1_400 care deduction gives itemized total + # 4_861 + 700 + 700 = 6_261, exceeding the unchanged 6_260 standard. + mt_child_dependent_care_expense_deduction: [700, 700, 0] + mt_itemized_deductions_indiv: [5_561, 700, 0] + mt_standard_deduction_indiv: [4_000, 2_260, 0] + mt_tax_unit_itemizes: true + mt_deductions_indiv: [5_561, 700, 0] 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 c7604752c4d..a6633222517 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 @@ -86,7 +86,7 @@ adjusted_gross_income_person: 0 person3: is_tax_unit_dependent: true - adjusted_gross_income_person: 0 + adjusted_gross_income_person: 10_000 tax_units: tax_unit: members: [person1, person2, person3] @@ -97,8 +97,38 @@ members: [person1, person2, person3] state_code: MT output: + # Only the parents' 40_000 + 0 AGI allocates their federal tax: + # 2_000 * 40_000 / 40_000 = 2_000 for the head. Including the + # dependent's 10_000 would incorrectly give 1_600 / 0 / 400. + mt_federal_income_tax_deduction_share: [1, 0, 0] mt_federal_income_tax_deduction_indiv: [2_000, 0, 0] +- name: No positive filer income retains main's zero federal tax allocation + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + adjusted_gross_income_person: 0 + person2: + adjusted_gross_income_person: 0 + 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: + # Form 2 line 4a (source cited above) does not specify how to allocate + # federal tax when neither spouse has income. Preserve main's zero + # allocation: 6_000 * 0 = 0 for each spouse. Assigning all tax to the head + # would invent a new fallback and produce min(6_000, 5_000) = 5_000. + mt_federal_income_tax_deduction_share: [0, 0] + mt_federal_income_tax_deduction_indiv: [0, 0] + - name: The deduction is repealed from 2024 period: 2024 input: diff --git a/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py b/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py new file mode 100644 index 00000000000..00b3c3ba12e --- /dev/null +++ b/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py @@ -0,0 +1,199 @@ +"""Invariants of Montana's separate-spouse federal tax allocation. + +The 2022 Form 2 instructions, Itemized Deductions Schedule line 4a (PDF +page 33), attribute federal withholding to the spouse earning the income; +line 4 limits each separately filing spouse's deduction to $5,000: +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 + +The model's positive federal-AGI shares approximate spouse-specific payments +because withholding inputs are unavailable. The form does not prescribe an +allocation when neither filer has positive income; the model retains main's +zero allocation in that case rather than inventing a head-only fallback. For +every eligible tax unit, the method must: + +1. With positive filer AGI, allocate all of the couple's federal tax across + head and spouse before each spouse's cap, without any dependent share. +2. Treat negative filer AGI identically to zero AGI and disregard changes + to dependent AGI. +3. Allocate no tax when neither filer has positive AGI, as main does when + its allocation denominator is nonpositive. +4. Keep deductions nonnegative and at most $5,000 per person; conserve + nonnegative federal tax when filer AGI is positive and federal tax is at + most $5,000, so no cap can bind. + +Each generated case is simulated alongside clipped-income, changed-dependent, +and zero-filer-income variants. Comparisons allow one cent for float32 tax +amounts and eight float32 spacings for dimensionless shares. +""" + +import hypothesis +import hypothesis.strategies as st +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEAR = 2022 +CAP = 5_000 +SHARE_TOLERANCE = 8 * np.finfo(np.float32).eps +AMOUNT_TOLERANCE = 0.01 +ARRANGEMENTS = ("original", "clipped", "dependent", "zero") + + +def build_situation(cases): + people, tax_units, households = {}, {}, {} + for i, case in enumerate(cases): + for arrangement in ARRANGEMENTS: + key = f"{arrangement}_{i}" + head, spouse, dependent = ( + f"head_{key}", + f"spouse_{key}", + f"dependent_{key}", + ) + filer_incomes = [case["head_agi"], case["spouse_agi"]] + if arrangement == "clipped": + filer_incomes = [max(income, 0) for income in filer_incomes] + elif arrangement == "zero": + filer_incomes = [0, 0] + dependent_income = case["dependent_agi"] + if arrangement in ("dependent", "zero"): + dependent_income = abs(dependent_income) + 25_000 + for name, age, income, is_dependent in zip( + (head, spouse, dependent), + (50, 49, 16), + (*filer_incomes, dependent_income), + (False, False, True), + ): + people[name] = { + "age": {YEAR: age}, + "adjusted_gross_income_person": {YEAR: income}, + "is_tax_unit_dependent": {YEAR: is_dependent}, + } + tax_units[f"tax_unit_{key}"] = { + "members": [head, spouse, dependent], + "income_tax_before_refundable_credits": {YEAR: case["federal_tax"]}, + "state_filing_status_if_married_filing_separately_on_same_return": { + YEAR: "SEPARATE" + }, + } + households[f"household_{key}"] = { + "members": [head, spouse, dependent], + "state_code": {YEAR: "MT"}, + } + return {"people": people, "tax_units": tax_units, "households": households} + + +def calculate(cases): + simulation = Simulation(situation=build_situation(cases)) + shape = (len(cases), len(ARRANGEMENTS), 3) + shares = np.asarray( + simulation.calculate("mt_federal_income_tax_deduction_share", YEAR) + ).reshape(shape) + deductions = np.asarray( + simulation.calculate("mt_federal_income_tax_deduction_indiv", YEAR) + ).reshape(shape) + return shares, deductions + + +def assert_properties(cases): + shares, deductions = calculate(cases) + for i, case in enumerate(cases): + case_shares, case_deductions = shares[i], deductions[i] + # Each array is indexed [arrangement, head/spouse/dependent]. + has_positive_filer_income = case["head_agi"] > 0 or case["spouse_agi"] > 0 + assert np.all(case_shares >= 0), case + assert np.all(case_shares <= 1), case + assert case_shares.sum(axis=1) == pytest.approx( + [int(has_positive_filer_income)] * 3 + [0], abs=SHARE_TOLERANCE + ), case + assert np.all(case_shares[:, 2] == 0), case + # Negative income and dependent income cannot shift tax to or away + # from either filer. + for arrangement in (1, 2): + assert case_shares[arrangement] == pytest.approx( + case_shares[0], abs=SHARE_TOLERANCE + ), case + assert case_deductions[arrangement] == pytest.approx( + case_deductions[0], abs=AMOUNT_TOLERANCE + ), case + # The source is silent on zero filer income; preserve main's zero + # allocation, even with a positive-income dependent. + assert case_shares[3] == pytest.approx([0, 0, 0], abs=SHARE_TOLERANCE), case + federal_tax = max(case["federal_tax"], 0) + assert case_deductions[3] == pytest.approx([0, 0, 0], abs=AMOUNT_TOLERANCE), ( + case + ) + assert np.all(case_deductions >= 0), case + assert np.all(case_deductions <= CAP), case + assert np.all(case_deductions[:, 2] == 0), case + if not has_positive_filer_income: + assert np.all(case_deductions == 0), case + elif federal_tax <= CAP: + assert case_deductions[:3].sum(axis=1) == pytest.approx( + [federal_tax] * 3, abs=AMOUNT_TOLERANCE + ), case + + +def test_worked_allocations(): + cases = [ + # 2_000 * 40_000 / (40_000 + 0) = 2_000; the dependent's + # 10_000 is excluded. An unmasked allocator gives 1_600 / 0 / 400. + { + "head_agi": 40_000, + "spouse_agi": 0, + "dependent_agi": 10_000, + "federal_tax": 2_000, + }, + # Negative head AGI contributes zero; all 2_000 goes to the spouse. + { + "head_agi": -5_000, + "spouse_agi": 10_000, + "dependent_agi": 25_000, + "federal_tax": 2_000, + }, + # Neither filer has positive AGI: main's zero allocation is retained. + { + "head_agi": 0, + "spouse_agi": 0, + "dependent_agi": 10_000, + "federal_tax": 2_000, + }, + { + "head_agi": -5_000, + "spouse_agi": -2_000, + "dependent_agi": 10_000, + "federal_tax": 2_000, + }, + ] + shares, deductions = calculate(cases) + assert shares[:, 0] == pytest.approx( + np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0], [0, 0, 0]]), + abs=SHARE_TOLERANCE, + ) + assert deductions[:, 0] == pytest.approx( + np.array([[2_000, 0, 0], [0, 2_000, 0], [0, 0, 0], [0, 0, 0]]), + abs=AMOUNT_TOLERANCE, + ) + + +@hypothesis.settings( + max_examples=10, + deadline=None, + suppress_health_check=[hypothesis.HealthCheck.too_slow], +) +@hypothesis.given( + st.lists( + st.fixed_dictionaries( + { + "head_agi": st.integers(min_value=-100_000, max_value=100_000), + "spouse_agi": st.integers(min_value=-100_000, max_value=100_000), + "dependent_agi": st.integers(min_value=-100_000, max_value=100_000), + "federal_tax": st.integers(min_value=-5_000, max_value=30_000), + } + ), + min_size=1, + max_size=6, + ) +) +def test_allocation_properties(cases): + assert_properties(cases) 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 index 2c6dfd8556b..a2d93c5cc6f 100644 --- 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 @@ -19,8 +19,9 @@ class mt_federal_income_tax_deduction_share(Variable): 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. + # income is on the dependent's own return. With no positive filer + # income, retain main's zero allocation; the instructions do not + # establish which spouse made the payments in that case. head_or_spouse = person("is_tax_unit_head_or_spouse", period) income = head_or_spouse * max_( person("adjusted_gross_income_person", period), 0 @@ -29,5 +30,4 @@ def formula(person, period, parameters): 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) + return share From 6aa5439baae83084e3ba486f966babcf422cf2a5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 14:29:55 -0400 Subject: [PATCH 10/11] Keep generated dependent adjustments below federal contribution limits --- .../tests/test_mt_elderly_credit_dependent_deductions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py b/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py index 2ae61858b49..90736451eca 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py +++ b/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py @@ -172,12 +172,18 @@ def test_worked_deductions_and_losses(): @st.composite def case_strategy(draw): + # Keep health/pension limits nonbinding, including after half-SE-tax: + # at the minimum $5,000 profit, half-SE-tax = $353.23875. With pension + # $500, Form7206 lines7-10,14 allow health up to $4,146.76125; the + # SEP 20% ceiling is ($5,000 - $353.23875) * .2 = $929.35225. + # https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1 + # https://www.irs.gov/pub/irs-prior/p560--2023.pdf#page=35 (PDFpp35-36) return { "pension": draw(st.integers(10_000, 35_000)), "parent_hsa": draw(st.integers(0, 3_000)), "parent_penalty": draw(st.integers(0, 500)), "wages": draw(st.integers(10_000, 20_000)), - "self_employment": draw(st.integers(1_000, 15_000)), + "self_employment": draw(st.integers(5_000, 15_000)), "interest": draw(st.integers(0, 10_000)), "ira": draw(st.integers(0, 10_000)), "educator": draw(st.integers(0, 1_000)), From e212a4104f060cba0155a82a5b3eb9d150d11290 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 17:00:24 -0400 Subject: [PATCH 11/11] Test Montana tax conservation whenever individual caps do not bind --- ...federal_income_tax_deduction_allocation.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py b/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py index 00b3c3ba12e..6f26c9dfea1 100644 --- a/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py +++ b/policyengine_us/tests/test_mt_federal_income_tax_deduction_allocation.py @@ -18,8 +18,8 @@ 3. Allocate no tax when neither filer has positive AGI, as main does when its allocation denominator is nonpositive. 4. Keep deductions nonnegative and at most $5,000 per person; conserve - nonnegative federal tax when filer AGI is positive and federal tax is at - most $5,000, so no cap can bind. + nonnegative federal tax when filer AGI is positive and each spouse's + uncapped deduction is at most $5,000. Each generated case is simulated alongside clipped-income, changed-dependent, and zero-filer-income variants. Comparisons allow one cent for float32 tax @@ -128,7 +128,7 @@ def assert_properties(cases): assert np.all(case_deductions[:, 2] == 0), case if not has_positive_filer_income: assert np.all(case_deductions == 0), case - elif federal_tax <= CAP: + elif np.all(case_shares[:3] * federal_tax <= CAP): assert case_deductions[:3].sum(axis=1) == pytest.approx( [federal_tax] * 3, abs=AMOUNT_TOLERANCE ), case @@ -164,14 +164,24 @@ def test_worked_allocations(): "dependent_agi": 10_000, "federal_tax": 2_000, }, + # 9_000 * 40_000 / (40_000 + 40_000) = 4_500 per spouse. + # Neither individual cap binds, so the full 9_000 is conserved. + { + "head_agi": 40_000, + "spouse_agi": 40_000, + "dependent_agi": 10_000, + "federal_tax": 9_000, + }, ] shares, deductions = calculate(cases) assert shares[:, 0] == pytest.approx( - np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0], [0, 0, 0]]), + np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0], [0, 0, 0], [0.5, 0.5, 0]]), abs=SHARE_TOLERANCE, ) assert deductions[:, 0] == pytest.approx( - np.array([[2_000, 0, 0], [0, 2_000, 0], [0, 0, 0], [0, 0, 0]]), + np.array( + [[2_000, 0, 0], [0, 2_000, 0], [0, 0, 0], [0, 0, 0], [4_500, 4_500, 0]] + ), abs=AMOUNT_TOLERANCE, )