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..c7e8d0d90f9 --- /dev/null +++ b/changelog.d/fix-mt-ehrc-household-member-income.fixed.md @@ -0,0 +1,2 @@ +Count household members' income in the Montana elderly homeowner/renter credit, preserving dependents' permitted person-level federal AGI adjustments and excluding losses, as well as including the income of members who file their own returns. +Apply the full credit multiplier when permitted adjustments make household income negative. diff --git a/changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md b/changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md new file mode 100644 index 00000000000..2f6c0989d0b --- /dev/null +++ b/changelog.d/fix-mt-ehrc-one-claimant-per-household.fixed.md @@ -0,0 +1 @@ +Limit the Montana elderly homeowner/renter credit to one claimant per household (MCA 15-30-2341(1)): when several tax units in a household qualify, only the one with the largest credit is paid. diff --git a/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md b/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md new file mode 100644 index 00000000000..749992a2659 --- /dev/null +++ b/changelog.d/fix-mt-ehrc-property-in-others-name.fixed.md @@ -0,0 +1 @@ +Count the property tax billed for a Montana elderly homeowner/renter credit claimant's home that is wholly in another household member's name, a dependent's included, as rent (ARM 42.4.302(2)(b)). diff --git a/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/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 472a0218dd0..13486feebea 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -11,6 +11,10 @@ tax_units: tax_unit: members: [person1] + households: + household: + members: [person1] + state_code: MT output: mt_elderly_homeowner_or_renter_credit: 0 @@ -33,6 +37,10 @@ tax_units: tax_unit: members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT output: # min_((propertytax + rent*rate - (grossincome-standard)*reduction_rate), 1150) * percentage(grossincome) # 1500+ 9000*2*0.15 - (34000-12600)*0.05 = 3130 # 1150*100% @@ -57,6 +65,10 @@ tax_units: tax_unit: members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT output: # min_((propertytax + rent*rate - (grossincome-standard)*reduction_rate), 1000) * percentage(grossincome) # 1500+ 9000*2*0.15 - (34000-6300)*0.05 = 2815 # 1000*100% @@ -87,6 +99,10 @@ tax_units: tax_unit: members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT output: # min_((propertytax + rent*rate - (grossincome-standard)*reduction_rate), 1150) * percentage(grossincome) # 1000+ 9000*0.15 - (43000-12600)*0.05 = 830 # 830*10% = 83 @@ -111,6 +127,10 @@ tax_units: tax_unit: members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT output: # Each person's gross household income is under the $35,000 threshold # (multiplier 1.0 per-person), but the tax-unit sum is $44,000 which @@ -130,6 +150,10 @@ tax_units: tax_unit: members: [person1] + households: + household: + members: [person1] + state_code: MT output: mt_elderly_homeowner_or_renter_credit: 0 @@ -408,6 +432,201 @@ mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] mt_elderly_homeowner_or_renter_credit: [1_130, 0] +- name: 2024 Schedule 2EC, two eligible siblings filing separately in one household; only the larger claim is paid + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 4_800 + person2: + age: 70 + taxable_pension_income: 15_000 + rent: 7_200 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Both siblings are 62 or older and file their own returns. Line 18 + # gross household income = 10_000 + 15_000 = 25_000 for each; line 20 = + # 25_000 - 12_600 = 12_400; line 21 = 0.05; line 22 = 620. + # person1: line 25 = 4_800 x 0.15 = 720; line 27 = 720 - 620 = 100. + # person2: line 25 = 7_200 x 0.15 = 1_080; line 27 = 1_080 - 620 = 460. + # Line 29 = 100% (under 35_000). "Only one claimant per household in a + # claim period ... is entitled to relief" (§ 15-30-2341(1)); the + # household claims through person2, whose credit is larger. Before the + # limit both were paid, 560 in total. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [100, 460] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + mt_elderly_homeowner_or_renter_credit: [0, 460] + mt_refundable_credits: [0, 460] + taxsim_state_property_tax_credit: [0, 460] + # 2024 Form 2, PDF p. 1: federal standard deduction 16,550 plus + # old-age subtraction 5,500 exceeds either pension. Tax before + # refundable credits is zero, so only person2 receives a refund. + mt_income_tax: [0, -460] + +- name: 2024 Schedule 2EC, one eligible tax unit and one younger household member who files separately + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 10_000 + rent: 4_800 + person2: + age: 58 + taxable_pension_income: 15_000 + rent: 7_200 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Same household as above, but person2 is under 62 and cannot be a + # claimant (§ 15-30-2338(1)(a)), even though their rent is larger. + # Line 22 = 620; person1's line 27 = 720 - 620 = 100. + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit: [100, 0] + +- name: 2025 Schedule 2EC, a joint couple who both qualify make one claim + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 15_000 + real_estate_taxes: 2_000 + person2: + age: 66 + taxable_pension_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # "If filing jointly and both spouses qualify for the credit, enter the + # spouse listed as the taxpayer on page 1 as the claimant" (2025 + # instructions). Line 18 = 25_000; line 22 = 12_400 x 0.05 = 620; + # line 27 = 2_000 - 620 = 1_380; line 28 = 1_150 cap; line 30 = 1_150, + # held by the tax unit head. + mt_elderly_homeowner_or_renter_credit_selected_claimant: true + mt_elderly_homeowner_or_renter_credit: [1_150, 0] + +- name: 2024 Schedule 2EC, two eligible tax units with equal credits; exactly one claims + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 68 + taxable_pension_income: 12_500 + rent: 6_000 + person2: + age: 72 + taxable_pension_income: 12_500 + rent: 6_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 25_000; line 22 = 620; each line 27 = 900 - 620 = 280. The + # household gets one 280 credit; the tie goes to the first tax unit in + # the household's member order. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [280, 280] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit: [280, 0] + +- name: 2024 Schedule 2EC, the one-claimant limit applies within each household, not across households + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 75 + taxable_pension_income: 10_000 + rent: 6_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household1: + members: [person1] + state_name: MT + household2: + members: [person2] + state_name: MT + output: + # Household 1: line 18 = 20_000; line 20 = 7_400; line 21 = 0.035; + # line 22 = 259; line 27 = 2_000 - 259 = 1_741; line 28 = 1_150. + # Household 2: line 18 = 10_000; line 20 = 0; line 22 = 0; + # line 27 = 6_000 x 0.15 = 900. Each household has its own claimant. + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, true] + mt_elderly_homeowner_or_renter_credit: [1_150, 900] + +- name: 2023 dependent self-employment income preserves the deductible half of SE tax + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_tax_unit_dependent: false + taxable_pension_income: 20_000 + rent: 12_000 + child: + age: 18 + is_tax_unit_dependent: true + self_employment_income: 10_000 + tax_units: + claimant_unit: + members: [claimant, child] + households: + household: + members: [claimant, child] + state_code: MT + output: + # 2023 Schedule SE, PDF p. 1, lines 4a and 10-13: + # https://www.irs.gov/pub/irs-prior/f1040sse--2023.pdf#page=1 + # 10,000 * .9235 * (.124 + .029) / 2 = 706.4775 deductible SE tax. + # 2023 MT instructions, PDF pp. 51-52, lines 1 and 9: use each + # member's federal AGI and add back losses, preserving other adjustments. + # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 + # Income = 20,000 + 10,000 - 706.4775 = 29,293.5225. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 9_293.5225] + # Credit = 12,000 * .15 - (29,293.5225 - 12,600) * .05 = 965.323875. + mt_elderly_homeowner_or_renter_credit: [965.323875, 0] + - 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 @@ -580,3 +799,247 @@ # = 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] + +- name: Federal refundable credits and actual receipts reduce both elderly claims before selecting the larger one + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 4_800 + mt_elderly_homeowner_or_renter_credit_received: 60 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 20 + person2: + age: 70 + taxable_pension_income: 15_000 + rent: 7_200 + mt_elderly_homeowner_or_renter_credit_received: 40 + mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received: 80 + tax_units: + tax_unit1: + members: [person1] + income_tax_refundable_credits: 500 + tax_unit2: + members: [person2] + income_tax_refundable_credits: 300 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Schedule 2EC line 8 counts each return's federal refundable credits + # once, plus actual elderly-credit and rebate receipts. Person amounts: + # 10_000 + 500 + 60 + 20 = 10_580; 15_000 + 300 + 40 + 80 = 15_420. + # Line 18 = 26_000; line 22 = (26_000 - 12_600) * .05 = 670. + # Line 25 rent equivalents = [4_800, 7_200] * .15 = [720, 1_080]. + # Line 27 = [720, 1_080] - 670 = [50, 410], both below the 1_150 cap. + # Line 29 = 100% below 35_000. Only the larger 410 claim is paid; + # without the 1_000 of federal credits/receipts, it would be 460. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [10_580, 15_420] + mt_elderly_homeowner_or_renter_credit_net_household_income: [670, 670] + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [50, 410] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + mt_elderly_homeowner_or_renter_credit: [0, 410] + +- name: 2024 Schedule 2EC, a parent living in an adult child's home claims the child's property tax as rent + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + person2: + age: 45 + taxable_interest_income: 6_000 + rent: 0 + real_estate_taxes: 4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The home is in the child's name. "If the property occupied by you is + # in a name other than your own, the property taxes billed for that + # property can qualify as rent only" (2025 instructions, line 23; ARM + # 42.4.302(2)(b)). + # Line 18 = 14_000 + 6_000 = 20_000; line 20 = 20_000 - 12_600 = 7_400; + # line 21 = 0.035; line 22 = 259. + # Parent: line 23 = 0; line 24 = 4_000 (the child's bill, as rent); + # line 25 = 600; line 26 = 600; line 27 = 600 - 259 = 341; line 28 = + # 341; line 29 = 100% (under 35_000); line 30 = 341. Before this rule + # the parent's line 24 was 0 and the credit 0. + # The child is 45, so not a claimant (§ 15-30-2338(1)(a)). + mt_elderly_homeowner_or_renter_credit_net_household_income: [259, 0] + mt_elderly_homeowner_or_renter_credit_gross_rent: [4_000, 0] + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [341, 0] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + mt_elderly_homeowner_or_renter_credit: [341, 0] + mt_refundable_credits: [341, 0] + +- name: 2024 Schedule 2EC, a parent who pays the child less rent than the child's bill counts the bill + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 2_400 + person2: + age: 45 + taxable_interest_income: 6_000 + rent: 0 + real_estate_taxes: 4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 22 = 259 as above. Line 24 = the larger of the rent paid + # (2_400) and the child's bill (4_000) = 4_000; line 25 = 600; line 27 = + # 341. Counting rent paid alone would give 360 - 259 = 101. + mt_elderly_homeowner_or_renter_credit_gross_rent: [4_000, 0] + mt_elderly_homeowner_or_renter_credit: [341, 0] + +- name: 2024 Schedule 2EC, a parent who pays the child more rent than the child's bill counts the rent, not both + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 6_000 + person2: + age: 45 + taxable_interest_income: 6_000 + rent: 0 + real_estate_taxes: 4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 22 = 259. Line 24 = max(6_000, 4_000) = 6_000; line 25 = 900; + # line 27 = 900 - 259 = 641. Adding the bill to the rent (10_000) would + # give 1_500 - 259 = 1_241, capped at 1_150. + mt_elderly_homeowner_or_renter_credit_gross_rent: [6_000, 0] + mt_elderly_homeowner_or_renter_credit: [641, 0] + +- name: 2024 Schedule 2EC, two elderly siblings and one owns; the owner's larger credit is the one claim + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 0 + person2: + age: 70 + taxable_pension_income: 10_000 + rent: 0 + real_estate_taxes: 2_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 20_000; line 20 = 7_400; line 21 = 0.035; line 22 = 259. + # person1 (home in person2's name): line 24 = 2_000; line 25 = 300; + # line 27 = 300 - 259 = 41. + # person2: line 23 = 2_000; line 27 = 2_000 - 259 = 1_741; line 28 = + # 1_150 cap. + # One claimant per household (§ 15-30-2341(1)): person2 claims 1_150. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [41, 1_150] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + mt_elderly_homeowner_or_renter_credit: [0, 1_150] + +- name: 2024 Schedule 2EC, a home in a dependent's name counts as the claimant's rent, not property tax billed + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + is_tax_unit_dependent: false + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 4_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 14_000; line 20 = 1_400; line 21 = 0 (under 2_000); line + # 22 = 0. The home is in the dependent's name, not the claimant's: line + # 23 = 0; line 24 = 4_000; line 25 = 600; line 27 = 600; line 30 = 600. + # Counting the dependent's bill on line 23 would give 4_000, capped at + # 1_150. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + mt_elderly_homeowner_or_renter_credit_gross_rent: 4_000 + mt_elderly_homeowner_or_renter_credit: [600, 0] + +- name: 2024 Schedule 2EC, a home co-owned with a dependent keeps both bills on line 23 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + is_tax_unit_dependent: false + real_estate_taxes: 500 + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The claimant is billed too, so the home is in the claimant's name and + # ARM 42.4.302(2)(b) does not apply. Line 18 = 14_000; line 20 = 1_400; + # line 21 = 0; line 22 = 0. Line 23 = 500 + 2_000 = 2_500; line 24 = 0; + # line 27 = 2_500; line 28 = 1_150 cap; line 30 = 1_150, as before + # ARM 42.4.302(2)(b) was modeled. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_500 + mt_elderly_homeowner_or_renter_credit_gross_rent: 0 + mt_elderly_homeowner_or_renter_credit: [1_150, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml new file mode 100644 index 00000000000..0edc1f5d79a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.yaml @@ -0,0 +1,323 @@ +- name: Case 1, rent paid, with no property tax billed in the household. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 6_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Schedule 2EC line 24: "the rent that you paid". + mt_elderly_homeowner_or_renter_credit_gross_rent: 6_000 + +- name: Case 2, a parent living in an adult child's home counts the child's property tax as rent. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The parent is billed nothing: the home is "in a name other than the + # claimant", so the child's 3_000 bill goes on the parent's line 24 as + # rent (ARM 42.4.302(2)(b)). The child is billed their own property tax + # (line 23) and pays no rent. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [0, 3_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 3, a claimant billed their own property tax does not count another member's bill. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + real_estate_taxes: 500 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The home is in person1's name too, so ARM 42.4.302(2)(b) does not + # apply: line 23 = 500 and line 24 = 0. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [500, 3_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + +- name: Case 4, rent paid below the other member's bill; the bill counts. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 2_400 + person2: + age: 50 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 24 = max(2_400, 3_000): the bill is an alternative measure of the + # rent for the same home, not an addition to it. + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 5, rent paid above the other member's bill; the rent counts. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 4_800 + person2: + age: 50 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 24 = max(4_800, 3_000), not 4_800 + 3_000. + mt_elderly_homeowner_or_renter_credit_gross_rent: [4_800, 0] + +- name: Case 6, a home in a dependent's name counts as rent. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 0 + is_tax_unit_dependent: false + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The dependent is not the claimant, so their bill "can qualify as rent + # only": line 23 = 0 and line 24 = 2_000. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + mt_elderly_homeowner_or_renter_credit_gross_rent: 2_000 + +- name: Case 7, a home co-owned by two other members counts the whole bill. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 1_000 + person3: + age: 48 + rent: 0 + real_estate_taxes: 1_500 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # "The property taxes billed for that property": 1_000 + 1_500. + mt_elderly_homeowner_or_renter_credit_gross_rent: [2_500, 0, 0] + +- name: Case 8, a joint couple billed through the spouse does not count another member's bill. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + person2: + age: 66 + rent: 0 + real_estate_taxes: 1_800 + person3: + age: 40 + rent: 0 + real_estate_taxes: 2_000 + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # A joint return is one claimant, billed 1_800 through the spouse. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [1_800, 2_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + +- name: Case 9, in 2009 ARM 42.4.302(5) already counted property in another's name as rent. + period: 2009 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # ARM 42.4.302(5), effective 8/20/2004 to 11/10/2017: "Property taxes + # billed are allowable as rent if the property occupied by an eligible + # claimant is in a name other than the claimant." It became (2)(b) in + # 2017. + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 10, outside Montana. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + rent: 0 + person2: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_gross_rent: [0, 0] + +- name: Case 11, a joint couple billed nothing counts the other member's bill once. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + rent: 0 + person2: + age: 66 + rent: 0 + person3: + age: 45 + rent: 0 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # One claim for the joint return: line 24 = 3_000, not 6_000. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [0, 3_000] + mt_elderly_homeowner_or_renter_credit_gross_rent: [3_000, 0] + +- name: Case 12, a home co-owned with a dependent stays on line 23. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + rent: 0 + is_tax_unit_dependent: false + real_estate_taxes: 500 + person2: + age: 50 + rent: 0 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The home is in the claimant's name too, so ARM 42.4.302(2)(b) does not + # apply: the tax unit's bills, 500 + 2_000, are line 23, and line 24 is 0. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_500 + mt_elderly_homeowner_or_renter_credit_gross_rent: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml new file mode 100644 index 00000000000..fcdb8c37865 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml @@ -0,0 +1,107 @@ +- name: Case 1, each eligible tax unit's credit before the one-claimant limit. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 10_000 + rent: 4_800 + person2: + age: 70 + taxable_pension_income: 15_000 + rent: 7_200 + person3: + age: 40 + taxable_interest_income: 5_000 + rent: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # 2024 Schedule 2EC. Line 18 = 10_000 + 15_000 + 5_000 = 30_000 for the + # whole household; line 20 = 17_400; line 21 = 0.05; line 22 = 870. + # person1: line 25 = 720; line 27 = max(720 - 870, 0) = 0. + # person2: line 25 = 1_080; line 27 = 1_080 - 870 = 210. + # person3 is under 62, so not a claimant (§ 15-30-2338(1)(a)). + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [0, 210, 0] + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true, false] + mt_elderly_homeowner_or_renter_credit: [0, 210, 0] + +- name: Case 2, a tax unit billed no property tax counts another member's bill as rent. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + taxable_pension_income: 14_000 + rent: 0 + person2: + age: 72 + taxable_pension_income: 4_000 + rent: 0 + real_estate_taxes: 900 + person3: + age: 45 + taxable_interest_income: 2_000 + rent: 0 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # 2024 Schedule 2EC. Line 18 = 14_000 + 4_000 + 2_000 = 20_000; line 20 + # = 7_400; line 21 = 0.035; line 22 = 259. + # person1 (home in person2's name): line 24 = 900; line 25 = 135; line + # 27 = max(135 - 259, 0) = 0. + # person2: line 23 = 900; line 27 = 900 - 259 = 641. + # person3 is billed nothing either, so their line 24 is also 900, but + # they are under 62 and not a claimant. + mt_elderly_homeowner_or_renter_credit_gross_rent: [900, 0, 900] + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [0, 641, 0] + +- name: Case 3, the home's bill as rent makes the only eligible tax unit's credit positive. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 64 + taxable_pension_income: 9_000 + rent: 0 + person2: + age: 60 + taxable_pension_income: 3_000 + rent: 0 + real_estate_taxes: 2_500 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 12_000; line 20 = max(12_000 - 12_600, 0) = 0; line 22 = 0. + # person1: line 24 = 2_500; line 25 = 375; line 27 = 375. + # person2 is 60, so not a claimant, though the home is in their name. + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [375, 0] + mt_elderly_homeowner_or_renter_credit: [375, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml new file mode 100644 index 00000000000..f490d893e65 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.yaml @@ -0,0 +1,129 @@ +- name: Case 1, a single owner's property tax billed. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Schedule 2EC line 23. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_000 + +- name: Case 2, a joint couple's bills are one claimant's. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + real_estate_taxes: 1_200 + person2: + age: 66 + real_estate_taxes: 800 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # A joint return makes one claim: line 23 = 1_200 + 800. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_000 + +- name: Case 3, a home in a dependent's name is not billed to the claimant. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + is_tax_unit_dependent: false + person2: + age: 50 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The property is "in a name other than the claimant", so its tax "can + # qualify as rent only" (ARM 42.4.302(2)(b)): line 23 = 0. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + +- name: Case 4, each tax unit counts only the bill in its own name. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + person2: + age: 45 + real_estate_taxes: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_property_tax_billed: [0, 3_000] + +- name: Case 5, outside Montana. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 0 + +- name: Case 6, a home co-owned with a dependent counts both bills. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 75 + is_tax_unit_dependent: false + real_estate_taxes: 500 + person2: + age: 50 + is_tax_unit_dependent: true + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # The claimant is billed, so the home is in the claimant's name and the + # tax unit's bills are line 23: 500 + 2_000. + mt_elderly_homeowner_or_renter_credit_property_tax_billed: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml new file mode 100644 index 00000000000..31b6c0d4192 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.yaml @@ -0,0 +1,226 @@ +- name: Case 1, two eligible tax units in one household; the larger credit claims. + period: 2024 + input: + people: + person1: + age: 66 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + person2: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 460 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + +- name: Case 2, equal credits; the first tax unit in member order claims. + period: 2024 + input: + people: + person1: + age: 68 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 280 + person2: + age: 72 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 280 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, false] + +- name: Case 3, an ineligible tax unit is never selected, whatever its amount. + period: 2024 + input: + people: + person1: + age: 58 + mt_elderly_homeowner_or_renter_credit_eligible: false + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person2: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + +- name: Case 4, no eligible tax unit in the household. + period: 2024 + input: + people: + person1: + age: 45 + mt_elderly_homeowner_or_renter_credit_eligible: false + person2: + age: 50 + mt_elderly_homeowner_or_renter_credit_eligible: false + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, false] + +- name: Case 5, three eligible tax units; the largest credit claims wherever it is listed. + period: 2024 + input: + people: + person1: + age: 64 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + person2: + age: 80 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person3: + age: 75 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 460 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true, false] + +- name: Case 6, the limit is per household; each household selects its own claimant. + period: 2024 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 1_150 + person2: + age: 75 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person3: + age: 66 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 300 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3] + households: + household1: + members: [person1] + state_code: MT + household2: + members: [person2, person3] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [true, true, false] + +- name: Case 7, a joint couple is one tax unit and one claim. + period: 2025 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 1_150 + person2: + age: 66 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 0 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: true + +- name: Case 8, only tax unit heads are ranked; another member's amount does not count. + period: 2024 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 100 + person2: + # A dependent: the model holds a tax unit's credit on its head, so + # this amount, set here only to test the ranking, is ignored. + age: 12 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 900 + person3: + age: 67 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 200 + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] + +- name: Case 9, outside Montana no tax unit is selected. + period: 2024 + input: + people: + person1: + age: 70 + mt_elderly_homeowner_or_renter_credit_eligible: true + mt_elderly_homeowner_or_renter_credit_pre_one_claimant: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ID + output: + mt_elderly_homeowner_or_renter_credit_selected_claimant: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml index 6b5b7e020f8..a9033e68da4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml @@ -15,3 +15,41 @@ mt_standard_deduction_indiv: 10_000 output: mt_tax_unit_itemizes: false + +- name: Combined-AGI care phaseout keeps itemized deductions below the standard deduction + period: 2022 + input: + people: + head: + age: 40 + mt_agi_indiv: 20_000 + real_estate_taxes: 4_650 + spouse: + age: 40 + mt_agi_indiv: 0 + child: + age: 5 + tax_units: + tax_unit: + members: [head, spouse, child] + filing_status: JOINT + tax_unit_childcare_expenses: 3_000 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + # 2022 Form 2441-M, PDF p. 1, lines 2-7: min(3,000, 2,400) + # - .5 * (20,000 - 18,000) = 1,400; half to each spouse = 700. + # https://revenue.mt.gov/files/Forms/Child-and-Dependent-Care-Expense-Deduction-Form-2441-M/2022_Child_and_Dependent_Care_Expense_Deduction_Form_2441-M.pdf#page=1 + mt_child_dependent_care_expense_deduction: [700, 700, 0] + # 2022 Form 2 instructions, PDF p. 35: standard deductions are + # max(min(.2 * 20,000, 5,090), 2,260) = 4,000 and the 2,260 floor. + # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=35 + # Itemized total = 4,650 + 700 + 700 = 6,050 < 4,000 + 2,260 = 6,260. + # Using each spouse's AGI would give the spouse 1,200 and total 6,550, + # incorrectly switching the couple to itemization. + mt_itemized_deductions_indiv: [5_350, 700, 0] + mt_standard_deduction_indiv: [4_000, 2_260, 0] + mt_tax_unit_itemizes: false + mt_deductions_indiv: [4_000, 2_260, 0] diff --git a/policyengine_us/tests/test_mt_elderly_credit_household_members.py b/policyengine_us/tests/test_mt_elderly_credit_household_members.py index d1c1fff15ef..2d551fb39fd 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_household_members.py +++ b/policyengine_us/tests/test_mt_elderly_credit_household_members.py @@ -10,16 +10,19 @@ and puts the income of "other members of your household" on line 17. The model reaches the same quantity by two paths. A member who is the -claimant's tax unit dependent is counted through dependent_gross_income and -dependent_taxable_social_security; a member who files their own return is -counted through federal AGI, the loss add-back and taxable_social_security. +claimant's tax unit dependent is counted through their income and permitted +adjustments; a member who files their own return is counted through federal +AGI, the loss add-back and taxable_social_security. For a claimant aged 62 or older (taxable pension, Social Security, property -tax, rent) and one other adult in the household (taxable interest, a -long-term capital gain or loss, Social Security): +tax, rent) and one other member in the household (taxable interest, a +long-term capital gain or loss, Social Security, self-employment profit and +permitted non-loss adjustments): 1. The household's gross household income is the claimant's pension and - Social Security plus the other member's interest, positive capital gain - and Social Security, whichever path counts the other member. + Social Security plus the other member's interest, positive capital gain, + self-employment profit and Social Security, less permitted non-loss + adjustments, whichever path counts the other member. Capital losses + cannot reduce this amount. 2. So the claimant's credit is the same in both arrangements, and the other member's own tax unit, with no one aged 62 or older, claims nothing. 3. The credit is between zero and the cap, and zero when gross household @@ -27,10 +30,15 @@ 4. More interest for the other member never raises the credit: gross household income rises, so net household income (line 22) cannot fall and the credit multiplier (line 29) cannot rise. +5. Early-withdrawal penalties and self-employed health/pension expenses + reduce household income by their deductible amounts, even when those + adjustments make the member's contribution negative. -No one has earned income, so no refundable credit enters line 8. The model -computes in single precision, so comparisons allow one cent or eight float32 -spacings at the household's total income, whichever is larger. +Earned-income cases keep the other member under 25 with no dependents on +their own return, so neither arrangement receives refundable earned-income +or child tax credits. The claimant has no earned income. The model computes +in single precision, so comparisons allow one cent or eight float32 spacings +at the household's total income, whichever is larger. """ import numpy as np @@ -42,12 +50,14 @@ CAP = 1_150 INCOME_LIMIT = 45_000 TOLERANCE = 0.01 +ARRANGEMENTS = "ABCD" def build_situation(cases): - """Three Montana households per case: (A) the other member is the + """Four Montana households per case: (A) the other member is the claimant's dependent, (B) the other member files their own return and - (C) as A with the other member's extra interest added.""" + (C) as A with the other member's extra interest added, and (D) as A + without early-withdrawal, self-employed health or pension expenses.""" people, tax_units, marital_units, spm_units, families, households = ( {}, {}, @@ -57,7 +67,7 @@ def build_situation(cases): {}, ) for i, c in enumerate(cases): - for arrangement in "ABC": + for arrangement in ARRANGEMENTS: key = f"{arrangement}{i}" claimant, member = f"claimant_{key}", f"member_{key}" interest = c["interest"] + ( @@ -75,17 +85,29 @@ def build_situation(cases): "taxable_interest_income": {YEAR: interest}, "long_term_capital_gains": {YEAR: c["capital_gain"]}, "social_security_survivors": {YEAR: c["member_ss"]}, + "self_employment_income": {YEAR: c["self_employment_income"]}, + "early_withdrawal_penalty": { + YEAR: 0 if arrangement == "D" else c["early_withdrawal_penalty"] + }, + "self_employed_health_insurance_premiums": { + YEAR: 0 if arrangement == "D" else c["health_premiums"] + }, + "self_employed_pension_contributions": { + YEAR: 0 if arrangement == "D" else c["pension_contributions"] + }, } if arrangement == "B": tax_units[f"tax_unit_{key}"] = {"members": [claimant]} tax_units[f"tax_unit_{key}_member"] = {"members": [member]} else: tax_units[f"tax_unit_{key}"] = {"members": [claimant, member]} - # Set for everyone: an input for some people gives the others - # its default (false), not its formula. Without it the model - # takes an adult in the claimant's tax unit to be the spouse. - people[claimant]["is_tax_unit_dependent"] = {YEAR: False} - people[member]["is_tax_unit_dependent"] = {YEAR: arrangement != "B"} + # Complete roles distinguish adult dependents from spouses + # and keep a minor's separate return headed by that minor; + # age-based inference only considers adults as filers. + people[claimant]["tax_unit_role_input"] = {YEAR: "HEAD"} + people[member]["tax_unit_role_input"] = { + YEAR: "HEAD" if arrangement == "B" else "DEPENDENT" + } marital_units[f"marital_unit_{key}"] = {"members": [claimant]} marital_units[f"marital_unit_{key}_member"] = {"members": [member]} spm_units[f"spm_unit_{key}"] = {"members": [claimant, member]} @@ -124,22 +146,59 @@ def calculate(cases): simulation.calculate("mt_elderly_homeowner_or_renter_credit", YEAR) ) dependent = np.asarray(simulation.calculate("is_tax_unit_dependent", YEAR)) - # Households and people are in situation order: A0, B0, C0, A1, ...; + other_refundable_credits = np.asarray( + simulation.calculate( + "mt_refundable_credits_before_renter_credit", YEAR, map_to="household" + ) + ) + federal_refundable_credits = np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit_federal_refundable_credits", + YEAR, + map_to="household", + ) + ) + # Households and people are in situation order: A0, B0, C0, D0, A1, ...; # the member follows the claimant. - n = len(cases) + n, arrangements = len(cases), len(ARRANGEMENTS) return { - "gross_household_income": gross_household_income.reshape(n, 3).T, - "credit": credit.reshape(n, 3).T, - "member_credit": person_credit[1::2].reshape(n, 3).T, - "member_is_dependent": dependent[1::2].reshape(n, 3).T, + "gross_household_income": gross_household_income.reshape(n, arrangements).T, + "credit": credit.reshape(n, arrangements).T, + "member_credit": person_credit[1::2].reshape(n, arrangements).T, + "member_is_dependent": dependent[1::2].reshape(n, arrangements).T, + "other_refundable_credits": other_refundable_credits.reshape(n, arrangements).T, + "federal_refundable_credits": federal_refundable_credits.reshape( + n, arrangements + ).T, } -def expected_gross_household_income(c, extra_interest=0): - """2024 Schedule 2EC line 18: lines 5 and 6 for the claimant, and line - 17 for the other member's interest, gains and Social Security.""" +def elective_adjustments(c): + # Generated health premiums and pension contributions stay below the + # member's eligible self-employment earnings, so all are deductible. + return ( + c["early_withdrawal_penalty"] + + c["health_premiums"] + + c["pension_contributions"] + ) + + +def expected_gross_household_income(c): + """All members' federal AGI without losses, plus nontaxable income: + MCA 15-30-2337(9); 2023 Form 2 instructions PDF pp. 51-52. + https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 + """ claimant = c["pension"] + c["claimant_ss"] - member = c["interest"] + extra_interest + max(0, c["capital_gain"]) + member = c["interest"] + max(0, c["capital_gain"]) + c["self_employment_income"] + # Schedule SE lines 4a, 4c, 10-13: taxable earnings are 92.35% of + # profit; no SE tax below $400. Generated profits stay below the + # Social Security wage base, and no one has wages. + # https://www.irs.gov/pub/irs-prior/f1040sse--2024.pdf#page=1 + taxable_earnings = c["self_employment_income"] * 0.9235 + half_se_tax = ( + taxable_earnings * (0.124 + 0.029) * 0.5 if taxable_earnings >= 400 else 0 + ) + member -= half_se_tax + elective_adjustments(c) return claimant + member + c["member_ss"] @@ -149,33 +208,44 @@ def tolerance(amount): def assert_properties(cases): result = calculate(cases) - a, b, c_ = 0, 1, 2 - # The other member is a dependent in A and C and their own filer in B. + a, b, c_, d = 0, 1, 2, 3 + # Equivalence assumes these income definitions contain no refundable + # credits whose eligibility changes when the member files separately. + assert np.all(result["other_refundable_credits"] == 0), cases + assert np.all(result["federal_refundable_credits"] == 0), cases + # The other member is a dependent in A, C and D and their own filer in B. assert result["member_is_dependent"].tolist() == [ [True] * len(cases), [False] * len(cases), [True] * len(cases), + [True] * len(cases), ] for i, c in enumerate(cases): expected = expected_gross_household_income(c) - tol = tolerance(expected + c["extra_interest"]) + expenses = elective_adjustments(c) + tol = tolerance(expected + c["extra_interest"] + expenses) ghi = result["gross_household_income"] credit = result["credit"] # 1. Both paths give the Schedule 2EC total. assert ghi[a, i] == pytest.approx(expected, abs=tol), c assert ghi[b, i] == pytest.approx(expected, abs=tol), c assert ghi[c_, i] == pytest.approx(expected + c["extra_interest"], abs=tol), c + assert ghi[d, i] == pytest.approx(expected + expenses, abs=tol), c # 2. The claimant's credit does not depend on the arrangement, and # the other member claims nothing. assert credit[a, i] == pytest.approx(credit[b, i], abs=tol), c assert np.all(result["member_credit"][:, i] == 0), c - for arrangement in (a, b, c_): + for arrangement in (a, b, c_, d): # 3. Bounds and the income limit. assert -tol <= credit[arrangement, i] <= CAP + tol, c if ghi[arrangement, i] >= INCOME_LIMIT + tol: assert credit[arrangement, i] == 0, c # 4. More income never raises the credit. assert credit[c_, i] <= credit[a, i] + tol, c + # 5. Non-loss adjustments lower income by the full deductible + # amount, including when the member has negative AGI without losses. + assert ghi[d, i] - ghi[a, i] == pytest.approx(expenses, abs=tol), c + assert credit[d, i] <= credit[a, i] + tol, c def test_worked_example(): @@ -194,16 +264,62 @@ def test_worked_example(): "interest": 10_000, "capital_gain": 0, "member_ss": 0, + "self_employment_income": 0, + "early_withdrawal_penalty": 0, + "health_premiums": 0, + "pension_contributions": 0, "extra_interest": 5_000, } result = calculate([case]) assert result["gross_household_income"][:, 0] == pytest.approx( - [30_000, 30_000, 35_000], abs=TOLERANCE + [30_000, 30_000, 35_000, 30_000], abs=TOLERANCE + ) + assert result["credit"][:, 0] == pytest.approx( + [1_130, 1_130, 352, 1_130], abs=TOLERANCE ) - assert result["credit"][:, 0] == pytest.approx([1_130, 1_130, 352], abs=TOLERANCE) assert_properties([case]) +def negative_household_cases(): + return [ + { + "claimant_age": claimant_age, + "pension": 0, + "claimant_ss": 347, + "property_tax": 319, + "rent": 0, + "member_age": 43, + "interest": interest, + "capital_gain": 0, + "member_ss": member_ss, + "self_employment_income": 0, + "early_withdrawal_penalty": penalty, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": extra_interest, + } + for claimant_age, interest, member_ss, penalty, extra_interest in ( + (62, 4_420, 894, 5_986, 377), + (86, 0, 0, 894, 1), + ) + ] + + +def test_negative_household_income_keeps_full_multiplier(): + # 347 + 4,420 + 894 - 5,986 = -325; adding 377 gives 52; + # removing the penalty gives 5,661. In the second case, 347 - 894 + # = -547, or -546 with extra interest, or 347 without the penalty. + # Every total is below 12,600, so net household contribution is zero. + # Line 29 gives 100% below 35,000, so the 319 property tax is paid: + # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=49 + result = calculate(negative_household_cases()) + assert result["gross_household_income"] == pytest.approx( + np.array([[-325, -547], [-325, -547], [52, -546], [5_661, 347]]), + abs=TOLERANCE, + ) + assert result["credit"] == pytest.approx(np.full((4, 2), 319), abs=TOLERANCE) + + try: import hypothesis import hypothesis.strategies as st @@ -216,26 +332,125 @@ def test_worked_example(): SLOW = [hypothesis.HealthCheck.too_slow] def amount(high): + if high == 0: + return st.just(0) return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) @st.composite def case(draw): + member_age = draw(st.integers(min_value=15, max_value=61)) + self_employment_income = draw(amount(20_000)) if member_age <= 24 else 0 return { "claimant_age": draw(st.integers(min_value=62, max_value=90)), "pension": draw(amount(40_000)), "claimant_ss": draw(amount(30_000)), "property_tax": draw(st.integers(min_value=0, max_value=4_000)), "rent": draw(amount(12_000)), - "member_age": draw(st.integers(min_value=15, max_value=61)), + "member_age": member_age, "interest": draw(amount(40_000)), "capital_gain": draw( st.one_of(st.just(0), st.integers(min_value=-20_000, max_value=40_000)) ), "member_ss": draw(amount(20_000)), + "self_employment_income": self_employment_income, + "early_withdrawal_penalty": draw(amount(40_000)), + # Keep these expenses within earned income and conservative + # plan limits, so both filer arrangements permit the deduction. + "health_premiums": draw(amount(self_employment_income // 4)), + "pension_contributions": draw(amount(self_employment_income // 10)), "extra_interest": draw(st.integers(min_value=1, max_value=20_000)), } - @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=15, derandomize=True, deadline=None, suppress_health_check=SLOW + ) + @hypothesis.example(negative_household_cases()) + @hypothesis.example( + [ + # A minor can head their own return. With no gross income, + # a deductible penalty of 1 gives AGI -1 in both arrangements. + { + "claimant_age": 62, + "pension": 0, + "claimant_ss": 0, + "property_tax": 0, + "rent": 0, + "member_age": 15, + "interest": 0, + "capital_gain": 0, + "member_ss": 0, + "self_employment_income": 0, + "early_withdrawal_penalty": 1, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": 1, + } + ] + ) + @hypothesis.example( + [ + # P2: 10,000 * .9235 * .153 / 2 = 706.4775; + # gross household income = 20,000 + 10,000 - 706.4775. + { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 0, + "rent": 12_000, + "member_age": 18, + "interest": 0, + "capital_gain": 0, + "member_ss": 0, + "self_employment_income": 10_000, + "early_withdrawal_penalty": 0, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": 1_000, + }, + # The capital loss is excluded. 3,000 * .9235 * .153 / 2 + # = 211.94325; the other adjustments total 5,700, leaving + # member income 3,500 - 211.94325 - 5,700 = -2,411.94325. + { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 0, + "rent": 12_000, + "member_age": 24, + "interest": 500, + "capital_gain": -3_000, + "member_ss": 0, + "self_employment_income": 3_000, + "early_withdrawal_penalty": 5_000, + "health_premiums": 500, + "pension_contributions": 200, + "extra_interest": 1_000, + }, + ] + ) + @hypothesis.example( + [ + # Taxable SE earnings: 433 * .9235 = 399.8755 (no tax), + # 434 * .9235 = 400.799 (half-tax deduction 30.6611235). + { + "claimant_age": 70, + "pension": 20_000, + "claimant_ss": 0, + "property_tax": 0, + "rent": 12_000, + "member_age": 18, + "interest": 0, + "capital_gain": 0, + "member_ss": 0, + "self_employment_income": profit, + "early_withdrawal_penalty": 0, + "health_premiums": 0, + "pension_contributions": 0, + "extra_interest": 1_000, + } + for profit in (433, 434) + ] + ) @hypothesis.given(st.lists(case(), min_size=1, max_size=8)) def test_properties(cases): assert_properties(cases) diff --git a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py new file mode 100644 index 00000000000..c77da08ba2e --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py @@ -0,0 +1,415 @@ +"""Property tests: one claimant per household for the Montana elderly +homeowner/renter credit. + +MCA 15-30-2341(1): "Only one claimant per household in a claim period under +the provisions of 15-30-2337 through 15-30-2341 is entitled to relief." The +2024 and 2025 Schedule 2EC attestation reads "I am the only member of my +household claiming this credit". Gross household income (line 18) covers +everyone in the household, so every tax unit in a household with a member +aged 62 or older computes its credit on the same net household income (line +22) and multiplier (line 29); the credits differ only by each tax unit's +property tax billed (line 23) and rent (line 24). A tax unit billed no +property tax is treated as living in a home in another member's name, whose +property tax "can qualify as rent only" (ARM 42.4.302(2)(b)): its line 24 is +the larger of the rent it paid and the property tax billed to the +household's other members. The model pays the tax unit whose credit is +largest. + +A reference written here from the schedule's lines is compared with the +model for random Montana households of one to three tax units (single or +joint, any mix of ages, pension, interest, Social Security, property tax and +rent; no earned income, so no refundable credit enters line 8). The +generator has no dependents, so the household's other members are its other +tax units; the YAML tests cover a home in a dependent's name: + +1. Each tax unit's credit before the limit matches the reference. +2. The household's credit is the largest of its tax units' credits, so it + never exceeds the $1,150 cap of a single claim. +3. At most one tax unit per household is paid. Exactly one is selected in a + household with an eligible tax unit, and none in a household without one. +4. A tax unit is paid either nothing or its whole credit, and the selected + one is paid its whole credit. +5. Reversing the order of the tax units in a household does not change the + household's credit. + +A second test raises the property tax billed to one tax unit and checks: + +6. No other tax unit's credit before the limit falls, and neither does that + tax unit's own credit if it was already billed property tax. (A tax unit + newly billed property tax can lose credit: it stops counting the other + members' bill as rent and counts only its own bill.) +7. The household's credit does not fall unless the raised tax unit was billed + nothing before. + +The model computes in single precision, so comparisons allow one cent or +eight float32 spacings at the household's total income, whichever is larger. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system + +YEAR = 2024 +TOLERANCE = 0.01 +P = system.parameters(YEAR).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter + + +def build_situation(households): + """Each household is a list of tax units; each tax unit is a dict with + "adults" (a list of dicts with age, pension, interest and social + security) and the head's property tax and rent. Every household is built + twice: as given, and with its tax units in reverse order.""" + people, tax_units, marital_units, spm_units, families, situation_households = ( + {}, + {}, + {}, + {}, + {}, + {}, + ) + for h, units in enumerate(households): + for order, ordered_units in (("fwd", units), ("rev", units[::-1])): + key = f"{order}{h}" + members = [] + for u, unit in enumerate(ordered_units): + unit_key = f"{key}_u{u}" + unit_members = [] + for a, adult in enumerate(unit["adults"]): + person = f"{unit_key}_p{a}" + people[person] = { + "age": {YEAR: adult["age"]}, + "taxable_pension_income": {YEAR: adult["pension"]}, + "taxable_interest_income": {YEAR: adult["interest"]}, + "social_security_retirement": {YEAR: adult["social_security"]}, + "is_tax_unit_dependent": {YEAR: False}, + } + unit_members.append(person) + # The first adult is the oldest, so the head: the property + # tax and rent are theirs. + head = unit_members[0] + people[head]["real_estate_taxes"] = {YEAR: unit["property_tax"]} + people[head]["rent"] = {YEAR: unit["rent"]} + tax_units[unit_key] = {"members": unit_members} + marital_units[unit_key] = {"members": unit_members} + members += unit_members + spm_units[key] = {"members": members} + families[key] = {"members": members} + situation_households[key] = { + "members": members, + "state_code": {YEAR: "MT"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": spm_units, + "families": families, + "households": situation_households, + } + + +def reference(units): + """Each tax unit's credit before the one-claimant limit, from the 2024 + Schedule 2EC lines.""" + # Line 18: the pension, interest and Social Security of everyone in the + # household. + gross = sum( + adult["pension"] + adult["interest"] + adult["social_security"] + for unit in units + for adult in unit["adults"] + ) + # Lines 19-22. + reduced = max(gross - P.net_household_income.standard_exclusion, 0) + net = reduced * float(P.net_household_income.reduction_rate.calc(reduced)) + # Line 29. + multiplier = float(P.multiplier.calc(gross)) + household_property_tax = sum(unit["property_tax"] for unit in units) + credits = [] + for unit in units: + eligible = max(adult["age"] for adult in unit["adults"]) >= P.age_threshold + # Line 24: a tax unit billed no property tax may use the property tax + # billed to the household's other members as rent, in place of the + # rent it paid if that is larger (ARM 42.4.302(2)(b)). + rent = unit["rent"] + if unit["property_tax"] == 0: + rent = max(rent, household_property_tax) + # Lines 23-28. + housing = unit["property_tax"] + rent * P.rent_equivalent_tax_rate + capped = min(max(housing - net, 0), P.cap) + credits.append(eligible * capped * multiplier) + return gross, credits + + +def tolerance(amount): + return max(TOLERANCE, 8 * float(np.spacing(np.float32(abs(amount))))) + + +def calculate(households): + """Per tax unit, in situation order: the credit before the limit, the + selection and the credit.""" + simulation = Simulation(situation=build_situation(households)) + + def by_tax_unit(variable): + return np.asarray(simulation.calculate(variable, YEAR, map_to="tax_unit")) + + return { + "pre_one_claimant": by_tax_unit( + "mt_elderly_homeowner_or_renter_credit_pre_one_claimant" + ), + "selected": np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit_selected_claimant", YEAR + ) + ), + "credit": by_tax_unit("mt_elderly_homeowner_or_renter_credit"), + } + + +def assert_properties(households): + result = calculate(households) + start = 0 + for units in households: + n = len(units) + gross, expected = reference(units) + tol = tolerance(gross) + totals = {} + for order, ordered_expected in (("fwd", expected), ("rev", expected[::-1])): + pre = result["pre_one_claimant"][start : start + n] + selected = result["selected"][start : start + n] + credit = result["credit"][start : start + n] + start += n + # 1. Each tax unit's credit before the limit. + assert pre == pytest.approx(ordered_expected, abs=tol), units + # 2. The household is paid its largest credit, within the cap. + totals[order] = credit.sum() + assert totals[order] == pytest.approx(max(expected), abs=tol), units + assert -tol <= totals[order] <= P.cap + tol, units + # 3. At most one payment; one selection where any tax unit is + # eligible. + assert (credit > tol).sum() <= 1, units + any_eligible = any( + max(adult["age"] for adult in unit["adults"]) >= P.age_threshold + for unit in units + ) + assert selected.sum() == int(any_eligible), units + # 4. Nothing or the whole credit; the selected unit gets it all. + assert np.all((np.abs(credit) <= tol) | (np.abs(credit - pre) <= tol)), ( + units + ) + assert credit[selected] == pytest.approx(pre[selected], abs=tol), units + # 5. The order of the tax units does not matter. + assert totals["fwd"] == pytest.approx(totals["rev"], abs=tol), units + + +def adult(age, pension=0, interest=0, social_security=0): + return { + "age": age, + "pension": pension, + "interest": interest, + "social_security": social_security, + } + + +def test_worked_example(): + """2024 Schedule 2EC: two siblings, 66 and 70, filing separately in one + household with pensions of 10,000 and 15,000 and rent of 4,800 and + 7,200. Line 18 = 25,000; line 22 = 12,400 x 0.05 = 620. Line 27 is + 720 - 620 = 100 for the first and 1,080 - 620 = 460 for the second. + Only the second is paid; before the limit the household received 560.""" + households = [ + [ + {"adults": [adult(66, pension=10_000)], "property_tax": 0, "rent": 4_800}, + {"adults": [adult(70, pension=15_000)], "property_tax": 0, "rent": 7_200}, + ] + ] + result = calculate(households) + # Forward order, then reversed. + assert result["pre_one_claimant"] == pytest.approx( + [100, 460, 460, 100], abs=TOLERANCE + ) + assert result["selected"].tolist() == [False, True, True, False] + assert result["credit"] == pytest.approx([0, 460, 460, 0], abs=TOLERANCE) + assert_properties(households) + + +PARENT_IN_CHILDS_HOME = [ + {"adults": [adult(80, pension=14_000)], "property_tax": 0, "rent": 0}, + {"adults": [adult(45, interest=6_000)], "property_tax": 4_000, "rent": 0}, +] + + +def test_property_tax_in_another_name_worked_example(): + """2024 Schedule 2EC: a parent, 80, with a 14,000 pension lives in the + home of an adult child, 45, who has 6,000 of interest and is billed 4,000 + of property tax; each files their own return. Line 18 = 20,000; line 22 + = 7,400 x 0.035 = 259. The home is in the child's name, so its property + tax "can qualify as rent only" on the parent's schedule (ARM + 42.4.302(2)(b)): line 24 = 4,000, line 25 = 600, line 27 = 341. The child + is under 62 and cannot claim.""" + households = [PARENT_IN_CHILDS_HOME] + result = calculate(households) + assert result["pre_one_claimant"] == pytest.approx([341, 0, 0, 341], abs=TOLERANCE) + assert result["selected"].tolist() == [True, False, False, True] + assert result["credit"] == pytest.approx([341, 0, 0, 341], abs=TOLERANCE) + assert_properties(households) + + +def raise_property_tax(units, index, increase): + """The household with `increase` more property tax billed to the head of + tax unit `index`.""" + raised = list(units) + raised[index] = { + **units[index], + "property_tax": units[index]["property_tax"] + increase, + } + return raised + + +def assert_raising_a_bill(units, index, increase): + """Invariants 6 and 7.""" + result = calculate([units, raise_property_tax(units, index, increase)]) + n = len(units) + tol = tolerance(reference(units)[0]) + # build_situation lays out each household forward, then reversed; compare + # the forward copies. + before = result["pre_one_claimant"][:n] + after = result["pre_one_claimant"][2 * n : 3 * n] + already_billed = units[index]["property_tax"] > 0 + for j in range(n): + if j != index or already_billed: + assert after[j] >= before[j] - tol, (units, index, increase) + if already_billed: + household_before = result["credit"][:n].sum() + household_after = result["credit"][2 * n : 3 * n].sum() + assert household_after >= household_before - tol, (units, index, increase) + + +def test_newly_billed_tax_unit_can_lose_credit(): + """The exception in invariant 6, which follows the form's reading of line + 23 ("the property tax you were billed"). Billing the parent 100 of + property tax makes the home theirs too: line 23 = 100 and line 24 = 0, + so line 27 = max(100 - 259, 0) = 0 instead of 341. Line 22 is unchanged + at 259. The child's bill still counts for no one but the child.""" + result = calculate( + [PARENT_IN_CHILDS_HOME, raise_property_tax(PARENT_IN_CHILDS_HOME, 0, 100)] + ) + assert result["pre_one_claimant"][[0, 4]] == pytest.approx([341, 0], abs=TOLERANCE) + assert_raising_a_bill(PARENT_IN_CHILDS_HOME, 1, 500) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + @st.composite + def person(draw): + return adult( + draw(st.integers(min_value=40, max_value=90)), + pension=draw(amount(25_000)), + interest=draw(amount(15_000)), + social_security=draw(amount(20_000)), + ) + + @st.composite + def tax_unit(draw): + adults = draw(st.lists(person(), min_size=1, max_size=2)) + # Put the oldest first so that the head holds the housing costs. + adults.sort(key=lambda a: -a["age"]) + if len(adults) == 2 and adults[0]["age"] == adults[1]["age"]: + adults[1]["age"] -= 1 + return { + "adults": adults, + "property_tax": draw(amount(4_000)), + "rent": draw(amount(12_000)), + } + + @hypothesis.settings( + max_examples=15, deadline=None, derandomize=True, suppress_health_check=SLOW + ) + @hypothesis.given( + st.lists( + st.lists(tax_unit(), min_size=1, max_size=3), + min_size=1, + max_size=4, + ) + ) + # Both tax units reach the $1,150 cap (line 18 = 20,000; line 22 = 259; + # line 27 = 2,741 and 1,741): one claim of 1,150. + @hypothesis.example( + [ + [ + { + "adults": [adult(70, pension=10_000)], + "property_tax": 3_000, + "rent": 0, + }, + { + "adults": [adult(66, pension=10_000)], + "property_tax": 2_000, + "rent": 0, + }, + ] + ] + ) + # A renter paying 6,000 (line 26 = 900) and an owner billed 1,000 (line + # 26 = 1,000): the owner's 741 is paid, not the renter's 641, although + # the renter pays more for housing. + @hypothesis.example( + [ + [ + { + "adults": [adult(70, pension=10_000)], + "property_tax": 0, + "rent": 6_000, + }, + { + "adults": [adult(66, pension=10_000)], + "property_tax": 1_000, + "rent": 0, + }, + ] + ] + ) + # A parent in an adult child's home: the child's bill is the parent's + # rent (line 24 = 4,000; line 27 = 600 - 259 = 341). + @hypothesis.example([PARENT_IN_CHILDS_HOME]) + # The same parent paying the child 2,400 of rent: line 24 is the larger + # bill, 4,000, not 2,400 + 4,000. + @hypothesis.example( + [ + [ + {**PARENT_IN_CHILDS_HOME[0], "rent": 2_400}, + PARENT_IN_CHILDS_HOME[1], + ] + ] + ) + def test_properties(households): + assert_properties(households) + + @st.composite + def raised_bill(draw): + units = draw(st.lists(tax_unit(), min_size=1, max_size=3)) + index = draw(st.integers(min_value=0, max_value=len(units) - 1)) + increase = draw(st.integers(min_value=1, max_value=4_000)) + return units, index, increase + + @hypothesis.settings( + max_examples=10, deadline=None, derandomize=True, suppress_health_check=SLOW + ) + @hypothesis.given(raised_bill()) + # Raising the child's bill raises the parent's rent and credit. + @hypothesis.example((PARENT_IN_CHILDS_HOME, 1, 500)) + def test_raising_a_bill(case): + assert_raising_a_bill(*case) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py index f2cbf8e8162..b0f123f4552 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.py @@ -9,41 +9,18 @@ class mt_elderly_homeowner_or_renter_credit(Variable): definition_period = YEAR defined_for = "mt_elderly_homeowner_or_renter_credit_eligible" reference = ( - "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0410/0150-0300-0230-0410.html", # 2024 Form 2, Schedule 2EC "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", ) def formula(person, period, parameters): - p = parameters( - period - ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter - # § 15-30-2337(4) defines gross household income as "all income - # received by all individuals of a household", including members - # outside the claimant's return (2024 Schedule 2EC line 17), so the - # multiplier phase-out applies to the household's total income rather - # than each person's share. - gross_household_income = add( - person.household, - period, - ["mt_elderly_homeowner_or_renter_credit_gross_household_income"], + # Only one claimant per household is entitled to relief + # (§ 15-30-2341(1)). + credit = person( + "mt_elderly_homeowner_or_renter_credit_pre_one_claimant", period ) - # Get net_household_income and allocate it to the head - head = person("is_tax_unit_head", period) - net_household_income = add( - person.tax_unit, - period, - ["mt_elderly_homeowner_or_renter_credit_net_household_income"], + selected = person.tax_unit( + "mt_elderly_homeowner_or_renter_credit_selected_claimant", period ) - # Credit Computation - property_tax = add(person.tax_unit, period, ["real_estate_taxes"]) - rent = add(person.tax_unit, period, ["rent"]) - countable_rent = rent * p.rent_equivalent_tax_rate - countable_rent_and_property_tax = property_tax + countable_rent - uncapped_credit_unit = max_( - countable_rent_and_property_tax - net_household_income, 0 - ) - uncapped_credit = uncapped_credit_unit * head - capped_credit = min_(uncapped_credit, p.cap) - multiplier = p.multiplier.calc(gross_household_income) - return capped_credit * multiplier + return credit * selected diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py new file mode 100644 index 00000000000..7a2cd0f939c --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_rent.py @@ -0,0 +1,64 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_gross_rent(Variable): + value_type = float + entity = TaxUnit + label = "Montana gross rent for the elderly homeowner/renter credit" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # § 15-30-2337(5): gross rent; § 15-30-2340(2): a claimant who rents + # the homestead + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0400/0150-0300-0230-0400.html", + # ARM 42.4.302(2)(b): property taxes billed may be used "as rent if + # the property occupied by the claimant is in a name other than the + # claimant"; (3)(a) and (3)(c)(v): two measures of housing cost for + # one period count "either ... or ..., but not both" + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # ARM 42.4.302(5) before its 2017 restructuring, in force from + # 8/20/2004: "Property taxes billed are allowable as rent if the + # property occupied by an eligible claimant is in a name other than + # the claimant." + "https://web.archive.org/web/20080624004125/http://www.mtrules.org/gateway/RuleNo.asp?RN=42.4.302", + # 2025 Schedule 2EC lines 24 and 25 + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + # 2025 instructions, lines 23 and 24: "If the property occupied by + # you is in a name other than your own, the property taxes billed for + # that property can qualify as rent only." + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47", + ) + + def formula(tax_unit, period, parameters): + # Schedule 2EC line 24: "the rent that you paid". + rent = add(tax_unit, period, ["rent"]) + # The model treats a tax unit billed no property tax as living in a + # home in another household member's name, a dependent's included. + # Its property tax "can qualify as rent only" (2025 instructions, + # line 23; ARM 42.4.302(2)(b)): it goes on line 24, not line 23, so + # line 25 counts 15% of it. With the claimant billed nothing, "the + # property taxes billed for that property" are the household's whole + # property tax. + # The claimant "may use" the bill as rent: a choice between two + # measures of what it costs to occupy one home, not an addition to + # the rent paid. The rule's facility provisions likewise let a + # claimant report one housing cost or another for a period, "but not + # both" (ARM 42.4.302(3)(a), (3)(c)(v)). So line 24 is the larger of + # the rent paid and the bill. + # Owners outside the household, such as a landlord, are not visible + # here, and real_estate_taxes is not limited to the residence and + # one acre. + claimant_property_tax = tax_unit( + "mt_elderly_homeowner_or_renter_credit_property_tax_billed", period + ) + person = tax_unit.members + head = person("is_tax_unit_head", period) + household_property_tax = tax_unit.sum( + add(person.household, period, ["real_estate_taxes"]) * head + ) + property_tax_as_rent = where( + claimant_property_tax <= 0, household_property_tax, 0 + ) + return max_(rent, property_tax_as_rent) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py index a46ccf6f5c6..56053085070 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_net_household_income.py @@ -18,8 +18,10 @@ def formula(person, period, parameters): p = parameters( period ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter - # Only one claim is allowed per household - # married taxpayer who are living apart may qualify for only one credit per year + # Only one claimant per household is entitled to relief + # (§ 15-30-2341(1)); mt_elderly_homeowner_or_renter_credit_selected_claimant + # picks the claimant. Married couples living apart may receive only + # one credit per year (ARM 42.4.302(3)(a)). standard_exclusion = p.net_household_income.standard_exclusion # Gross household income counts every member of the household, # including those outside the claimant's return (§ 15-30-2337(4); diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py new file mode 100644 index 00000000000..04e7d41e713 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.py @@ -0,0 +1,62 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_pre_one_claimant(Variable): + value_type = float + entity = Person + label = "Montana Elderly Homeowner/Renter Credit before the one-claimant limit" + unit = USD + definition_period = YEAR + defined_for = "mt_elderly_homeowner_or_renter_credit_eligible" + reference = ( + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0400/0150-0300-0230-0400.html", + # ARM 42.4.302(1): property tax billed on the owned part plus the + # rent-equivalent tax on the rented part + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # 2024 Form 2, Schedule 2EC + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + # 2024 instructions, line 29 Credit Multiplier Table + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=49", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.states.mt.tax.income.credits.elderly_homeowner_or_renter + # § 15-30-2337(4) defines gross household income as "all income + # received by all individuals of a household", including members + # outside the claimant's return (2024 Schedule 2EC line 17), so the + # multiplier phase-out applies to the household's total income rather + # than each person's share. + gross_household_income = add( + person.household, + period, + ["mt_elderly_homeowner_or_renter_credit_gross_household_income"], + ) + # Get net_household_income and allocate it to the head + head = person("is_tax_unit_head", period) + net_household_income = add( + person.tax_unit, + period, + ["mt_elderly_homeowner_or_renter_credit_net_household_income"], + ) + # Credit Computation: Schedule 2EC lines 23 to 26 + property_tax = person.tax_unit( + "mt_elderly_homeowner_or_renter_credit_property_tax_billed", period + ) + rent = person.tax_unit( + "mt_elderly_homeowner_or_renter_credit_gross_rent", period + ) + countable_rent = rent * p.rent_equivalent_tax_rate + countable_rent_and_property_tax = property_tax + countable_rent + uncapped_credit_unit = max_( + countable_rent_and_property_tax - net_household_income, 0 + ) + uncapped_credit = uncapped_credit_unit * head + capped_credit = min_(uncapped_credit, p.cap) + # The table gives 100% for all income less than $35,000, including + # negative AGI after permitted adjustments. Preserve signed income + # and floor only the lookup input at its first threshold. + multiplier = p.multiplier.calc(max_(0, gross_household_income)) + return capped_credit * multiplier diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py new file mode 100644 index 00000000000..d961cdec535 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_property_tax_billed.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_property_tax_billed(Variable): + value_type = float + entity = TaxUnit + label = "Montana property tax billed for the elderly homeowner/renter credit" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # § 15-30-2337(10): property tax billed; § 15-30-2340(1): a claimant + # who owns the homestead + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0400/0150-0300-0230-0400.html", + # ARM 42.4.302(2)(b): property in a name other than the claimant + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # 2025 Schedule 2EC line 23: "the property tax you were billed" + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + # PDF pages 46-47: line 23, and property "in a name other than your + # own" + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=46", + ) + + def formula(tax_unit, period, parameters): + # Schedule 2EC line 23 is "the property tax you were billed". A joint + # return makes one claim, so the claimant is the head and spouse. + # When either is billed property tax, the home is in the claimant's + # name and the tax unit's bills count here, including a dependent + # co-owner's. When neither is, the home is in a name other than the + # claimant's, a dependent's included, and its property tax "can + # qualify as rent only" (ARM 42.4.302(2)(b)): line 23 is 0 and + # mt_elderly_homeowner_or_renter_credit_gross_rent counts it. + person = tax_unit.members + property_tax = person("real_estate_taxes", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + claimant_billed = tax_unit.sum(property_tax * head_or_spouse) > 0 + return where(claimant_billed, tax_unit.sum(property_tax), 0) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py new file mode 100644 index 00000000000..8be14dd130a --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_selected_claimant.py @@ -0,0 +1,68 @@ +from policyengine_us.model_api import * + + +class mt_elderly_homeowner_or_renter_credit_selected_claimant(Variable): + value_type = bool + entity = TaxUnit + label = "Selected claimant for the Montana Elderly Homeowner/Renter Credit" + definition_period = YEAR + defined_for = StateCode.MT + reference = ( + # § 15-30-2341(1): one claimant per household + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0410/0150-0300-0230-0410.html", + # § 15-30-2337(5), (7), (10): gross rent, household, property tax + # billed + "https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html", + # ARM 42.4.302 (implementing § 15-30-2341): property tax billed to + # another counts as rent; married couples living apart receive only + # one credit per year + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-42.4.302", + # 2025 Schedule 2EC: the attestation "I am the only member of my + # household claiming this credit"; line 23 "the property tax you were + # billed"; line 24 "the rent that you paid" + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2/2025_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=10", + # 2025 instructions: the attestation, the household definition and + # the joint filers' claimant + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=45", + # 2025 instructions, lines 23 and 24: property in another's name "can + # qualify as rent only"; "Your rent is only the amount of money that + # you paid to occupy your home" + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47", + ) + + def formula(tax_unit, period, parameters): + # "Only one claimant per household in a claim period ... is entitled + # to relief" (§ 15-30-2341(1)). Neither the statute nor ARM 42.4.302 + # says which member claims. Every eligible tax unit in a household has + # the same gross and net household income, so their credits differ + # only by Schedule 2EC line 26: property tax billed plus 15% of rent, + # before the $1,150 cap. The model counts each tax unit's own + # property tax and rent, following Schedule 2EC's line labels ("the + # property tax you were billed", "the rent that you paid"). A tax + # unit whose head and spouse are billed no property tax is treated as + # living in a home in another member's name, whose property tax "can + # qualify as rent only" (2025 instructions; ARM 42.4.302(2)(b)): it + # counts the household's property tax as rent, if that is larger + # (see mt_elderly_homeowner_or_renter_credit_gross_rent). The statute + # instead defines both for the homestead (§ 15-30-2337(5), (10)), + # which would give the one claimant the household's whole property + # tax and rent; that reading is not modeled. We select the tax unit + # whose credit is largest, the claim a household would choose. + # get_rank ranks the candidates in each household 0, 1, 2, ..., so a + # household with any candidate has exactly one at rank 0; its stable + # sort gives a tie to the first in member order. A joint return is one + # tax unit and one claim; the 2025 instructions name the spouse listed + # as the taxpayer as the claimant when both qualify, and the model + # holds that credit on the tax unit head. + # ARM 42.4.302(3)(a) also allows married couples living apart only + # one credit a year; a separated spouse in another tax unit is not + # linked to the other spouse, so that limit is not modeled. + person = tax_unit.members + head = person("is_tax_unit_head", period) + eligible = person("mt_elderly_homeowner_or_renter_credit_eligible", period) + credit = person( + "mt_elderly_homeowner_or_renter_credit_pre_one_claimant", period + ) + candidate = head & eligible + rank = person.get_rank(person.household, -credit, condition=candidate) + return tax_unit.any(candidate & (rank == 0))