diff --git a/changelog.d/fix-mt-salt-cap-joint-return.fixed.md b/changelog.d/fix-mt-salt-cap-joint-return.fixed.md new file mode 100644 index 00000000000..512c8a641c6 --- /dev/null +++ b/changelog.d/fix-mt-salt-cap-joint-return.fixed.md @@ -0,0 +1 @@ +Cap Montana's 2021–2023 state and local tax deduction (Itemized Deductions Schedule, line 5) once for a joint return and at $5,000 in each spouse's column when filing separately on the same form, and include general sales taxes (line 5a) and local income taxes (line 5b). diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml new file mode 100644 index 00000000000..8b6d63360ab --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml @@ -0,0 +1,16 @@ +description: Montana computes itemized deductions on its own Itemized Deductions Schedule, in lieu of adjusted federal itemized deductions, if this is true. +metadata: + label: Montana state specific itemized deductions apply + period: year + unit: bool + reference: + - title: Montana Individual Income Tax Return (Form 2) 2023 | Itemized Deductions Schedule + href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2023_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7 + - title: 2024 Montana Form 2 Instructions | Discontinued Worksheets and Schedules + 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=8 + - title: 2024 Montana Form 2 Instructions | Line 2 and Worksheet A, Adjusted Federal Itemized Deductions + 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=15 + +values: + 2021-01-01: true + 2024-01-01: false diff --git a/policyengine_us/tests/policy/baseline/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.yaml b/policyengine_us/tests/policy/baseline/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.yaml new file mode 100644 index 00000000000..1096df63047 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.yaml @@ -0,0 +1,21 @@ +- name: Each person owes 1% of their own Kansas City taxable earnings + period: 2024 + input: + people: + person1: + age: 45 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + person2: + age: 45 + mo_kansas_city_earnings_tax_taxable_earnings: 50_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MO + output: + # 1% x 100,000 and 1% x 50,000. + mo_kansas_city_earnings_tax_person: [1_000, 500] + mo_kansas_city_earnings_tax: 1_500 diff --git a/policyengine_us/tests/policy/baseline/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.yaml b/policyengine_us/tests/policy/baseline/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.yaml new file mode 100644 index 00000000000..dc90c9bc2c9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.yaml @@ -0,0 +1,24 @@ +- name: A person's credit reduces only their own St. Louis earnings tax + period: 2024 + input: + people: + person1: + age: 45 + mo_st_louis_earnings_tax_taxable_earnings: 100_000 + mo_st_louis_earnings_tax_credit: 1_200 + person2: + age: 45 + mo_st_louis_earnings_tax_taxable_earnings: 100_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MO + output: + # 1% x 100,000 = 1,000 each. Person 1's 1,200 credit takes their tax to + # zero and does not reduce person 2's. + mo_st_louis_earnings_tax_person: [0, 1_000] + mo_st_louis_earnings_tax: 1_000 + mo_st_louis_earnings_tax_before_credit: 2_000 diff --git a/policyengine_us/tests/policy/baseline/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.yaml b/policyengine_us/tests/policy/baseline/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.yaml new file mode 100644 index 00000000000..f7b94f9bcbb --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.yaml @@ -0,0 +1,37 @@ +- name: Each person's Philadelphia wage tax uses their own rate + period: 2024 + input: + people: + person1: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 100_000 + pa_philadelphia_wage_tax_resident: true + person2: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 100_000 + pa_philadelphia_wage_tax_resident: false + person3: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 40_000 + pa_philadelphia_wage_tax_resident: true + pa_philadelphia_wage_tax_reduced_rate_eligible: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: PA + output: + # Rates from July 2023: resident 3.75%, nonresident 3.44%, reduced 1.5%. + pa_philadelphia_wage_tax_person: [3_750, 3_440, 600] + pa_philadelphia_wage_tax: 7_790 + +- name: No Philadelphia wages, no tax + period: 2024 + input: + pa_philadelphia_wage_tax_taxable_wages: 0 + state_code: PA + output: + pa_philadelphia_wage_tax_person: 0 + pa_philadelphia_wage_tax: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml index 4435e64a801..47b6f0f8a7c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml @@ -3,7 +3,7 @@ input: charitable_deduction: 800 investment_interest_expense: 265 - mt_salt_deduction: 5_000 + mt_salt_deduction_indiv: 5_000 mt_medical_expense_deduction_indiv: 1_000 casualty_loss_deduction: 400 mt_child_dependent_care_expense_deduction: 123 @@ -17,7 +17,7 @@ input: charitable_deduction: 0 investment_interest_expense: 1_234 - mt_salt_deduction: 10_000 + mt_salt_deduction_indiv: 10_000 mt_medical_expense_deduction_indiv: 3_000 casualty_loss_deduction: 0 mt_child_dependent_care_expense_deduction: 0 @@ -31,7 +31,7 @@ input: charitable_deduction: 1_000 mortgage_interest: 34 - mt_salt_deduction: 1_035 + mt_salt_deduction_indiv: 1_035 mt_medical_expense_deduction_indiv: 3_000 casualty_loss_deduction: 745 mt_child_dependent_care_expense_deduction: 1_004 @@ -49,20 +49,20 @@ is_tax_unit_head_or_spouse: true mt_misc_deductions: 100 mt_medical_expense_deduction_indiv: 200 - mt_salt_deduction: 300 + mt_salt_deduction_indiv: 300 mt_federal_income_tax_deduction_for_federal_itemization_indiv: 400 person2: is_tax_unit_head_or_spouse: true mt_misc_deductions: 100 mt_medical_expense_deduction_indiv: 200 - mt_salt_deduction: 300 + mt_salt_deduction_indiv: 300 mt_federal_income_tax_deduction_for_federal_itemization_indiv: 100 person3: is_tax_unit_head_or_spouse: false is_tax_unit_dependent: true mt_misc_deductions: 100 mt_medical_expense_deduction_indiv: 12 - mt_salt_deduction: 200 + mt_salt_deduction_indiv: 200 mt_federal_income_tax_deduction_for_federal_itemization_indiv: 100 tax_units: tax_unit: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml index 2bc390f8ad3..c10c66239d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml @@ -3,7 +3,7 @@ input: charitable_deduction: 800 investment_interest_expense: 265 - mt_salt_deduction: 5_000 + mt_salt_deduction_indiv: 5_000 mt_medical_expense_deduction_indiv: 1_000 casualty_loss_deduction: 400 mt_child_dependent_care_expense_deduction: 123 @@ -17,7 +17,7 @@ input: charitable_deduction: 0 investment_interest_expense: 1_234 - mt_salt_deduction: 10_000 + mt_salt_deduction_indiv: 10_000 mt_medical_expense_deduction_indiv: 3_000 casualty_loss_deduction: 0 mt_child_dependent_care_expense_deduction: 0 @@ -31,7 +31,7 @@ input: charitable_deduction: 1_000 mortgage_interest: 34 - mt_salt_deduction: 1_035 + mt_salt_deduction_indiv: 1_035 mt_medical_expense_deduction_indiv: 3_000 casualty_loss_deduction: 745 mt_child_dependent_care_expense_deduction: 1_004 @@ -49,20 +49,20 @@ is_tax_unit_head_or_spouse: true mt_misc_deductions: 100 mt_medical_expense_deduction_indiv: 200 - mt_salt_deduction: 300 + mt_salt_deduction_indiv: 300 mt_federal_income_tax_deduction_indiv: 400 person2: is_tax_unit_head_or_spouse: true mt_misc_deductions: 100 mt_medical_expense_deduction_indiv: 200 - mt_salt_deduction: 300 + mt_salt_deduction_indiv: 300 mt_federal_income_tax_deduction_indiv: 100 person3: is_tax_unit_head_or_spouse: false is_tax_unit_dependent: true mt_misc_deductions: 100 mt_medical_expense_deduction_indiv: 12 - mt_salt_deduction: 200 + mt_salt_deduction_indiv: 200 mt_federal_income_tax_deduction_indiv: 100 tax_units: tax_unit: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction.yaml index 1e3da2a4f1b..51d97882b53 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction.yaml @@ -35,3 +35,151 @@ output: mt_salt_deduction: 10_000 +- name: Test case 5, head of household, higher than cap + period: 2022 + input: + filing_status: HEAD_OF_HOUSEHOLD + real_estate_taxes: 12_000 + state_code: MT + output: + mt_salt_deduction: 10_000 + +# Form 2, Itemized Deductions Schedule, line 5 (2021-2023): "Add lines 5a +# through 5d, enter the total here, but not more than $10,000 if your status +# is single, head of household or married filing jointly". A joint return has +# one column, so the couple's taxes are added and capped once. + +- name: Joint return caps the couple's combined state and local taxes once + period: 2023 + input: + people: + person1: + age: 45 + real_estate_taxes: 8_000 + person2: + age: 45 + real_estate_taxes: 8_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 5c: 8,000 + 8,000 = 16,000; line 5: min(16,000, 10,000). + mt_salt_deduction: [10_000, 0] + # No income, so the federal income tax deduction (line 4) is zero and + # line 5 is the only itemized deduction. + mt_itemized_deductions_joint: [10_000, 0] + +- name: Joint return below the cap deducts both spouses' taxes + period: 2022 + input: + people: + person1: + age: 45 + real_estate_taxes: 6_000 + person2: + age: 45 + real_estate_taxes: 2_500 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 5c: 6,000 + 2,500 = 8,500, under the 10,000 cap. + mt_salt_deduction: [8_500, 0] + +- name: Joint return adds general sales taxes before the cap + period: 2021 + input: + people: + person1: + age: 45 + real_estate_taxes: 9_000 + person2: + age: 45 + tax_units: + tax_unit: + members: [person1, person2] + state_sales_tax: 1_500 + local_sales_tax: 300 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 5a: 1,500 + 300 = 1,800; line 5c: 9,000. Line 5: min(10,800, 10,000). + mt_salt_deduction: [10_000, 0] + +- name: Single return adds general sales taxes below the cap + period: 2023 + input: + filing_status: SINGLE + real_estate_taxes: 2_000 + state_sales_tax: 1_500 + local_sales_tax: 300 + state_code: MT + output: + # Line 5a: 1,800; line 5c: 2,000. Line 5: 3,800, under the 10,000 cap. + mt_salt_deduction: 3_800 + +- name: Single return adds another city's earnings tax + period: 2023 + input: + filing_status: SINGLE + real_estate_taxes: 2_000 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + state_code: MT + output: + # Line 5b: Kansas City earnings tax, 1% x 100,000 = 1,000; line 5c: + # 2,000. Line 5: 3,000. + mt_salt_deduction: 3_000 + +- name: Joint return adds a spouse's city earnings tax before the cap + period: 2022 + input: + people: + person1: + age: 45 + real_estate_taxes: 9_500 + person2: + age: 45 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 5b: 1,000; line 5c: 9,500. Line 5: min(10,500, 10,000). + mt_salt_deduction: [10_000, 0] + +- name: Joint return adds both spouses' city taxes before the cap + period: 2023 + input: + people: + person1: + age: 45 + real_estate_taxes: 4_000 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + person2: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 100_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 5b: 1,000 (Kansas City) + 3,440 (Philadelphia nonresident 3.44%); + # line 5c: 4,000. Line 5: 8,440, under the 10,000 cap. + mt_salt_deduction: [8_440, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction_indiv.yaml new file mode 100644 index 00000000000..fcb4eb1a0b1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_salt_deduction_indiv.yaml @@ -0,0 +1,266 @@ +# Form 2, Itemized Deductions Schedule, line 5 (2021-2023): "Add lines 5a +# through 5d, enter the total here, but not more than $10,000 if your status +# is single, head of household or married filing jointly; or $5,000 if you are +# married filing separately." Spouses filing separately on the same form +# (filing status 2a) allocate between columns A and B, "with each column +# constituting its own return" (2023 instructions, PDF p. 12). So each +# spouse's column is capped at $5,000. + +- name: Each spouse's column is capped at the married filing separately amount + period: 2023 + input: + people: + person1: + age: 45 + real_estate_taxes: 8_000 + person2: + age: 45 + real_estate_taxes: 8_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Column A: min(8,000, 5,000); column B: min(8,000, 5,000). + mt_salt_deduction_indiv: [5_000, 5_000] + # No income, so the federal income tax deduction (line 4) is zero and + # line 5 is each column's only itemized deduction. + mt_itemized_deductions_indiv: [5_000, 5_000] + +- name: A spouse below the cap deducts their own taxes + period: 2022 + input: + people: + person1: + age: 45 + real_estate_taxes: 8_000 + person2: + age: 45 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Column A: min(8,000, 5,000); column B: 2,000. + mt_salt_deduction_indiv: [5_000, 2_000] + +- name: The couple's general sales taxes are divided equally between the columns + period: 2021 + input: + people: + person1: + age: 45 + real_estate_taxes: 4_800 + person2: + age: 45 + tax_units: + tax_unit: + members: [person1, person2] + state_sales_tax: 1_000 + local_sales_tax: 0 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 5a: 1,000 / 2 = 500 in each column. + # Column A: min(4,800 + 500, 5,000) = 5,000; column B: 500. + mt_salt_deduction_indiv: [5_000, 500] + +- name: A dependent has no column + period: 2023 + input: + people: + person1: + age: 45 + real_estate_taxes: 3_000 + person2: + age: 45 + real_estate_taxes: 1_000 + person3: + age: 20 + is_tax_unit_dependent: true + real_estate_taxes: 1_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_salt_deduction_indiv: [3_000, 1_000, 0] + +- name: A single filer's one column takes the single cap + period: 2023 + input: + filing_status: SINGLE + real_estate_taxes: 12_000 + state_code: MT + output: + # min(12,000, 10,000) + mt_salt_deduction_indiv: 10_000 + +- name: A single filer below the single cap deducts all taxes + period: 2022 + input: + filing_status: SINGLE + real_estate_taxes: 8_000 + state_code: MT + output: + mt_salt_deduction_indiv: 8_000 + +- name: A married person filing on a separate form takes the separate cap + period: 2021 + input: + filing_status: SEPARATE + real_estate_taxes: 6_000 + state_code: MT + output: + # min(6,000, 5,000) + mt_salt_deduction_indiv: 5_000 + +- name: No separate columns from 2024 + period: 2024 + input: + people: + person1: + age: 45 + real_estate_taxes: 8_000 + person2: + age: 45 + real_estate_taxes: 8_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # SB 399 ended filing separately on the same form after 2023. + mt_salt_deduction_indiv: [0, 0] + +# Line 5b: "Deductions that are attributable to only one spouse must be +# claimed by that spouse" (2023 instructions, PDF p. 13). A city earnings tax +# belongs to the spouse who earned the taxed income. + +- name: A city earnings tax goes in the column of the spouse who earned it + period: 2023 + input: + people: + person1: + age: 45 + real_estate_taxes: 3_000 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + person2: + age: 45 + real_estate_taxes: 3_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Column A: 3,000 + 1% x 100,000 = 4,000; column B: 3,000. + mt_salt_deduction_indiv: [4_000, 3_000] + +- name: Both spouses' city earnings taxes, one column at the cap + period: 2022 + input: + people: + person1: + age: 45 + real_estate_taxes: 4_500 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + person2: + age: 45 + mo_kansas_city_earnings_tax_taxable_earnings: 50_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Kansas City tax: 1,000 on column A's earnings and 500 on column B's. + # Column A: min(4,500 + 1,000, 5,000) = 5,000; column B: 500. + mt_salt_deduction_indiv: [5_000, 500] + +- name: Each column takes its own spouse's Philadelphia wage tax at that spouse's rate + period: 2023 + input: + people: + person1: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 100_000 + person2: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 100_000 + pa_philadelphia_wage_tax_reduced_rate_eligible: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Nonresident rate 3.44% x 100,000 = 3,440; reduced rate 1.5% x 100,000 + # = 1,500. Splitting the 4,940 by wages would give 2,470 each. + mt_salt_deduction_indiv: [3_440, 1_500] + +- name: A St. Louis credit reduces only its own spouse's column + period: 2023 + input: + people: + person1: + age: 45 + mo_st_louis_earnings_tax_taxable_earnings: 100_000 + mo_st_louis_earnings_tax_credit: 1_000 + person2: + age: 45 + mo_st_louis_earnings_tax_taxable_earnings: 100_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Column A: 1% x 100,000 - 1,000 credit = 0; column B: 1,000. + mt_salt_deduction_indiv: [0, 1_000] + +- name: Two cities' taxes, one column at the cap + period: 2023 + input: + people: + person1: + age: 45 + real_estate_taxes: 4_000 + mo_kansas_city_earnings_tax_taxable_earnings: 100_000 + person2: + age: 45 + pa_philadelphia_wage_tax_taxable_wages: 100_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Column A: min(4,000 + 1% x 100,000, 5,000) = 5,000. + # Column B: 3.44% x 100,000 = 3,440. + mt_salt_deduction_indiv: [5_000, 3_440] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/test_mt_salt_deduction_invariants.py b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/test_mt_salt_deduction_invariants.py new file mode 100644 index 00000000000..8a008587b55 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/test_mt_salt_deduction_invariants.py @@ -0,0 +1,327 @@ +"""Invariants for Montana's state and local tax deduction, 2021-2023. + +Form 2, Itemized Deductions Schedule, line 5 adds lines 5a (general sales +taxes), 5b (local income taxes), 5c (real estate taxes) and 5d, and caps the +total at $10,000, or $5,000 if married filing separately. A joint return has +one column, so its cap applies once to the couple's combined taxes +(mt_salt_deduction, on the head). Spouses filing separately on the same form +(status 2a) each have their own column, capped at $5,000 +(mt_salt_deduction_indiv). From 2024 the schedule is gone and this change +leaves the earlier per-person amounts as they were. + +The YAML files hold the worked cases. This file checks what they cannot: many +mixed tax units (single, head of household, separate, joint, joint with +dependents, some with sales taxes or Kansas City and St. Louis earnings taxes) +in one vectorized simulation, and how results move when inputs change. For +every tax unit: + +1. Differential: both variables equal an independent numpy calculation of + line 5, with the caps taken from the forms rather than from parameters. +2. Bounds and order: each value lies between zero and its cap, and a couple's + two separate columns never deduct more than their joint return. +3. Monotone: raising any member's real estate taxes never lowers the joint + return's or any column's deduction. +4. Head swap: swapping which spouse is the head moves each column with its + spouse and leaves the joint return's deduction unchanged. +5. From 2024 the per-person amounts equal min(real estate taxes, federal cap) + for the tax unit's filing status, as before this change. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 # dollars + +# Form 2 (2021-2023), Itemized Deductions Schedule, line 5. +FORM_CAP = {"SINGLE": 10_000, "HEAD_OF_HOUSEHOLD": 10_000, "JOINT": 10_000} +FORM_CAP["SEPARATE"] = 5_000 +SCHEDULE_YEARS = [2021, 2022, 2023] +# Kansas City and St. Louis earnings tax rates: local income taxes with no +# residence test, on each person's own earnings in the city. +KANSAS_CITY_RATE = 0.01 +ST_LOUIS_RATE = 0.01 +# 26 USC 164(b)(6) and (7)(A): $10,000 ($5,000 separate) through 2024, and +# $40,400 ($20,200 separate) for 2026. +FEDERAL_CAP = { + 2024: {"SINGLE": 10_000, "HEAD_OF_HOUSEHOLD": 10_000, "JOINT": 10_000}, + 2026: {"SINGLE": 40_400, "HEAD_OF_HOUSEHOLD": 40_400, "JOINT": 40_400}, +} +FEDERAL_CAP[2024]["SEPARATE"] = 5_000 +FEDERAL_CAP[2026]["SEPARATE"] = 20_200 +KINDS = ["single", "hoh", "separate", "joint", "joint_dependents"] +STATUS = { + "single": "SINGLE", + "hoh": "HEAD_OF_HOUSEHOLD", + "separate": "SEPARATE", + "joint": "JOINT", + "joint_dependents": "JOINT", +} + +# Mostly amounts near the caps, plus zeros and large amounts. +taxes = st.one_of( + st.just(0.0), + st.integers(1, 6_000).map(float), + st.integers(6_000, 30_000).map(float), +) +sales = st.one_of(st.just(0.0), st.integers(1, 3_000).map(float)) +increases = st.one_of(st.just(0.0), st.integers(1, 5_000).map(float)) +city_earnings = st.one_of(st.just(0.0), st.integers(1, 300_000).map(float)) +city_credits = st.one_of(st.just(0.0), st.integers(1, 3_000).map(float)) + + +@st.composite +def people(draw): + return { + "real_estate_taxes": draw(taxes), + "increase": draw(increases), + "kansas_city_earnings": draw(city_earnings), + "st_louis_earnings": draw(city_earnings), + "st_louis_credit": draw(city_credits), + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(KINDS)) + n_dependents = draw(st.integers(1, 2)) if kind in ("hoh", "joint_dependents") else 0 + return { + "kind": kind, + "head": draw(people()), + "spouse": draw(people()) if kind.startswith("joint") else None, + "dependents": [draw(people()) for _ in range(n_dependents)], + "state_sales_tax": draw(sales), + "local_sales_tax": draw(sales), + } + + +SEED = 20261007 + + +def _seeded_units(n=150): + rng = np.random.default_rng(SEED) + + def person(): + band = rng.choice(3) + return { + "real_estate_taxes": float( + [0, rng.integers(1, 6_001), rng.integers(6_000, 30_001)][band] + ), + "increase": float(rng.integers(0, 5_001)) if rng.random() < 0.7 else 0.0, + "kansas_city_earnings": float(rng.integers(1, 300_001)) + if rng.random() < 0.2 + else 0.0, + "st_louis_earnings": float(rng.integers(1, 300_001)) + if rng.random() < 0.2 + else 0.0, + "st_louis_credit": float(rng.integers(1, 3_001)) + if rng.random() < 0.3 + else 0.0, + } + + units = [] + for _ in range(n): + kind = str(rng.choice(KINDS)) + n_dependents = ( + int(rng.integers(1, 3)) if kind in ("hoh", "joint_dependents") else 0 + ) + units.append( + { + "kind": kind, + "head": person(), + "spouse": person() if kind.startswith("joint") else None, + "dependents": [person() for _ in range(n_dependents)], + "state_sales_tax": float(rng.integers(0, 3_001)) + if rng.random() < 0.3 + else 0.0, + "local_sales_tax": float(rng.integers(0, 601)) + if rng.random() < 0.3 + else 0.0, + } + ) + return units + + +def _situation(units, year, *, raised=False, swapped=False): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + + def add(name, values, role): + people[name] = { + "age": 20 if role == "dependent" else 45, + "is_tax_unit_head": role == "head", + "is_tax_unit_spouse": role == "spouse", + "is_tax_unit_dependent": role == "dependent", + "real_estate_taxes": values["real_estate_taxes"] + + raised * values["increase"], + "mo_kansas_city_earnings_tax_taxable_earnings": values[ + "kansas_city_earnings" + ], + "mo_st_louis_earnings_tax_taxable_earnings": values["st_louis_earnings"], + "mo_st_louis_earnings_tax_credit": values["st_louis_credit"], + } + + for i, unit in enumerate(units): + first, second = f"first_{i}", f"second_{i}" + couple = unit["spouse"] is not None + # With swapped, the second spouse is the head. + add(first, unit["head"], "spouse" if couple and swapped else "head") + members, marital = [first], [first] + if couple: + add(second, unit["spouse"], "head" if swapped else "spouse") + members.append(second) + marital.append(second) + groups["marital_units"][f"couple_{i}"] = {"members": marital} + for j, dependent in enumerate(unit["dependents"]): + name = f"dependent_{i}_{j}" + add(name, dependent, "dependent") + members.append(name) + groups["marital_units"][f"single_{i}_{j}"] = {"members": [name]} + groups["tax_units"][f"tax_unit_{i}"] = { + "members": members, + "filing_status": STATUS[unit["kind"]], + "state_sales_tax": unit["state_sales_tax"], + "local_sales_tax": unit["local_sales_tax"], + } + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": "MT", + } + situation = {"people": people, **groups} + for entities in situation.values(): + for values in entities.values(): + for key, value in values.items(): + if key != "members": + values[key] = {year: value} + return situation + + +def _run(units, year, **kwargs): + sim = Simulation(situation=_situation(units, year, **kwargs)) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in [ + "mt_salt_deduction", + "mt_salt_deduction_indiv", + "real_estate_taxes", + "mo_kansas_city_earnings_tax_taxable_earnings", + "mo_st_louis_earnings_tax_taxable_earnings", + "mo_st_louis_earnings_tax_credit", + ] + } + out["unit"] = sim.populations["tax_unit"].members_entity_id + out["head"] = np.asarray(sim.calculate("is_tax_unit_head", year)) + out["filer"] = np.asarray(sim.calculate("is_tax_unit_head_or_spouse", year)) + return out + + +def _unit_sum(run, values): + return np.bincount(run["unit"], weights=values) + + +def _check_schedule_year(units, year): + base = _run(units, year) + unit, head, filer = base["unit"], base["head"], base["filer"] + status = np.array([STATUS[u["kind"]] for u in units]) + married = status == "JOINT" + sales = np.array([u["state_sales_tax"] + u["local_sales_tax"] for u in units]) + joint_cap = np.array([FORM_CAP[s] for s in status]) + column_cap = np.where(married, FORM_CAP["SEPARATE"], joint_cap) + + # 1. Differential against numpy. Each person owes city tax on their own + # earnings there, less their own credits; a column takes its own + # spouse's tax. + city_tax = KANSAS_CITY_RATE * base[ + "mo_kansas_city_earnings_tax_taxable_earnings" + ] + np.maximum( + 0, + ST_LOUIS_RATE * base["mo_st_louis_earnings_tax_taxable_earnings"] + - base["mo_st_louis_earnings_tax_credit"], + ) + total = _unit_sum(base, base["real_estate_taxes"] + city_tax) + sales + np.testing.assert_allclose( + base["mt_salt_deduction"], + head * np.minimum(total, joint_cap)[unit], + atol=TOLERANCE, + ) + share = np.where(married, 0.5, 1.0)[unit] + own = base["real_estate_taxes"] + share * sales[unit] + city_tax + np.testing.assert_allclose( + base["mt_salt_deduction_indiv"], + filer * np.minimum(own, column_cap[unit]), + atol=TOLERANCE, + ) + + # 2. Bounds and order. + joint = _unit_sum(base, base["mt_salt_deduction"]) + columns = _unit_sum(base, base["mt_salt_deduction_indiv"]) + assert (base["mt_salt_deduction"] >= 0).all() + assert (base["mt_salt_deduction_indiv"] >= 0).all() + assert (joint <= joint_cap + TOLERANCE).all() + assert (base["mt_salt_deduction_indiv"] <= column_cap[unit] + TOLERANCE).all() + assert (columns[married] <= joint[married] + TOLERANCE).all() + + # 3. Monotone in real estate taxes. + raised = _run(units, year, raised=True) + for name in ["mt_salt_deduction", "mt_salt_deduction_indiv"]: + assert (raised[name] >= base[name] - TOLERANCE).all(), name + + # 4. Head swap. + swapped = _run(units, year, swapped=True) + np.testing.assert_allclose( + _unit_sum(swapped, swapped["mt_salt_deduction"]), joint, atol=TOLERANCE + ) + np.testing.assert_allclose( + swapped["mt_salt_deduction_indiv"], + base["mt_salt_deduction_indiv"], + atol=TOLERANCE, + ) + + +def _check_unchanged_year(units, year): + base = _run(units, year) + status = np.array([STATUS[u["kind"]] for u in units]) + cap = np.array([FEDERAL_CAP[year][s] for s in status])[base["unit"]] + np.testing.assert_allclose( + base["mt_salt_deduction"], + np.minimum(base["real_estate_taxes"], cap), + atol=TOLERANCE, + ) + # Spouses no longer file separately on the same form. + assert (base["mt_salt_deduction_indiv"] == 0).all() + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.sampled_from(SCHEDULE_YEARS), st.lists(tax_units(), min_size=5, max_size=30)) +def test_mt_salt_deduction_schedule_invariants(year, units): + _check_schedule_year(units, year) + + +@settings(**SETTINGS) +@given( + st.sampled_from(sorted(FEDERAL_CAP)), st.lists(tax_units(), min_size=5, max_size=30) +) +def test_mt_salt_deduction_unchanged_from_2024(year, units): + _check_unchanged_year(units, year) + + +@pytest.mark.parametrize("year", SCHEDULE_YEARS) +def test_seeded_population_schedule_years(year): + _check_schedule_year(_seeded_units(), year) + + +@pytest.mark.parametrize("year", sorted(FEDERAL_CAP)) +def test_seeded_population_unchanged_from_2024(year): + _check_unchanged_year(_seeded_units(), year) diff --git a/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.py b/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.py index 220f8254fd2..a31e9c6523a 100644 --- a/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.py +++ b/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax.py @@ -11,11 +11,4 @@ class mo_kansas_city_earnings_tax(Variable): definition_period = YEAR unit = USD reference = "https://www.kcmo.gov/city-hall/departments/finance/earnings-tax" - - def formula(tax_unit, period, parameters): - person = tax_unit.members - p = parameters(period).gov.local.mo.kansas_city.tax.income - taxable_earnings = person( - "mo_kansas_city_earnings_tax_taxable_earnings", period - ) - return tax_unit.sum(taxable_earnings * p.rate) + adds = ["mo_kansas_city_earnings_tax_person"] diff --git a/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax_person.py b/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax_person.py new file mode 100644 index 00000000000..99ff1759eda --- /dev/null +++ b/policyengine_us/variables/gov/local/mo/kansas_city/tax/income/mo_kansas_city_earnings_tax_person.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class mo_kansas_city_earnings_tax_person(Variable): + value_type = float + entity = Person + label = "Kansas City earnings tax for each person" + documentation = "Kansas City earnings tax on the person's own taxable earnings." + definition_period = YEAR + unit = USD + reference = "https://www.kcmo.gov/city-hall/departments/finance/earnings-tax" + + def formula(person, period, parameters): + p = parameters(period).gov.local.mo.kansas_city.tax.income + taxable_earnings = person( + "mo_kansas_city_earnings_tax_taxable_earnings", period + ) + return taxable_earnings * p.rate diff --git a/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.py b/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.py index 1c31987db47..42bbf173fb7 100644 --- a/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.py +++ b/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax.py @@ -14,11 +14,4 @@ class mo_st_louis_earnings_tax(Variable): "https://www.stlouis-mo.gov/government/departments/collector/" "earnings-tax/file-earnings-tax.cfm" ) - - def formula(tax_unit, period, parameters): - person = tax_unit.members - p = parameters(period).gov.local.mo.st_louis.tax.income - taxable_earnings = person("mo_st_louis_earnings_tax_taxable_earnings", period) - credits = person("mo_st_louis_earnings_tax_credit", period) - person_tax = taxable_earnings * p.rate - return tax_unit.sum(max_(0, person_tax - credits)) + adds = ["mo_st_louis_earnings_tax_person"] diff --git a/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax_person.py b/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax_person.py new file mode 100644 index 00000000000..8a2b5aa7f5d --- /dev/null +++ b/policyengine_us/variables/gov/local/mo/st_louis/tax/income/mo_st_louis_earnings_tax_person.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class mo_st_louis_earnings_tax_person(Variable): + value_type = float + entity = Person + label = "St. Louis earnings tax for each person" + documentation = ( + "St. Louis earnings tax on the person's own taxable earnings, after " + "the person's optional credits supplied as inputs." + ) + definition_period = YEAR + unit = USD + reference = ( + "https://www.stlouis-mo.gov/government/departments/collector/" + "earnings-tax/file-earnings-tax.cfm" + ) + + def formula(person, period, parameters): + p = parameters(period).gov.local.mo.st_louis.tax.income + taxable_earnings = person("mo_st_louis_earnings_tax_taxable_earnings", period) + credits = person("mo_st_louis_earnings_tax_credit", period) + return max_(0, taxable_earnings * p.rate - credits) diff --git a/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.py b/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.py index 62e37e476a6..f74a1154d72 100644 --- a/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.py +++ b/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax.py @@ -15,18 +15,4 @@ class pa_philadelphia_wage_tax(Variable): "https://www.phila.gov/services/payments-assistance-taxes/taxes/" "business-taxes/business-taxes-by-type/wage-tax-employers/" ) - - def formula(tax_unit, period, parameters): - person = tax_unit.members - taxable_wages = person("pa_philadelphia_wage_tax_taxable_wages", period) - if not np.any(taxable_wages): - return 0 - - p = parameters(period).gov.local.pa.philadelphia.tax.income - resident = person("pa_philadelphia_wage_tax_resident", period) - reduced_rate_eligible = person( - "pa_philadelphia_wage_tax_reduced_rate_eligible", period - ) - standard_rate = where(resident, p.resident_rate, p.nonresident_rate) - rate = where(reduced_rate_eligible, p.reduced_rate, standard_rate) - return tax_unit.sum(taxable_wages * rate) + adds = ["pa_philadelphia_wage_tax_person"] diff --git a/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax_person.py b/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax_person.py new file mode 100644 index 00000000000..3b2018fae7c --- /dev/null +++ b/policyengine_us/variables/gov/local/pa/philadelphia/tax/income/pa_philadelphia_wage_tax_person.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * + + +class pa_philadelphia_wage_tax_person(Variable): + value_type = float + entity = Person + label = "Philadelphia wage tax for each person" + documentation = ( + "Philadelphia employee wage or earnings tax on the person's own " + "taxable wages, at the rate for the person's resident status." + ) + definition_period = YEAR + unit = USD + reference = ( + "https://www.phila.gov/services/payments-assistance-taxes/taxes/" + "business-taxes/business-taxes-by-type/wage-tax-employers/" + ) + + def formula(person, period, parameters): + taxable_wages = person("pa_philadelphia_wage_tax_taxable_wages", period) + if not np.any(taxable_wages): + return 0 + + p = parameters(period).gov.local.pa.philadelphia.tax.income + resident = person("pa_philadelphia_wage_tax_resident", period) + reduced_rate_eligible = person( + "pa_philadelphia_wage_tax_reduced_rate_eligible", period + ) + standard_rate = where(resident, p.resident_rate, p.nonresident_rate) + rate = where(reduced_rate_eligible, p.reduced_rate, standard_rate) + return taxable_wages * rate diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py index 5d1cb0c5153..2757859ca05 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py @@ -30,7 +30,7 @@ def formula(person, period, parameters): [ "mt_misc_deductions", "mt_medical_expense_deduction_indiv", - "mt_salt_deduction", + "mt_salt_deduction_indiv", "mt_federal_income_tax_deduction_for_federal_itemization_indiv", ], ) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py index 7b31319e265..ba313b8e88a 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py @@ -32,7 +32,7 @@ def formula(person, period, parameters): [ "mt_misc_deductions", "mt_medical_expense_deduction_indiv", - "mt_salt_deduction", + "mt_salt_deduction_indiv", "mt_federal_income_tax_deduction_indiv", ], ) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction.py index e5d5221dbf0..00c82008e1c 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction.py @@ -7,19 +7,53 @@ class mt_salt_deduction(Variable): label = "Montana state and local tax deduction" unit = USD definition_period = YEAR + documentation = ( + "Itemized Deductions Schedule, line 5, for a return with one column: " + "a single, head of household or joint return, or a married person " + "filing on a separate form. Through 2023 the return's total is " + "assigned to the tax unit head." + ) reference = ( + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2021_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", - "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", - # Montana Code Annotated MT Code § 15-30-2131 (2022) (1)(a) + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2023_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2021_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=32", + "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=34", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=37", + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + # Former MCA 15-30-2131(1)(a)(ii) (2021): items under IRC 161, except + # state income tax paid. The cap is 26 USC 164(b)(6). ) defined_for = StateCode.MT def formula(person, period, parameters): - # Not included the SALT tax, only real estate taxes - p = parameters(period).gov.irs.deductions + p = parameters(period).gov.irs.deductions.itemized.salt_and_real_estate + p_mt = parameters(period).gov.states.mt.tax.income.deductions.itemized filing_status = person.tax_unit("filing_status", period) + cap = p.cap[filing_status] + if p_mt.state_specific_deduction_applies: + # Line 5 adds lines 5a through 5d for the whole return and caps + # the total once: "not more than $10,000 if your status is + # single, head of household or married filing jointly; or $5,000 + # if you are married filing separately". + # 5a: general state and local sales taxes. + # 5b: local income taxes. No Montana locality levies one, but + # a resident can owe another city's earnings or wage tax. + # 5c: real estate taxes. + # 5d: value-based personal property taxes, not modeled. + taxes = add( + person.tax_unit, + period, + [ + "real_estate_taxes", + "state_sales_tax", + "local_sales_tax", + "local_income_tax", + ], + ) + is_head = person("is_tax_unit_head", period) + return is_head * min_(taxes, cap) + # From 2024 the schedule is discontinued. This keeps the earlier + # per-person amounts until the 2024 Form 2 line 2 is modeled. real_estate_tax = person("real_estate_taxes", period) - return min_( - real_estate_tax, - p.itemized.salt_and_real_estate.cap[filing_status], - ) + return min_(real_estate_tax, cap) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction_indiv.py new file mode 100644 index 00000000000..3054cfc2aae --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/salt/mt_salt_deduction_indiv.py @@ -0,0 +1,68 @@ +from policyengine_us.model_api import * + + +class mt_salt_deduction_indiv(Variable): + value_type = float + entity = Person + label = "Montana state and local tax deduction when married couples are filing separately" + unit = USD + definition_period = YEAR + documentation = ( + "Itemized Deductions Schedule, line 5, in each spouse's column when " + "a married couple files separately on the same form (filing status " + "2a). Each column is a separate return, capped at the married filing " + "separately amount." + ) + reference = ( + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2021_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2023_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=12", + # 2023 instructions, "How to allocate income and deductions when + # filing separately": "each column constituting its own return". + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=37", + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + # Former MCA 15-30-2131(1)(a)(ii) (2021). The cap is 26 USC 164(b)(6). + ) + defined_for = "mt_married_filing_separately_on_same_return_eligible" + + def formula(person, period, parameters): + p = parameters(period).gov.irs.deductions.itemized.salt_and_real_estate + p_mt = parameters(period).gov.states.mt.tax.income.deductions.itemized + filing_status = person.tax_unit("filing_status", period) + # A married couple's columns are each "married filing separately": + # "not more than ... $5,000 if you are married filing separately". + # Other filers have one column at their own status's cap. + married = filing_status == filing_status.possible_values.JOINT + cap = where(married, p.cap.SEPARATE, p.cap[filing_status]) + # Line 5c: each spouse's own real estate taxes. + real_estate_tax = person("real_estate_taxes", period) + # Line 5a: PolicyEngine estimates sales taxes for the tax unit only. + # Jointly paid expenses "can be allocated to either spouse in any + # proportional amount", so a couple divides them equally. + sales_tax = add(person.tax_unit, period, ["state_sales_tax", "local_sales_tax"]) + sales_tax_share = where(married, p_mt.spouse_allocation_rate, 1) + # Line 5b: "Deductions that are attributable to only one spouse must + # be claimed by that spouse." The local income taxes PolicyEngine + # levies on a Montana household are city taxes on each person's own + # earnings there (Kansas City, St. Louis, Philadelphia), so each + # column takes that person's own tax. Any other local income tax of + # the tax unit is divided like the sales taxes. + own_local_tax = add( + person, + period, + [ + "mo_kansas_city_earnings_tax_person", + "mo_st_louis_earnings_tax_person", + "pa_philadelphia_wage_tax_person", + ], + ) + local_income_tax = person.tax_unit("local_income_tax", period) + other_local_tax = max_(0, local_income_tax - person.tax_unit.sum(own_local_tax)) + taxes = ( + real_estate_tax + + sales_tax_share * (sales_tax + other_local_tax) + + own_local_tax + ) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return head_or_spouse * min_(taxes, cap)