diff --git a/changelog.d/mt-elderly-credit-government-assistance.fixed.md b/changelog.d/mt-elderly-credit-government-assistance.fixed.md new file mode 100644 index 00000000000..5c1b0d95f6f --- /dev/null +++ b/changelog.d/mt-elderly-credit-government-assistance.fixed.md @@ -0,0 +1 @@ +Montana's elderly homeowner/renter credit now counts SSI, TANF, workers' compensation and guaranteed income payments received by household members in gross household income (MCA 15-30-2337(9)(a); Schedule 2EC line 16). diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml index 7c3c2184cdf..fb84aef0128 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml @@ -4,27 +4,31 @@ metadata: period: year label: Montana elderly homeowner or renter credit gross income sources reference: - - title: HB191 (2021), section 1(9), all nontaxable income - href: https://archive.legmt.gov/bills/2021/HB0199/HB0191_X.pdf#page=2 - - title: Montana 2022 Form2 Individual Income Tax Instructions, Page47, Gross Household Income - href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47 + - title: MCA 15-30-2337(9)(a), all nontaxable income, including (vi) cash public assistance and relief + href: https://mca.legmt.gov/bills/mca/title_0150/chapter_0300/part_0230/section_0370/0150-0300-0230-0370.html + - title: MCA 15-30-2337(9)(a) (2009), the same definition + href: https://archive.legmt.gov/bills/2009/mca/15/30/15-30-2337.htm - title: 2022 Montana Code Annotated, Title 15, Chapter 30, Part 23, 15-30-2337 (9) href: https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-23/section-15-30-2337/ - - title: 2024 Montana Individual Income Tax Return - href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=46 + - title: HB191 (2021), section 1(9), all nontaxable income + href: https://archive.legmt.gov/bills/2021/HB0199/HB0191_X.pdf#page=2 - title: ARM 42.4.301(2) as in effect 8/20/2004 to 11/9/2017 ("elderly homeowner/renter credit refunds received") href: https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/788508cc-8418-41c8-926d-3dfb11af3fd6 - title: ARM 42.4.301(2) as in effect 11/10/2017 to 9/6/2024 ("federal refundable credits received" and "any state refundable credits received") href: https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/297c7a8d-f6f4-4b70-a7b7-fe1cedee53e7 - title: ARM 42.4.301(2) as in effect from 9/7/2024 ("federal refundable tax credits received" and "any state refundable tax credits received") href: https://rules.mt.gov/api/policy-library-public/collections/aec52c46-128e-4279-9068-8af5d5432d74/policies/c90ea93a-03a2-4605-a0f8-f2f3760198bf/document/768c8344-24ee-4950-9d5e-9d8fc97ec0f7 - - title: 2017 Montana Form 2EC instructions, Gross Household Income Worksheet, item 4 ("Any federal or state refundable credits including the 2EC credit") - href: https://taxsim.nber.org/historical_state_tax_forms/MT/2017/Form%202%202017%20Booklet.pdf#page=65 - - title: 2023 Montana Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 7, refundable credits received + - title: 2016 Montana Form 2EC instructions, Part II, disability benefits including SSI payments and workers' compensation, and cash public assistance and relief + href: https://taxsim.nber.org/historical_state_tax_forms/MT/2016/Form_2_booklet_2016.pdf#page=74 + - title: "2017 Montana Form 2EC instructions, Part II, disability benefits including SSI payments and workers' compensation, and cash public assistance and relief; Gross Household Income Worksheet, items 4 and 5 #page 64-65" + href: https://taxsim.nber.org/historical_state_tax_forms/MT/2017/Form%202%202017%20Booklet.pdf#page=64 + - title: "2022 Montana Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, gross household income; line 8, disability benefits including SSI payments and workers' compensation #page 47-48" + href: 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=47 + - title: "2023 Montana Form 2 instructions, Elderly Homeowner/Renter Credit Schedule, line 7, refundable credits received; line 8, disability benefits including SSI payments and workers' compensation #page 51-52" href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=51 - - title: 2024 Montana Form 2 instructions, Schedule 2EC, line 8, refundable credits received - href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=47 - - title: 2025 Montana Form 2 instructions, Schedule 2EC, line 8, refundable credits received + - title: "2024 Montana Form 2 instructions, Schedule 2EC, gross household income; line 8, refundable credits received; line 16, government assistance #page 46-48" + href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=46 + - title: "2025 Montana Form 2 instructions, Schedule 2EC, line 8, refundable credits received; line 16, government assistance #page 45-46" href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2025_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=45 values: @@ -43,6 +47,19 @@ values: - child_support_received # § 15-30-2337(9)(a)(iv) – support money - strike_benefits # § 15-30-2337(9)(a)(v) – nontaxable strike benefits - general_assistance # § 15-30-2337(9)(a)(vi) – cash public assistance + # Government assistance (Schedule 2EC line 16 from 2024; "disability + # benefits including ... SSI payments and workers' compensation" and + # "cash public assistance and relief" in earlier instructions). TANF is + # held for the SPM unit; tanf_person is each member's per capita share, + # so the household sum counts the unit's TANF once. LIEAP and SNAP are + # not cash public assistance (2014-2017 Form 2EC instructions). + - ssi + - tanf_person + - workers_compensation + # Guaranteed income pilot payments are outside federal AGI here, and + # (9)(a) counts all nontaxable income; a government-funded pilot is also + # "any other government assistance" (Schedule 2EC line 16). + - gi_cash_assistance # Refundable credits received (2023 schedule line 7, Schedule 2EC line 8 # from 2024): Montana's refundable credits other than this credit, # and this credit as received during the year (ARM 42.4.301(2)(c)). @@ -60,6 +77,10 @@ values: - child_support_received - strike_benefits - general_assistance + - ssi + - tanf_person + - workers_compensation + - gi_cash_assistance - mt_refundable_credits_before_renter_credit - mt_elderly_homeowner_or_renter_credit_received - mt_elderly_homeowner_or_renter_credit_federal_refundable_credits @@ -76,6 +97,10 @@ values: - child_support_received - strike_benefits - general_assistance + - ssi + - tanf_person + - workers_compensation + - gi_cash_assistance - mt_refundable_credits_before_renter_credit - mt_elderly_homeowner_or_renter_credit_received - mt_elderly_homeowner_or_renter_credit_federal_refundable_credits diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index 77815f66850..dac39e3fb98 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 @@ -351,6 +351,304 @@ mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] mt_elderly_homeowner_or_renter_credit: [1_130, 0] +# Government assistance in gross household income. § 15-30-2337(9)(a) +# counts "all nontaxable income, including but not limited to ... (vi) cash +# public assistance and relief". 2024 and 2025 Schedule 2EC line 16 names +# SSI payments, workers' compensation "and any other government assistance"; +# the 2014-2023 instructions list "Disability benefits including, but not +# limited to, veteran's disability, SSI payments and workers' compensation". + +- name: 2024 Schedule 2EC worked case, the claimant's SSI counts on line 16 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 15_000 + ssi: 6_000 + real_estate_taxes: 1_200 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Line 5 pensions 15_000; line 16 SSI 6_000; line 18 = 21_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 21_000 + # Line 19 12_600; line 20 = 8_400; line 21 = 0.039 (8_000 to 8_999); + # line 22 = 8_400 x 0.039 = 327.60. + mt_elderly_homeowner_or_renter_credit_net_household_income: 327.60 + # Line 23 = 1_200; line 26 = 1_200; line 27 = 1_200 - 327.60 = 872.40; + # line 28 = 872.40 (under 1_150); line 29 = 100%; line 30 = 872.40. + # Without the SSI, line 18 = 15_000, line 22 = 2_400 x 0.006 = 14.40 and + # the credit would be the 1_150 cap. + mt_elderly_homeowner_or_renter_credit: 872.40 + + +- name: 2025 Schedule 2EC worked case, a household member's workers' compensation counts on line 17 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + taxable_pension_income: 18_000 + real_estate_taxes: 1_500 + person2: + age: 45 + workers_compensation: 9_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The claimant's adult son lives in the household and files his own + # return. Line 5 pensions 18_000; line 17 his workers' compensation, a + # line 16 item for another member, 9_000; line 18 = 27_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [18_000, 9_000] + # Line 20 = 27_000 - 12_600 = 14_400; line 21 = 0.05; line 22 = 720. + mt_elderly_homeowner_or_renter_credit_net_household_income: [720, 0] + # Line 27 = 1_500 - 720 = 780; line 28 = 780; line 29 = 100%; line 30 = + # 780. Without his workers' compensation, line 18 = 18_000, line 22 = + # 5_400 x 0.028 = 151.20 and the credit would be the 1_150 cap. + mt_elderly_homeowner_or_renter_credit: [780, 0] + + +- name: 2024 Schedule 2EC worked case, a household member's guaranteed income payments count on line 17 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 16_000 + real_estate_taxes: 1_200 + person2: + age: 35 + gi_cash_assistance: 6_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The claimant's daughter files her own return and receives 6_000 from a + # guaranteed income pilot, which is outside federal AGI. § 15-30-2337(9)(a) + # counts "all nontaxable income", and line 16 counts "any other + # government assistance". Line 5 = 16_000; line 17 = 6_000; line 18 = + # 22_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [16_000, 6_000] + # Line 20 = 9_400; line 21 = 0.042; line 22 = 394.80. + mt_elderly_homeowner_or_renter_credit_net_household_income: [394.80, 0] + # Line 27 = 1_200 - 394.80 = 805.20; line 28 = 805.20; line 29 = 100%; + # line 30 = 805.20. Without the payments, line 18 = 16_000, line 22 = + # 3_400 x 0.016 = 54.40 and the credit 1_145.60. + mt_elderly_homeowner_or_renter_credit: [805.20, 0] + + +- name: 2024 Schedule 2EC worked case, TANF in a shared SPM unit counts once across two returns + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 1_500 + person2: + age: 30 + person3: + age: 5 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + spm_units: + spm_unit: + members: [person1, person2, person3] + tanf: 4_800 + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # The claimant's daughter and granddaughter live with her; the daughter + # files her own return and receives 4_800 of TANF. Line 5 pensions + # 20_000; line 17 the TANF 4_800; line 18 = 24_800. The model holds TANF + # for the SPM unit and counts each member's per capita share + # (tanf_person, 1_600 each), so the household total counts it once. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [21_600, 1_600, 1_600] + # Line 20 = 24_800 - 12_600 = 12_200; line 21 = 0.05; line 22 = 610. + mt_elderly_homeowner_or_renter_credit_net_household_income: [610, 0, 0] + # Line 27 = 1_500 - 610 = 890; line 28 = 890; line 29 = 100%; line 30 = + # 890. Without the TANF, line 18 = 20_000, line 22 = 7_400 x 0.035 = 259 + # and the credit would be the 1_150 cap. Counting the whole 4_800 for + # each of the three members would give line 18 = 34_400 and a credit of + # 1_500 - 1_090 = 410. + mt_elderly_homeowner_or_renter_credit: [890, 0, 0] + + +- name: 2024 Schedule 2EC worked case, Montana TANF paid to a separate SPM unit counts on line 17 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 1_500 + person2: + age: 30 + person3: + age: 5 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2, person3] + mt_tanf: 4_800 + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # The same household with the daughter's family in its own SPM unit and + # the payment entered as Montana TANF (ARM 37.78), which tanf sums with + # the other states' programs. Line 17 = 4_800; line 18 = 24_800. + tanf: [0, 4_800] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 2_400, 2_400] + # Line 22 = (24_800 - 12_600) x 0.05 = 610; line 27 = 890; line 30 = 890. + mt_elderly_homeowner_or_renter_credit_net_household_income: [610, 0, 0] + mt_elderly_homeowner_or_renter_credit: [890, 0, 0] + + +- name: 2023 Elderly Homeowner/Renter Credit Schedule worked case, workers' compensation and a spouse's SSI count on line 8 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + taxable_pension_income: 14_000 + workers_compensation: 6_000 + real_estate_taxes: 1_000 + person2: + age: 64 + ssi: 5_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Joint return. Line 1 federal AGI 14_000; line 8 "Disability benefits + # including, but not limited to, veteran's disability, SSI payments, and + # workers' compensation": 6_000 + 5_000 = 11_000; line 10 = 25_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 5_000] + # Line 11 12_600; line 12 = 12_400; line 13 = 0.05; line 14 = 620. + mt_elderly_homeowner_or_renter_credit_net_household_income: [620, 0] + # Line 15 = 1_000; line 18 = 1_000; line 19 = 1_000 - 620 = 380; line 20 + # = 380; line 21 = 100%; line 22 = 380. Without either amount, line 10 = + # 14_000, line 12 = 1_400 at a 0 rate and the credit would be 1_000. + mt_elderly_homeowner_or_renter_credit: [380, 0] + + +- name: 2017 Form 2EC worked case, a household member's SSI counts in gross household income + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 15_000 + real_estate_taxes: 1_000 + person2: + age: 40 + # 2017 federal benefit rate: 735 a month. + ssi: 8_820 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The claimant's adult son receives SSI. Gross Household Income + # Worksheet: line 6 pensions 15_000; line 7 income of other household + # members, his SSI 8_820 ("Disability benefits including ... SSI + # payments"); line 8 = 23_820, Form 2EC line 1. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [15_000, 8_820] + # Line 2 6_300; line 3 = 17_520; line 4 = 0.05; line 5 = 876. + mt_elderly_homeowner_or_renter_credit_net_household_income: [876, 0] + # Line 6 = 1_000; line 9 = 1_000; line 10 = 1_000 - 876 = 124; line 11 = + # 124 (under 1_000); line 12 = 100%; line 13 = 124. Without his SSI, line + # 1 = 15_000, line 5 = 8_700 x 0.039 = 339.30 and the credit 660.70. + mt_elderly_homeowner_or_renter_credit: [124, 0] + + +- name: 2016 Form 2EC worked case, cash public assistance received by another household member counts + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 12_000 + real_estate_taxes: 900 + person2: + age: 30 + person3: + age: 5 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2, person3] + tanf: 5_400 + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # Gross Household Income Worksheet: line 8 pensions 12_000; the + # daughter's 5_400 of TANF, cash public assistance (line 7), as income of + # another household member (line 9); line 10 = 17_400, Form 2EC line 1. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [12_000, 2_700, 2_700] + # Line 2 6_300; line 3 = 11_100; line 4 = 0.048; line 5 = 532.80. + mt_elderly_homeowner_or_renter_credit_net_household_income: [532.80, 0, 0] + # Line 6 = 900; line 10 = 900 - 532.80 = 367.20; line 11 = 367.20; line + # 12 = 100%; line 13 = 367.20. Without the TANF, line 1 = 12_000, line 5 + # = 5_700 x 0.028 = 159.60 and the credit 740.40. + mt_elderly_homeowner_or_renter_credit: [367.20, 0, 0] + - name: 2024 Schedule 2EC worked case, a dependent's wages move the credit into the 40 percent band period: 2024 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml index ec00f8c2fad..442caea56f2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml @@ -8,6 +8,9 @@ child_support_received: 3_000 strike_benefits: 2_000 general_assistance: 1_000 + # These amounts would make the model compute SSI, which also counts; + # its own cases are in the government assistance test file. + ssi: 0 tax_units: tax_unit: members: [person1] @@ -86,6 +89,29 @@ # income: (20_000 - 12_600) * 0.035 = 259, allocated to the head. mt_elderly_homeowner_or_renter_credit_net_household_income: [259, 0] +- name: SNAP is not cash public assistance and does not count + period: 2017 + input: + people: + person1: + age: 70 + taxable_pension_income: 15_000 + spm_units: + spm_unit: + members: [person1] + snap: 1_800 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # 2017 Form 2EC instructions: "Cash, public assistance and relief - Do + # not include LIEAP (Low Income Energy Assistance Program) or SNAP". + mt_elderly_homeowner_or_renter_credit_gross_household_income: 15_000 + - name: A dependent's wages count in gross household income period: 2024 input: @@ -117,6 +143,8 @@ people: person1: age: 70 + # No SSI for the claimant, so the case isolates the dependent's income. + ssi: 0 person2: age: 17 employment_income: 25_000 @@ -144,6 +172,8 @@ people: person1: age: 70 + # No SSI for the claimant, so the case isolates the dependent's income. + ssi: 0 person2: age: 16 employment_income: 10_000 diff --git a/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py b/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py index 90736451eca..819ce2ce5ff 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py +++ b/policyengine_us/tests/test_mt_elderly_credit_dependent_deductions.py @@ -31,6 +31,8 @@ def build_situation(cases, year): "age": {year: 70}, "taxable_pension_income": {year: case["pension"]}, "early_withdrawal_penalty": {year: case["parent_penalty"]}, + # No computed SSI; it has its own tests (government assistance). + "ssi": {year: 0}, "is_tax_unit_dependent": {year: False}, "mt_refundable_credits_before_renter_credit": {year: 0}, "mt_elderly_homeowner_or_renter_credit_received": {year: 0}, diff --git a/policyengine_us/tests/test_mt_elderly_credit_government_assistance.py b/policyengine_us/tests/test_mt_elderly_credit_government_assistance.py new file mode 100644 index 00000000000..aaf5fa438f5 --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_government_assistance.py @@ -0,0 +1,323 @@ +"""Property tests: government assistance in the Montana elderly +homeowner/renter credit. + +MCA 15-30-2337(9)(a) counts "all nontaxable income, including but not +limited to ... (vi) cash public assistance and relief", and (4) counts "all +income received by all individuals of a household". The 2024 Schedule 2EC +line 16 names "supplemental security income (SSI) payments, worker's +compensation, and any other government assistance"; line 17 carries the same +items for the other members of the household. + +The model holds SSI, workers' compensation and guaranteed income payments +per person and TANF per SPM unit, which it spreads over the unit's members +(tanf_person). For a claimant aged 62 or older (taxable pension, SSI, +workers' compensation, property tax, rent) living with another adult (SSI, +workers' compensation, guaranteed income payments) and a child, whose SPM +unit receives TANF, in four arrangements: + + A. one return, the other adult and the child the claimant's dependents, and + one SPM unit; + B. two returns (the claimant's, and the other adult's with the child) and + one SPM unit; + C. two returns and two SPM units, the TANF going to the other adult's; + D. as C, with more of one kind of assistance; + +1. The household's gross household income is the pension plus every + member's SSI, workers' compensation and guaranteed income payments plus + the TANF, once, in A, B and C. +2. So the claimant's credit is the same in A, B and C, and nobody else + claims. +3. The credit equals an independent reading of Schedule 2EC lines 18 to 30, + is between zero and the cap, and is zero at $45,000 or more (line 29). +4. More assistance never raises the credit (D against C). + +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. + +YAML cases (mt_elderly_homeowner_or_renter_credit.yaml) pin fixed worked +examples. This file adds what they cannot: randomized amounts across +arrangements that must agree, an independent Schedule 2EC reference for the +credit, and monotonicity. Each example is one small Simulation on the shared +tax-benefit system. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEAR = 2024 +CAP = 1_150 +INCOME_LIMIT = 45_000 +TOLERANCE = 0.01 +ARRANGEMENTS = "ABCD" +# 2024 Schedule 2EC, line 19. +EXCLUSION = 12_600 +# Household Income Reduction Table (line 21): lower bounds and rates. +REDUCTION_TABLE = [ + (0, 0), + (2_000, 0.006), + (3_000, 0.016), + (4_000, 0.024), + (5_000, 0.028), + (6_000, 0.032), + (7_000, 0.035), + (8_000, 0.039), + (9_000, 0.042), + (10_000, 0.045), + (11_000, 0.048), + (12_000, 0.05), +] +# Credit Multiplier Table (line 29): lower bounds of line 18 and percentages. +MULTIPLIER_TABLE = [ + (0, 1), + (35_000, 0.4), + (37_501, 0.3), + (40_001, 0.2), + (42_501, 0.1), + (45_000, 0), +] +EXTRA_KINDS = [ + "claimant_ssi", + "member_workers_compensation", + "member_gi_cash_assistance", + "tanf", +] + + +def amounts(c, arrangement): + """The case's assistance amounts, with D's extra amount added.""" + a = { + "claimant_ssi": c["claimant_ssi"], + "claimant_workers_compensation": c["claimant_workers_compensation"], + "member_ssi": c["member_ssi"], + "member_workers_compensation": c["member_workers_compensation"], + "member_gi_cash_assistance": c["member_gi_cash_assistance"], + "tanf": c["tanf"], + } + if arrangement == "D": + a[c["extra_kind"]] += c["extra"] + return a + + +def build_situation(cases): + people, tax_units, marital_units, spm_units, families, households = ( + {}, + {}, + {}, + {}, + {}, + {}, + ) + for i, c in enumerate(cases): + for arrangement in ARRANGEMENTS: + key = f"{arrangement}{i}" + a = amounts(c, arrangement) + claimant, member, child = ( + f"claimant_{key}", + f"member_{key}", + f"child_{key}", + ) + people[claimant] = { + "age": {YEAR: c["claimant_age"]}, + "taxable_pension_income": {YEAR: c["pension"]}, + "ssi": {YEAR: a["claimant_ssi"]}, + "workers_compensation": {YEAR: a["claimant_workers_compensation"]}, + "real_estate_taxes": {YEAR: c["property_tax"]}, + "rent": {YEAR: c["rent"]}, + "is_tax_unit_dependent": {YEAR: False}, + } + people[member] = { + "age": {YEAR: c["member_age"]}, + "ssi": {YEAR: a["member_ssi"]}, + "workers_compensation": {YEAR: a["member_workers_compensation"]}, + "gi_cash_assistance": {YEAR: a["member_gi_cash_assistance"]}, + "is_tax_unit_dependent": {YEAR: arrangement == "A"}, + } + people[child] = { + "age": {YEAR: c["child_age"]}, + "is_tax_unit_dependent": {YEAR: True}, + } + if arrangement == "A": + tax_units[f"tax_unit_{key}"] = {"members": [claimant, member, child]} + else: + tax_units[f"tax_unit_{key}"] = {"members": [claimant]} + tax_units[f"tax_unit_{key}_member"] = {"members": [member, child]} + if arrangement in "AB": + spm_units[f"spm_unit_{key}"] = { + "members": [claimant, member, child], + "tanf": {YEAR: a["tanf"]}, + } + else: + spm_units[f"spm_unit_{key}"] = { + "members": [claimant], + "tanf": {YEAR: 0}, + } + spm_units[f"spm_unit_{key}_member"] = { + "members": [member, child], + "tanf": {YEAR: a["tanf"]}, + } + for person in (claimant, member, child): + marital_units[f"marital_unit_{person}"] = {"members": [person]} + families[f"family_{key}"] = {"members": [claimant, member, child]} + households[f"household_{key}"] = { + "members": [claimant, member, child], + "state_code": {YEAR: "MT"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": spm_units, + "families": families, + "households": households, + } + + +def calculate(cases): + """Household totals of gross household income and the credit, and the + other members' credits, as arrays indexed [arrangement, case].""" + simulation = Simulation(situation=build_situation(cases)) + n, k = len(cases), len(ARRANGEMENTS) + + def by_household(variable): + values = simulation.calculate(variable, YEAR, map_to="household") + return np.asarray(values).reshape(n, k).T + + person_credit = np.asarray( + simulation.calculate("mt_elderly_homeowner_or_renter_credit", YEAR) + ).reshape(n, k, 3) + return { + "gross_household_income": by_household( + "mt_elderly_homeowner_or_renter_credit_gross_household_income" + ), + "credit": by_household("mt_elderly_homeowner_or_renter_credit"), + # The other adult's and the child's credits. + "others_credit": person_credit[:, :, 1:].transpose(1, 0, 2), + } + + +def expected_gross_household_income(c, arrangement): + """2024 Schedule 2EC line 18: line 5 pension and line 16 assistance for + the claimant, line 17 the other members' assistance and guaranteed income + payments, the TANF once.""" + return c["pension"] + sum(amounts(c, arrangement).values()) + + +def lookup(table, amount): + rate = 0 + for lower, value in table: + if amount >= lower: + rate = value + return rate + + +def expected_credit(c, gross_household_income): + """2024 Schedule 2EC lines 19 to 30 for a claimant who owns or rents.""" + line_20 = max(gross_household_income - EXCLUSION, 0) + line_22 = line_20 * lookup(REDUCTION_TABLE, line_20) + line_26 = c["property_tax"] + 0.15 * c["rent"] + line_27 = max(line_26 - line_22, 0) + line_28 = min(line_27, CAP) + return line_28 * lookup(MULTIPLIER_TABLE, gross_household_income) + + +def tolerance(amount): + return max(TOLERANCE, 8 * float(np.spacing(np.float32(abs(amount))))) + + +def assert_properties(cases): + result = calculate(cases) + ghi, credit = result["gross_household_income"], result["credit"] + for i, c in enumerate(cases): + for j, arrangement in enumerate(ARRANGEMENTS): + expected = expected_gross_household_income(c, arrangement) + tol = tolerance(expected) + # 1. Every member's assistance counts, the TANF once. + assert ghi[j, i] == pytest.approx(expected, abs=tol), (arrangement, c) + # 3. The schedule, the bounds and the income limit. + assert credit[j, i] == pytest.approx( + expected_credit(c, expected), abs=tol + ), (arrangement, c) + assert -tol <= credit[j, i] <= CAP + tol, (arrangement, c) + if expected >= INCOME_LIMIT: + assert credit[j, i] == 0, (arrangement, c) + # 2. The arrangement does not change the credit, and only the + # claimant claims. + tol = tolerance(expected_gross_household_income(c, "D")) + assert credit[1, i] == pytest.approx(credit[0, i], abs=tol), c + assert credit[2, i] == pytest.approx(credit[0, i], abs=tol), c + assert np.all(result["others_credit"][:, i] == 0), c + # 4. More assistance never raises the credit. + assert credit[3, i] <= credit[2, i] + tol, c + + +def test_worked_example(): + """2024 Schedule 2EC: line 5 pension 20,000; line 16 the claimant's SSI + 3,000; line 17 the other adult's SSI 2,000 and workers' compensation + 1,000 and the TANF 1,800; line 18 = 27,800; line 22 = (27,800 - 12,600) + x 0.05 = 760; line 27 = 1,500 - 760 = 740; line 30 = 740 in A, B and C. + D adds 8,000 of TANF: line 18 = 35,800; line 22 = 1,160; line 27 = 340; + line 29 = 40%; line 30 = 136.""" + case = { + "claimant_age": 70, + "pension": 20_000, + "claimant_ssi": 3_000, + "claimant_workers_compensation": 0, + "property_tax": 1_500, + "rent": 0, + "member_age": 35, + "member_ssi": 2_000, + "member_workers_compensation": 1_000, + "member_gi_cash_assistance": 0, + "child_age": 6, + "tanf": 1_800, + "extra_kind": "tanf", + "extra": 8_000, + } + result = calculate([case]) + assert result["gross_household_income"][:, 0] == pytest.approx( + [27_800, 27_800, 27_800, 35_800], abs=TOLERANCE + ) + assert result["credit"][:, 0] == pytest.approx([740, 740, 740, 136], abs=TOLERANCE) + assert_properties([case]) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + @st.composite + def case(draw): + return { + "claimant_age": draw(st.integers(min_value=62, max_value=90)), + "pension": draw(amount(40_000)), + "claimant_ssi": draw(amount(12_000)), + "claimant_workers_compensation": draw(amount(15_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=18, max_value=61)), + "member_ssi": draw(amount(12_000)), + "member_workers_compensation": draw(amount(15_000)), + "member_gi_cash_assistance": draw(amount(12_000)), + "child_age": draw(st.integers(min_value=0, max_value=17)), + "tanf": draw(amount(10_000)), + "extra_kind": draw(st.sampled_from(EXTRA_KINDS)), + "extra": draw(st.integers(min_value=1, max_value=20_000)), + } + + @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.given(st.lists(case(), min_size=1, max_size=8)) + def test_properties(cases): + assert_properties(cases) diff --git a/policyengine_us/tests/test_mt_elderly_credit_household_members.py b/policyengine_us/tests/test_mt_elderly_credit_household_members.py index 2d551fb39fd..bb0ef7d06ec 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_household_members.py +++ b/policyengine_us/tests/test_mt_elderly_credit_household_members.py @@ -79,6 +79,9 @@ def build_situation(cases): "social_security_retirement": {YEAR: c["claimant_ss"]}, "real_estate_taxes": {YEAR: c["property_tax"]}, "rent": {YEAR: c["rent"]}, + # The model would compute SSI for a claimant with little + # income; SSI has its own tests (government assistance). + "ssi": {YEAR: 0}, } people[member] = { "age": {YEAR: c["member_age"]}, diff --git a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py index d1dc874a300..ae224c8dea5 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py +++ b/policyengine_us/tests/test_mt_elderly_credit_one_claimant.py @@ -68,6 +68,10 @@ def build_situation(households): "taxable_pension_income": {YEAR: adult["pension"]}, "taxable_interest_income": {YEAR: adult["interest"]}, "social_security_retirement": {YEAR: adult["social_security"]}, + # The model would compute SSI for an adult with + # little income; SSI has its own tests (government + # assistance), so the reference leaves it out. + "ssi": {YEAR: 0}, "is_tax_unit_dependent": {YEAR: False}, } unit_members.append(person) diff --git a/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py b/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py index 9f44d62671b..09470be0d6f 100644 --- a/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py +++ b/policyengine_us/tests/test_mt_elderly_credit_rebate_receipts.py @@ -35,6 +35,8 @@ def build_situation(cases, year, with_receipts): }, "mt_refundable_credits_before_renter_credit": {year: 0}, "mt_elderly_homeowner_or_renter_credit_received": {year: 0}, + # No computed SSI; it has its own tests. + "ssi": {year: 0}, } if arrangement == "C": people[name][