diff --git a/changelog.d/fix-mt-medical-premiums-in-full.fixed.md b/changelog.d/fix-mt-medical-premiums-in-full.fixed.md new file mode 100644 index 00000000000..73d4d8a2304 --- /dev/null +++ b/changelog.d/fix-mt-medical-premiums-in-full.fixed.md @@ -0,0 +1 @@ +Deduct Montana's 2021–2023 medical and long-term care insurance premiums paid by the head or spouse in full on the Itemized Deductions Schedule (lines 2 and 3), net of premiums taken as the self-employed health insurance deduction, and apply the 7.5% floor only to other medical and dental expenses. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_eligible_long_term_care_insurance_premiums.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_eligible_long_term_care_insurance_premiums.yaml new file mode 100644 index 00000000000..ec8426839bf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_eligible_long_term_care_insurance_premiums.yaml @@ -0,0 +1,56 @@ +- name: Long-term care premiums qualify in full without the federal age-based limit + period: 2021 + absolute_error_margin: 0.01 + input: + age: 40 + long_term_health_insurance_premiums: 3_000 + state_code: MT + output: + # Montana line 3 deducts the premiums without the federal age-based + # limit on eligible long-term care premiums (26 U.S.C. 213(d)(10)). + mt_eligible_long_term_care_insurance_premiums: 3_000 + +- name: Long-term care premiums taken as the self-employed health insurance deduction do not qualify + period: 2022 + absolute_error_margin: 0.01 + input: + long_term_health_insurance_premiums: 400 + self_employed_health_insurance_ald_person: 400 + state_code: MT + output: + mt_eligible_long_term_care_insurance_premiums: 0 + +- name: The deduction applies first to medical premiums, then to long-term care premiums + period: 2023 + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 2_000 + long_term_health_insurance_premiums: 1_500 + self_employed_health_insurance_ald_person: 3_000 + state_code: MT + output: + # 3,000 - 2,000 used on line 2 leaves 1,000 for line 3: 1,500 - 1,000. + self_employed_health_insurance_ald_excluded_premiums: 2_000 + mt_eligible_medical_insurance_premiums: 0 + mt_eligible_long_term_care_insurance_premiums: 500 + +- name: A deduction smaller than the medical premiums leaves long-term care premiums in full + period: 2023 + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 5_000 + long_term_health_insurance_premiums: 1_500 + self_employed_health_insurance_ald_person: 3_000 + state_code: MT + output: + mt_eligible_medical_insurance_premiums: 2_000 + mt_eligible_long_term_care_insurance_premiums: 1_500 + +- name: Outside Montana the amount is zero + period: 2022 + absolute_error_margin: 0.01 + input: + long_term_health_insurance_premiums: 1_000 + state_code: ID + output: + mt_eligible_long_term_care_insurance_premiums: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_eligible_medical_insurance_premiums.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_eligible_medical_insurance_premiums.yaml new file mode 100644 index 00000000000..2557e410540 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_eligible_medical_insurance_premiums.yaml @@ -0,0 +1,94 @@ +- name: Premiums paid with after-tax dollars qualify in full + period: 2022 + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 4_000 + state_code: MT + output: + mt_eligible_medical_insurance_premiums: 4_000 + +- name: Medicare Part B premiums qualify alongside other premiums + period: 2022 + absolute_error_margin: 0.01 + input: + health_insurance_premiums_without_medicare_part_b: 1_000 + medicare_enrolled: true + medicare_part_b_premium: 2_000 + state_code: MT + output: + # The 2022 Form 2 instructions for line 2 make Medicare Part B premiums + # 100 percent deductible on this line. + medical_expense_health_insurance_premiums: 3_000 + mt_eligible_medical_insurance_premiums: 3_000 + +- name: Pre-tax premiums do not qualify + period: 2022 + absolute_error_margin: 0.01 + input: + employment_income: 50_000 + pre_tax_health_insurance_premiums: 3_000 + state_code: MT + output: + # Premiums paid through a cafeteria plan are excluded from wages and are + # not counted as medical expenses. + medical_expense_health_insurance_premiums: 0 + mt_eligible_medical_insurance_premiums: 0 + +- name: Premiums fully taken as the self-employed health insurance deduction do not qualify + period: 2022 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: 40_000 + health_insurance_premiums: 6_000 + state_code: MT + output: + self_employed_health_insurance_ald_person: 6_000 + mt_eligible_medical_insurance_premiums: 0 + +- name: Premiums above the self-employed health insurance deduction qualify + period: 2022 + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 6_000 + # The deduction claimed on Schedule 1, line 17, after its earnings limit. + self_employed_health_insurance_ald_person: 2_000 + state_code: MT + output: + # Only the 2,000 deducted in Montana AGI is excluded: 6,000 - 2,000. + mt_eligible_medical_insurance_premiums: 4_000 + +- name: A deduction above the medical premiums excludes no more than the premiums + period: 2022 + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 1_000 + long_term_health_insurance_premiums: 2_000 + self_employed_health_insurance_ald_person: 2_500 + state_code: MT + output: + # The deduction covers all 1,000 of the medical premiums; the other 1,500 + # covers long-term care premiums on line 3. + mt_eligible_medical_insurance_premiums: 0 + mt_eligible_long_term_care_insurance_premiums: 500 + +- name: Self-employed person with a loss deducts none of the premiums in AGI + period: 2023 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: -5_000 + health_insurance_premiums: 3_000 + state_code: MT + output: + self_employed_health_insurance_ald_person: 0 + mt_eligible_medical_insurance_premiums: 3_000 + +- name: Outside Montana the amount is zero + period: 2022 + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 4_000 + state_code: ID + output: + mt_eligible_medical_insurance_premiums: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml index 3bf6f36d639..c3cc0f15446 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml @@ -22,9 +22,10 @@ period: 2021 input: other_medical_expenses: 1_000 - mt_agi_indiv: 20_000 + health_insurance_premiums: 2_000 + mt_agi_indiv: 10_000 state_code: MT - is_tax_unit_head_or_spouse: true + is_tax_unit_head_or_spouse: false output: mt_medical_expense_deduction_indiv: 0 @@ -58,4 +59,114 @@ members: [head, spouse] state_code: MT output: - mt_medical_expense_deduction_indiv: [3_250, 750] + # Line 1 floors only the other expenses; line 2 adds the premiums net of + # each spouse's own self-employed health insurance deduction, in full. + # Head: max(0, 500 - 7.5% x 30,000) + (6,000 - 1,000) = 0 + 5,000. + # Spouse: max(0, 500 - 7.5% x 10,000) + (3,000 - 2,000) = 0 + 1,000. + mt_medical_expense_deduction_indiv: [5_000, 1_000] + +- name: Medical insurance premiums are deducted in full on line 2, not under the floor + period: 2022 + absolute_error_margin: 0.01 + input: + other_medical_expenses: 1_000 + health_insurance_premiums: 2_000 + mt_agi_indiv: 20_000 + state_code: MT + is_tax_unit_head_or_spouse: true + output: + # Line 1: max(0, 1,000 - 7.5% x 20,000) = max(0, 1,000 - 1,500) = 0. + # Line 2: 2,000 in full. + # Flooring the premiums with the other expenses would give + # 3,000 - 1,500 = 1,500. + mt_medical_expense_deduction_indiv: 2_000 + +- name: Long-term care insurance premiums are deducted in full on line 3 + period: 2023 + absolute_error_margin: 0.01 + input: + long_term_health_insurance_premiums: 1_500 + mt_agi_indiv: 50_000 + state_code: MT + is_tax_unit_head_or_spouse: true + output: + mt_medical_expense_deduction_indiv: 1_500 + +- name: Premiums taken as the self-employed health insurance deduction are not deducted again + period: 2021 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: 30_000 + health_insurance_premiums: 5_000 + other_medical_expenses: 4_000 + mt_agi_indiv: 25_000 + state_code: MT + is_tax_unit_head_or_spouse: true + output: + self_employed_health_insurance_ald_person: 5_000 + # Line 1: 4,000 - 7.5% x 25,000 = 2,125. Line 2: 0. + mt_medical_expense_deduction_indiv: 2_125 + +- name: Each spouse filing separately on the same form deducts their own premiums + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 3_000 + other_medical_expenses: 2_000 + mt_agi_indiv: 20_000 + person2: + is_tax_unit_spouse: true + health_insurance_premiums: 1_000 + long_term_health_insurance_premiums: 800 + mt_agi_indiv: 10_000 + person3: + is_tax_unit_dependent: true + health_insurance_premiums: 600 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Head: line 1 max(0, 2,000 - 7.5% x 20,000) = 500, line 2 3,000. + # Spouse: line 2 1,000, line 3 800. + mt_medical_expense_deduction_indiv: [3_500, 1_800, 0] + +- name: Long-term care premiums taken as the self-employed health insurance deduction are not deducted again + period: 2022 + absolute_error_margin: 0.01 + input: + long_term_health_insurance_premiums: 400 + self_employed_health_insurance_ald_person: 400 + other_medical_expenses: 1_000 + mt_agi_indiv: 10_000 + state_code: MT + is_tax_unit_head_or_spouse: true + output: + # The deduction took all 400 of the long-term care premiums in Montana + # AGI, so line 3 is 0. Line 1: 1,000 - 7.5% x 10,000 = 250. + mt_medical_expense_deduction_indiv: 250 + +- name: A reform that allows separate filing in 2024 keeps premiums under the floor + period: 2024 + reforms: policyengine_us.tests.policy.baseline.gov.states.mt.tax.income.enable_mt_mfss_2024_reform.enable_mt_mfss_2024_reform + absolute_error_margin: 0.01 + input: + health_insurance_premiums: 4_000 + long_term_health_insurance_premiums: 1_000 + other_medical_expenses: 2_000 + mt_agi_indiv: 60_000 + state_code: MT + is_tax_unit_head_or_spouse: true + output: + # The Itemized Deductions Schedule ended after 2023, so lines 2 and 3 do + # not apply: (4,000 + 2,000) - 7.5% x 60,000 = 1,500. Applying them would + # give 0 + 4,000 + 1,000 = 5,000. + mt_married_filing_separately_on_same_return_eligible: true + mt_medical_expense_deduction_indiv: 1_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml index af405bf3b43..2bd044b3c36 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml @@ -186,5 +186,573 @@ members: [head, spouse] state_code: MT output: - # (6,000 + 3,000 - 1,000 - 2,000 + 1,000) - 7.5% of 40,000. + # Line 1: max(0, (500 + 500) - 7.5% x 40,000) = 0. + # Line 2: (6,000 - 1,000) + (3,000 - 2,000) = 6,000, in full. + # The self-employed deductions are netted once, on line 2. + mt_medical_expense_deduction_joint: [6_000, 0] + +- name: Medical insurance premiums are deducted in full on line 2, not under the floor + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 4_000 + other_medical_expenses: 2_000 + person2: + is_tax_unit_head: false + tax_units: + tax_unit: + members: [person1, person2] + mt_agi_joint: 60_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 1: max(0, 2,000 - 7.5% x 60,000) = max(0, 2,000 - 4,500) = 0. + # Line 2: 4,000 in full. Line 3: 0. + # Flooring the premiums with the other expenses would give + # 6,000 - 4,500 = 1,500. + mt_medical_expense_deduction_joint: [4_000, 0] + +- name: Long-term care insurance premiums are deducted in full on line 3 + period: 2021 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + other_medical_expenses: 5_000 + long_term_health_insurance_premiums: 3_000 + person2: + is_tax_unit_head: false + long_term_health_insurance_premiums: 2_500 + tax_units: + tax_unit: + members: [person1, person2] + mt_agi_joint: 40_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 1: 5,000 - 7.5% x 40,000 = 2,000. + # Line 3: 3,000 + 2,500 = 5,500. + # Total: 2,000 + 5,500 = 7,500, held by the head. + mt_medical_expense_deduction_joint: [7_500, 0] + +- name: Premiums taken as the self-employed health insurance deduction are not deducted again + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + other_medical_expenses: 10_000 + tax_units: + tax_unit: + members: [person1] + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # The self-employed health insurance deduction takes all 6,000 of the + # premiums in federal and so in Montana AGI, so line 2 is 0. + self_employed_health_insurance_ald: 6_000 + # Line 1: 10,000 - 7.5% x 40,000 = 7,000. + mt_medical_expense_deduction_joint: 7_000 + +- name: Only filer-paid premiums count on line 2 while line 1 is below the floor + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 3_000 + person2: + is_tax_unit_spouse: true + person3: + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 1_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + mt_agi_joint: 50_000 + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Line 1: max(0, 1,000 - 7.5% x 50,000) = 0. + # Line 2: the head's 3,000. The dependent paid their own 1,200, + # so it is not a payment made by either filer. + mt_medical_expense_deduction_joint: [3_000, 0, 0] + +- name: Dependent-paid qualified long-term care premiums do not become the filers' deduction + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + health_insurance_premiums: 0 + long_term_health_insurance_premiums: 0 + spouse: + is_tax_unit_spouse: true + health_insurance_premiums: 0 + long_term_health_insurance_premiums: 0 + dependent: + is_tax_unit_dependent: true + long_term_health_insurance_premiums: 1_000 + self_employed_health_insurance_ald_person: 0 + tax_units: + tax_unit: + members: [head, spouse, dependent] + mt_agi_joint: 50_000 + self_employed_health_insurance_ald: 0 + households: + household: + members: [head, spouse, dependent] + state_code: MT + output: + # Former MCA 15-30-2131(1)(g) allows premiums paid by the taxpayer. + # These premiums were paid by the dependent, so neither filer claims them. + mt_medical_expense_deduction_joint: [0, 0, 0] + mt_medical_expense_deduction_indiv: [0, 0, 0] + mt_premium_deduction_joint: 0 + +- name: Joint return computed from wages, with premiums and other medical expenses + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 80_000 + health_insurance_premiums: 6_000 + other_medical_expenses: 3_000 + person2: + age: 40 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_agi_joint: 80_000 + # Line 1: max(0, 3,000 - 7.5% x 80,000) = max(0, 3,000 - 6,000) = 0. + # Line 2: 6,000. + # Flooring the premiums with the other expenses would give + # 9,000 - 6,000 = 3,000. + mt_medical_expense_deduction_joint: [6_000, 0] + +- name: From 2024 medical insurance premiums stay under the federal floor + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 60_000 + health_insurance_premiums: 4_000 + other_medical_expenses: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + adjusted_gross_income: 60_000 + # Federal Schedule A: (4,000 + 2,000) - 7.5% x 60,000 = 1,500. + medical_expense_deduction: 1_500 + mt_medical_expense_deduction_joint: 1_500 + +- name: The self-employed deduction left after medical premiums reduces long-term care premiums + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 2_000 + long_term_health_insurance_premiums: 1_500 + self_employed_health_insurance_ald_person: 3_000 + other_medical_expenses: 4_000 + tax_units: + tax_unit: + members: [person1] + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # The 3,000 deduction in Montana AGI covers the 2,000 of medical premiums + # and 1,000 of the long-term care premiums. + # Line 1: 4,000 - 7.5% x 40,000 = 1,000. + # Line 2: 2,000 - 2,000 = 0. Line 3: 1,500 - 1,000 = 500. + mt_medical_expense_deduction_joint: 1_500 + +- name: A self-employed deduction supplied only for the return still comes off line 2 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 6_000 + person2: + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + self_employed_health_insurance_ald: 2_000 + mt_agi_joint: 60_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 1: 0. Line 2: 6,000 - 2,000 = 4,000. Ignoring the supplied + # deduction would give 6,000. mt_medical_expense_deduction_joint: [4_000, 0] + +- name: A supplied federal medical expense subtotal reaches line 1 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + person2: + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + itemized_medical_expenses: 10_000 + mt_agi_joint: 40_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 1: 10,000 - 7.5% x 40,000 = 7,000. + mt_medical_expense_deduction_joint: [7_000, 0] + +- name: A supplied subtotal with premiums moves the premiums to line 2 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 6_000 + self_employed_health_insurance_ald_person: 1_000 + tax_units: + tax_unit: + members: [person1] + itemized_medical_expenses: 10_000 + mt_agi_joint: 30_000 + households: + household: + members: [person1] + state_code: MT + output: + # The supplied subtotal is the medical expenses paid, as North Dakota and + # New Mexico use it; no federal exclusion was applied to it. + itemized_medical_expenses_applied_exclusion: 0 + # Line 1: (10,000 - 6,000) - 7.5% x 30,000 = 1,750. + # Line 2: 6,000 - 1,000 = 5,000. + mt_medical_expense_deduction_joint: 6_750 + +- name: Long-term care premiums inside a supplied subtotal are not deducted twice + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + long_term_health_insurance_premiums: 400 + tax_units: + tax_unit: + members: [person1] + # Schedule A line 1: 10,000 of other expenses + the 400 of premiums. + itemized_medical_expenses: 10_400 + itemized_medical_expenses_long_term_care_premiums: 400 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # Line 1: (10,400 - 400) - 7.5% x 40,000 = 7,000. Line 3: 400. + mt_medical_expense_deduction_joint: 7_400 + +- name: Only the part of long-term care premiums inside a supplied subtotal leaves line 1 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 65 + medicare_enrolled: false + long_term_health_insurance_premiums: 6_000 + tax_units: + tax_unit: + members: [person1] + # Schedule A line 1 counts 4,510 of the premiums (the 2022 limit for + # ages 61-70, Rev. Proc. 2021-45). + itemized_medical_expenses: 14_510 + itemized_medical_expenses_long_term_care_premiums: 4_510 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # Line 1: (14,510 - 4,510) - 3,000 = 7,000. Line 3 deducts all 6,000, + # without the federal limit. + mt_medical_expense_deduction_joint: 13_000 + +- name: Long-term care premiums taken as the self-employed deduction are neither in the subtotal nor on line 3 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + long_term_health_insurance_premiums: 400 + self_employed_health_insurance_ald_person: 400 + tax_units: + tax_unit: + members: [person1] + # Schedule A line 1 leaves out the 400 of premiums deducted under + # 162(l), and says so. + itemized_medical_expenses: 10_000 + itemized_medical_expenses_applied_exclusion: 400 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # Line 1: 10,000 - 3,000 = 7,000. Line 3: 400 - 400 = 0. + mt_medical_expense_deduction_joint: 7_000 + +- name: A filer's premiums for an older parent's long-term care policy + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 40 + # Payer attribution: the filer paid the parent's 5,000 policy. + long_term_health_insurance_premiums: 5_000 + person2: + is_tax_unit_dependent: true + age: 75 + medicare_enrolled: false + tax_units: + tax_unit: + members: [person1, person2] + # Schedule A line 1: 10,000 + all 5,000, within the parent's age-75 + # limit of 5,640. + itemized_medical_expenses: 15_000 + itemized_medical_expenses_long_term_care_premiums: 5_000 + mt_agi_joint: 40_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 1: (15,000 - 5,000) - 3,000 = 7,000. Line 3: 5,000. + mt_medical_expense_deduction_joint: [12_000, 0] + +- name: A supplied subtotal with no long-term care premiums keeps them on line 3 only + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + long_term_health_insurance_premiums: 400 + tax_units: + tax_unit: + members: [person1] + # Same contents as the computed subtotal: no long-term care premiums. + itemized_medical_expenses: 10_000 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + itemized_medical_expenses_long_term_care_premiums: 0 + # Line 1: 10,000 - 3,000 = 7,000. Line 3: 400. + mt_medical_expense_deduction_joint: 7_400 + +- name: A net Schedule A subtotal with its self-employed exclusion keeps all other expenses on line 1 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + health_insurance_premiums: 6_000 + long_term_health_insurance_premiums: 400 + self_employed_health_insurance_ald_person: 6_400 + tax_units: + tax_unit: + members: [person1] + # Schedule A line 1: 10,000 of other expenses; the 6,000 of medical + # premiums and the 400 of long-term care premiums were taken as the + # self-employed deduction, so all 6,400 are the excluded amount. + itemized_medical_expenses: 10_000 + itemized_medical_expenses_applied_exclusion: 6_400 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # Medical premiums left in the subtotal: max(0, 6,000 - 6,400) = 0; the + # other 400 of the exclusion covered long-term care premiums, which are + # not in it. Line 1: 10,000 - 3,000 = 7,000. Lines 2 and 3: 0. + mt_premium_deduction_joint: 0 + mt_medical_expense_deduction_joint: 7_000 + +- name: A net Schedule A subtotal with a return-level self-employed deduction + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + health_insurance_premiums: 6_000 + tax_units: + tax_unit: + members: [person1] + self_employed_health_insurance_ald: 6_000 + itemized_medical_expenses: 10_000 + itemized_medical_expenses_applied_exclusion: 6_000 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # Line 1: 10,000 - 3,000 = 7,000. Line 2: 6,000 - 6,000 = 0. + mt_medical_expense_deduction_joint: 7_000 + +- name: A net Schedule A subtotal with part of the premiums taken as the self-employed deduction + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 6_000 + self_employed_health_insurance_ald_person: 1_000 + tax_units: + tax_unit: + members: [person1] + # Schedule A line 1: (6,000 - 1,000) of premiums + 5,000 of other + # expenses. Compare the gross-cost case above, where the same 10,000 + # includes all 6,000 of the premiums. + itemized_medical_expenses: 10_000 + itemized_medical_expenses_applied_exclusion: 1_000 + mt_agi_joint: 30_000 + households: + household: + members: [person1] + state_code: MT + output: + # Medical premiums left in the subtotal: 6,000 - min(6,000, 1,000) = 5,000. + # Line 1: (10,000 - 5,000) - 7.5% x 30,000 = 2,750. + # Line 2: 6,000 - 1,000 = 5,000. + mt_medical_expense_deduction_joint: 7_750 + +- name: A supplied exclusion above the filers' medical premiums is not taken from a dependent's premiums + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + health_insurance_premiums: 6_000 + long_term_health_insurance_premiums: 400 + self_employed_health_insurance_ald_person: 6_400 + person2: + is_tax_unit_dependent: true + age: 10 + health_insurance_premiums: 1_000 + tax_units: + tax_unit: + members: [person1, person2] + # Schedule A line 1: 10,000 of other expenses + the dependent's 1,000; + # the head's 6,000 and 400 of long-term care were taken as the + # self-employed deduction. + itemized_medical_expenses: 11_000 + itemized_medical_expenses_applied_exclusion: 6_400 + mt_agi_joint: 40_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Medical premiums left in the subtotal: 7,000 - min(6,000, 6,400) = 1,000. + # Line 1: (11,000 - 1,000) - 3,000 = 7,000. The filer's premiums were + # already deducted, and the dependent's own payments do not enter line 2. + mt_premium_deduction_joint: 0 + mt_medical_expense_deduction_joint: [7_000, 0] + +- name: A partial self-employed deduction leaves part of the long-term care premiums in the subtotal + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + age: 30 + health_insurance_premiums: 6_000 + long_term_health_insurance_premiums: 400 + self_employed_health_insurance_ald_person: 6_200 + tax_units: + tax_unit: + members: [person1] + # The 6,200 deduction covers the 6,000 of medical premiums and 200 of + # the long-term care premiums (medical first); Schedule A line 1 is + # 10,000 of other expenses + the other 200. + itemized_medical_expenses: 10_200 + itemized_medical_expenses_applied_exclusion: 6_200 + itemized_medical_expenses_long_term_care_premiums: 200 + mt_agi_joint: 40_000 + households: + household: + members: [person1] + state_code: MT + output: + # Line 1: (10,200 - 0 - 200) - 3,000 = 7,000. + # Line 3: 400 - (6,200 - 6,000) = 200. + mt_medical_expense_deduction_joint: 7_200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_premium_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_premium_deduction_joint.yaml new file mode 100644 index 00000000000..090e75eacba --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_premium_deduction_joint.yaml @@ -0,0 +1,133 @@ +- name: A self-employed deduction supplied only for the return is netted from the filers' premiums + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 6_000 + person2: + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + self_employed_health_insurance_ald: 2_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 2: 6,000 - 2,000 deducted in Montana AGI = 4,000. + mt_premium_deduction_joint: 4_000 + +- name: A return's self-employed deduction covering long-term care premiums removes them from line 3 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + long_term_health_insurance_premiums: 400 + tax_units: + tax_unit: + members: [person1] + self_employed_health_insurance_ald: 400 + households: + household: + members: [person1] + state_code: MT + output: + mt_premium_deduction_joint: 0 + +- name: The return's deduction left after medical premiums reduces either spouse's long-term care premiums + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 2_000 + person2: + is_tax_unit_spouse: true + long_term_health_insurance_premiums: 1_500 + tax_units: + tax_unit: + members: [person1, person2] + self_employed_health_insurance_ald: 3_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # Line 2: 2,000 - 2,000 = 0. Line 3: 1,500 - (3,000 - 2,000) = 500. + itemized_medical_expenses_excluded_premiums: 2_000 + mt_premium_deduction_joint: 500 + +- name: Each spouse's own deduction is netted once across the return + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 6_000 + self_employed_health_insurance_ald_person: 1_000 + person2: + is_tax_unit_spouse: true + health_insurance_premiums: 3_000 + self_employed_health_insurance_ald_person: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + self_employed_health_insurance_ald: 3_000 + # (6,000 + 3,000) - (1,000 + 2,000) = 6,000. + mt_premium_deduction_joint: 6_000 + +- name: A dependent's own premiums and deduction do not enter the filer's deduction + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 3_000 + person2: + is_tax_unit_dependent: true + health_insurance_premiums: 1_000 + long_term_health_insurance_premiums: 300 + self_employed_health_insurance_ald_person: 1_200 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Neither the dependent's premiums nor their deduction belongs to the + # filer. Only the head's 3,000 counts, with no self-employed exclusion. + self_employed_health_insurance_ald: 0 + mt_premium_deduction_joint: 3_000 + +- name: Outside Montana the amount is zero + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_tax_unit_head: true + health_insurance_premiums: 6_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ID + output: + mt_premium_deduction_joint: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml index a8a8257979a..f7b07d2ac3c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml @@ -212,3 +212,16 @@ itemized_medical_expenses_applied_exclusion: 1_000 # 40,000 - 3,000 - (5,500 + 1,000) = 30,500. nd_renters_refund_income: 30_500 + +- name: North Dakota restores the exclusion supplied with a net Schedule A subtotal + period: 2025 + input: + state_code: ND + employment_income: 30_000 + # Schedule A line 1 of 10,000 leaves out 6,400 of premiums (6,000 + # medical, 400 long-term care) taken as the self-employed deduction. + itemized_medical_expenses: 10_000 + itemized_medical_expenses_applied_exclusion: 6_400 + output: + # Medical costs paid: 10,000 + 6,400 = 16,400. + nd_renters_refund_income: 13_600 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml index bccb87ffc58..be256f04348 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml @@ -501,3 +501,17 @@ state_code: NM output: nm_medical_expense_credit: 2800 + +- name: New Mexico credit restores the exclusion supplied with a net Schedule A subtotal + period: 2025 + input: + state_code: NM + age: 65 + medicare_enrolled: false + claimed_as_dependent_on_another_return: false + # Schedule A line 1 of 25,000 leaves out 6,400 of premiums taken as the + # self-employed deduction; costs paid are 31,400, above the 28,000 floor. + itemized_medical_expenses: 25_000 + itemized_medical_expenses_applied_exclusion: 6_400 + output: + nm_medical_expense_credit: 2_800 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml index 0f3d7ef4e76..699e8768ad5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml @@ -177,3 +177,16 @@ state_code: NM output: nm_medical_expense_exemption: 3_000 + +- name: New Mexico restores the exclusion supplied with a net Schedule A subtotal + period: 2025 + input: + state_code: NM + age: 65 + medicare_enrolled: false + # Schedule A line 1 of 25,000 leaves out 6,400 of premiums taken as the + # self-employed deduction; costs paid are 31,400, above the 28,000 floor. + itemized_medical_expenses: 25_000 + itemized_medical_expenses_applied_exclusion: 6_400 + output: + nm_medical_expense_exemption: 3_000 diff --git a/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py b/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py index aa925d3fb6d..c9ea424b401 100644 --- a/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py +++ b/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py @@ -11,9 +11,10 @@ The deterministic tests evaluate every point of an input grid in one vectorized simulation; Hypothesis adds generated batches that check the same invariants: -- 2021-2023: the medical deduction is max(0, expenses - 7.5% of the return's - Montana AGI), and the standard deduction is 20% of that AGI held between the - filing status minimum and maximum; +- 2021-2023: the medical deduction is max(0, expenses other than premiums - + 7.5% of the return's Montana AGI) plus the premiums deducted in full on + lines 2 and 3, and the standard deduction is 20% of that AGI held between + the filing status minimum and maximum; - moving an income item between spouses preserves both deductions when the return's applicable AGI and medical expenses are held fixed; - each deduction is held once, by the head; @@ -163,23 +164,30 @@ def test_premises(grid, year): @pytest.mark.parametrize("year", STATE_YEARS) def test_medical_floor_uses_the_return_montana_agi(grid, year): sim, unit, role = grid - expenses = sim.calculate("itemized_medical_expenses", year) - expected = np.maximum( - expenses - MEDICAL_FLOOR * montana_agi_reference(sim, year), 0 + # Line 1 floors expenses other than premiums; lines 2 and 3 deduct the + # premiums in full. The grid's age-70 single filers have modeled + # Medicare Part B premiums. + expenses = per_unit(sim, "other_medical_expenses", year, unit) + premiums = per_unit( + sim, "mt_eligible_medical_insurance_premiums", year, unit + ) + per_unit(sim, "mt_eligible_long_term_care_insurance_premiums", year, unit) + expected = ( + np.maximum(expenses - MEDICAL_FLOOR * montana_agi_reference(sim, year), 0) + + premiums ) deduction = per_unit(sim, "mt_medical_expense_deduction_joint", year, unit) np.testing.assert_allclose(deduction, expected, atol=0.01) held = sim.calculate("mt_medical_expense_deduction_joint", year) assert np.all(held[role != "head"] == 0) - assert np.all(deduction >= 0) - assert np.all(deduction <= expenses + 0.01) + assert np.all(deduction >= premiums - 0.01) + assert np.all(deduction <= expenses + premiums + 0.01) spouse_side, head_side = twin_pairs() np.testing.assert_allclose(deduction[spouse_side], deduction[head_side]) assert deduction.max() > 0 # Adding each spouse's Montana AGI after flooring it at zero would change # the deduction somewhere on the grid. summed_individual = per_unit(sim, "mt_agi_indiv", year, unit) - unpooled = np.maximum(expenses - MEDICAL_FLOOR * summed_individual, 0) + unpooled = np.maximum(expenses - MEDICAL_FLOOR * summed_individual, 0) + premiums assert np.any(np.abs(unpooled - expected) > 1) diff --git a/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py b/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py new file mode 100644 index 00000000000..a5a90d8dceb --- /dev/null +++ b/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py @@ -0,0 +1,587 @@ +"""Invariants for Montana's 2021-2023 medical insurance premium deduction. + +The 2021-2023 Form 2 Itemized Deductions Schedule floors only line 1, medical +and dental expenses other than insurance premiums, at 7.5% of Montana AGI. +Line 2 deducts medical insurance premiums in full and line 3 long-term care +insurance premiums in full, except premiums already deducted through the +self-employed health insurance deduction, and pre-tax premiums. Former MCA +15-30-2131(1)(a)(iii)-(iv) and (1)(g). From 2024 Montana takes federal +itemized deductions, where premiums stay under the federal floor. + +For the head and spouse that deduction is part of Montana AGI. Premium inputs +use payer attribution: payments for dependent coverage belong on the paying +filer's input. A dependent's own premiums and self-employed deduction belong +on the dependent's own return and do not enter the filers' premium deduction. +The properties below hold for those attributed inputs and the deduction as +modeled; they do not independently establish who paid a premium. + +Why Python rather than YAML: Hypothesis generates mixed populations (single, +head of household and joint returns with dependents; premiums reported +directly, decomposed or with Medicare Part B), and every batch also holds +twins of each return whose differences are asserted. YAML cases check fixed +examples; these properties check relations between generated returns. + +Each batch takes the self-employed deduction one way: derived from +self-employment income ("derived"), supplied for every person ("person"), or +supplied for every return with person amounts derived ("unit"). The mode is +per batch because core treats a formula variable supplied for some members as +an input for all of them, filling the rest with 0. + +Each batch runs as one vectorized simulation holding three copies of every +tax unit: the drawn return, a twin with higher after-tax and long-term care +premiums for people whose premiums no deduction can absorb, and a twin with +no pre-tax premiums. The 7.5% rate is taken from Form 2 line 1c rather than +the model's parameters. Montana AGI and the self-employed deductions are read +from the model; they are inputs to these properties, not derived +independently. For every tax unit: + +1. Accounting (2021-2023): the joint deduction is + max(0, other expenses - 7.5% x Montana AGI) + lines 2 and 3, held by the + head. The separate variant is max(0, own other expenses - 7.5% x own + Montana AGI) + own lines 2 and 3 for the head and spouse; dependents hold + nothing. +2. Premium conservation against closed forms. Per-person helpers: + line 2 + line 3 = max(0, premiums + long-term care + premiums - own deduction), and line 2 = premiums - min(premiums, own + deduction). Joint return: lines 2 and 3 = max(0, filers' premiums + + filers' long-term care premiums - the return's deduction). Only the head + and spouse claim the per-person helpers on separate returns. Premiums are + computed from the raw inputs. Each deduction is netted exactly once. +3. Bounds (2021-2023): premiums are never floored, so the deduction is at + least lines 2 and 3 and at most those plus other expenses. +4. Slope (2021-2023): raising filers' after-tax or long-term care premiums + that no deduction can absorb raises the deduction by exactly the increase. + Raising dependent-paid premiums does not increase the filers' deduction. + Montana AGI is unchanged by those increases (asserted). +5. Pre-tax premiums never change lines 2 or 3. +6. Line 1 base: other expenses equal federal itemized medical expenses plus + the self-employed premiums those exclude, less the premiums. Line 1 + therefore tracks the federal definition apart from the premiums. +7. Differential (2021-2023): for single filers whose deduction is recorded + per person, the joint and separate variants agree. +8. From 2024 the joint deduction is the federal medical expense deduction and + the separate variant is zero. +9. Differential (seeded, 2021-2023): supplying a net Schedule A subtotal, the + exclusion of all premiums the self-employed deduction took (medical + first), and the long-term care premiums still inside gives the same + Montana deduction as the computed path. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +# Dollars; the model computes in float32 and unit amounts reach about $100,000. +TOLERANCE = 0.05 +FORM_FLOOR = 0.075 # Itemized Deductions Schedule line 1c +STATE_YEARS = [2021, 2022, 2023] +FEDERAL_YEAR = 2024 +YEARS = STATE_YEARS + [FEDERAL_YEAR] +MODES = ["derived", "person", "unit"] +VARIANTS = ["base", "raised", "no_pre_tax"] + +amount = st.one_of(st.just(0.0), st.integers(1, 15_000).map(float)) +small_amount = st.one_of(st.just(0.0), st.integers(1, 5_000).map(float)) +deduction_amount = st.one_of(st.just(0.0), st.integers(1, 20_000).map(float)) + + +@st.composite +def people(draw): + self_employed = draw(st.booleans()) + return { + "employment_income": float(draw(st.integers(0, 150_000))), + "self_employed": self_employed, + "self_employment_income": ( + float(draw(st.integers(-20_000, 60_000))) if self_employed else 0.0 + ), + # A deduction claimed on Schedule 1, line 17, used in "person" mode. + "supplied_deduction": draw(deduction_amount), + "premium_source": draw(st.sampled_from(["direct", "decomposed", "medicare"])), + "premium": draw(amount), + "part_b": draw(st.integers(0, 3_000).map(float)), + "other_medical": draw( + st.one_of(st.just(0.0), st.integers(1, 30_000).map(float)) + ), + "long_term_care": draw(small_amount), + "pre_tax": draw(small_amount), + "premium_increase": draw(small_amount), + "long_term_care_increase": draw(small_amount), + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "hoh", "joint", "joint_dependents"])) + n_dependents = draw(st.integers(1, 2)) if kind in ("hoh", "joint_dependents") else 0 + return { + "head": draw(people()), + "spouse": draw(people()) if kind.startswith("joint") else None, + "dependents": [draw(people()) for _ in range(n_dependents)], + # The return's deduction, used in "unit" mode. + "supplied_unit_deduction": draw(deduction_amount), + } + + +SEED = 20261006 + + +def _seeded_units(n=120): + rng = np.random.default_rng(SEED) + + def maybe(high): + return float(rng.integers(1, high + 1)) if rng.random() < 0.6 else 0.0 + + def person(): + self_employed = bool(rng.random() < 0.3) + return { + "employment_income": float(rng.integers(0, 150_001)), + "self_employed": self_employed, + "self_employment_income": ( + float(rng.integers(-20_000, 60_001)) if self_employed else 0.0 + ), + "supplied_deduction": maybe(20_000), + "premium_source": str(rng.choice(["direct", "decomposed", "medicare"])), + "premium": maybe(15_000), + "part_b": float(rng.integers(0, 3_001)), + "other_medical": maybe(30_000), + "long_term_care": maybe(5_000), + "pre_tax": maybe(5_000), + "premium_increase": maybe(5_000), + "long_term_care_increase": maybe(5_000), + } + + units = [] + for _ in range(n): + kind = rng.choice(["single", "hoh", "joint", "joint_dependents"]) + n_dependents = ( + int(rng.integers(1, 3)) if kind in ("hoh", "joint_dependents") else 0 + ) + units.append( + { + "head": person(), + "spouse": person() if kind.startswith("joint") else None, + "dependents": [person() for _ in range(n_dependents)], + "supplied_unit_deduction": maybe(20_000), + } + ) + return units + + +def _entries(unit): + entries = [("head", unit["head"])] + if unit["spouse"] is not None: + entries.append(("spouse", unit["spouse"])) + return entries + [("dependent", d) for d in unit["dependents"]] + + +def _no_own_deduction(values, mode): + """Whether the person's own deduction is known to be zero.""" + if mode == "person": + return values["supplied_deduction"] == 0 + return not values["self_employed"] + + +def _raisable(unit, role, values, mode): + """Whether no deduction can absorb a premium increase for this person. + + The return's deduction is pooled over the head's and spouse's premiums, + so any filer's deduction can absorb another filer's increase. + """ + if role == "dependent": + return _no_own_deduction(values, mode) + filers = [v for r, v in _entries(unit) if r != "dependent"] + if not all(_no_own_deduction(v, mode) for v in filers): + return False + return mode != "unit" or unit["supplied_unit_deduction"] == 0 + + +def _increases(unit, role, values, mode, variant): + if variant != "raised" or not _raisable(unit, role, values, mode): + return 0.0, 0.0 + return values["premium_increase"], values["long_term_care_increase"] + + +def _person_inputs(unit, role, values, mode, variant): + premium_increase, long_term_care_increase = _increases( + unit, role, values, mode, variant + ) + premium = values["premium"] + premium_increase + source = values["premium_source"] + inputs = { + "age": 10 if role == "dependent" else 40, + "is_tax_unit_head": role == "head", + "is_tax_unit_spouse": role == "spouse", + "is_tax_unit_dependent": role == "dependent", + "employment_income": values["employment_income"], + "is_self_employed": values["self_employed"], + "self_employment_income": values["self_employment_income"], + "other_medical_expenses": values["other_medical"], + "long_term_health_insurance_premiums": values["long_term_care"] + + long_term_care_increase, + "pre_tax_health_insurance_premiums": ( + 0.0 if variant == "no_pre_tax" else values["pre_tax"] + ), + "health_insurance_premiums": premium if source == "direct" else 0.0, + "health_insurance_premiums_without_medicare_part_b": ( + 0.0 if source == "direct" else premium + ), + "medicare_enrolled": source == "medicare", + "medicare_part_b_premium": values["part_b"] if source == "medicare" else 0.0, + } + if mode == "person": + inputs["self_employed_health_insurance_ald_person"] = values[ + "supplied_deduction" + ] + return inputs + + +def _expected_premium(unit, role, values, mode, variant): + """Premiums counted as federal medical expenses, from the inputs alone.""" + premium = values["premium"] + _increases(unit, role, values, mode, variant)[0] + if values["premium_source"] == "direct" and premium != 0: + return premium + part_b = values["part_b"] if values["premium_source"] == "medicare" else 0.0 + return (0.0 if values["premium_source"] == "direct" else premium) + part_b + + +def _situation(units, year, mode, unit_inputs=None): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + expected_premium, increase, roles = [], [], [] + unit_index = 0 + for variant in VARIANTS: + for i, unit in enumerate(units): + tag = f"{variant}_{i}" + members, couple = [], [] + unit_increase = 0.0 + for j, (role, values) in enumerate(_entries(unit)): + name = f"{tag}_{role}_{j}" + people[name] = _person_inputs(unit, role, values, mode, variant) + members.append(name) + expected_premium.append( + _expected_premium(unit, role, values, mode, variant) + ) + roles.append(role) + if role == "dependent": + groups["marital_units"][f"{tag}_single_{j}"] = {"members": [name]} + else: + couple.append(name) + unit_increase += sum(_increases(unit, role, values, mode, "raised")) + increase.append(unit_increase) + groups["marital_units"][f"{tag}_couple"] = {"members": couple} + tax_unit = {"members": members} + if mode == "unit": + tax_unit["self_employed_health_insurance_ald"] = { + year: unit["supplied_unit_deduction"] + } + # Supplied for every tax unit, so none falls back to 0. + for name, values in (unit_inputs or {}).items(): + tax_unit[name] = {year: float(values[unit_index])} + unit_index += 1 + groups["tax_units"][f"{tag}_tax_unit"] = tax_unit + groups["households"][f"{tag}_household"] = { + "members": members, + # Keyed by year: a bare value is not applied to the run year. + "state_code": {year: "MT"}, + } + people = { + name: {key: {year: value} for key, value in values.items()} + for name, values in people.items() + } + return ( + {"people": people, **groups}, + np.array(expected_premium), + np.array(increase), + np.array(roles), + ) + + +PERSON_VARIABLES = [ + "other_medical_expenses", + "long_term_health_insurance_premiums", + "medical_expense_health_insurance_premiums", + "self_employed_health_insurance_ald_person", + "mt_eligible_medical_insurance_premiums", + "mt_eligible_long_term_care_insurance_premiums", + "mt_agi_indiv", + "mt_medical_expense_deduction_joint", + "mt_medical_expense_deduction_indiv", +] +UNIT_VARIABLES = [ + "mt_agi_joint", + "self_employed_health_insurance_ald", + "mt_premium_deduction_joint", + "itemized_medical_expenses", + "itemized_medical_expenses_applied_exclusion", + "itemized_medical_expenses_excluded_premiums", + "itemized_medical_expenses_long_term_care_premiums", + "medical_expense_deduction", +] + + +def _run(units, year, mode, unit_inputs=None): + situation, expected_premium, increase, roles = _situation( + units, year, mode, unit_inputs + ) + sim = Simulation(situation=situation) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in PERSON_VARIABLES + UNIT_VARIABLES + } + out["unit"] = sim.populations["tax_unit"].members_entity_id + out["filing_status"] = sim.calculate("filing_status", year).decode_to_str() + out["state_code"] = sim.calculate("state_code", year).decode_to_str() + out["expected_premium"] = expected_premium + out["increase"] = increase + out["role"] = roles + return out + + +def _unit_sum(run, values): + return np.bincount(run["unit"], weights=values, minlength=len(run["mt_agi_joint"])) + + +def _check(units, year, mode): + run = _run(units, year, mode) + n = len(units) + base, raised, no_pre_tax = (slice(k * n, (k + 1) * n) for k in range(3)) + role = run["role"] + joint = run["mt_medical_expense_deduction_joint"] + indiv = run["mt_medical_expense_deduction_indiv"] + deduction = _unit_sum(run, joint) + assert (run["state_code"] == "MT").all() + assert (joint[role != "head"] == 0).all() + + if year == FEDERAL_YEAR: + # 8. Federal treatment from 2024. + np.testing.assert_allclose( + deduction, run["medical_expense_deduction"], atol=TOLERANCE + ) + assert (indiv == 0).all() + return None + + premium = run["medical_expense_health_insurance_premiums"] + long_term_care = run["long_term_health_insurance_premiums"] + own_deduction = np.maximum(run["self_employed_health_insurance_ald_person"], 0) + line_2 = run["mt_eligible_medical_insurance_premiums"] + line_3 = run["mt_eligible_long_term_care_insurance_premiums"] + other = run["other_medical_expenses"] + filer = role != "dependent" + + # 2. Premium conservation against closed forms. + np.testing.assert_allclose(premium, run["expected_premium"], atol=TOLERANCE) + if mode == "person": + supplied = np.array( + [ + v["supplied_deduction"] + for _ in VARIANTS + for unit in units + for _, v in _entries(unit) + ] + ) + np.testing.assert_allclose(own_deduction, supplied, atol=TOLERANCE) + if mode == "unit": + supplied_unit = np.array( + [unit["supplied_unit_deduction"] for _ in VARIANTS for unit in units] + ) + np.testing.assert_allclose( + run["self_employed_health_insurance_ald"], supplied_unit, atol=TOLERANCE + ) + np.testing.assert_allclose( + line_2 + line_3, + np.maximum(run["expected_premium"] + long_term_care - own_deduction, 0), + atol=TOLERANCE, + ) + np.testing.assert_allclose( + line_2, + run["expected_premium"] - np.minimum(run["expected_premium"], own_deduction), + atol=TOLERANCE, + ) + assert (line_3 <= long_term_care + TOLERANCE).all() + return_deduction = np.maximum(run["self_employed_health_insurance_ald"], 0) + expected_joint_premiums = np.maximum( + _unit_sum(run, filer * (run["expected_premium"] + long_term_care)) + - return_deduction, + 0, + ) + full = run["mt_premium_deduction_joint"] + np.testing.assert_allclose(full, expected_joint_premiums, atol=TOLERANCE) + + # 1. Accounting, joint and separate. + floored = np.maximum(_unit_sum(run, other) - FORM_FLOOR * run["mt_agi_joint"], 0) + np.testing.assert_allclose(deduction, floored + full, atol=TOLERANCE) + expected_indiv = filer * ( + np.maximum(other - FORM_FLOOR * run["mt_agi_indiv"], 0) + line_2 + line_3 + ) + np.testing.assert_allclose(indiv, expected_indiv, atol=TOLERANCE) + + # 3. Bounds. + assert (deduction >= full - TOLERANCE).all() + assert (deduction <= full + _unit_sum(run, other) + TOLERANCE).all() + + # 4. Slope of one for filer increases no deduction can absorb, and zero + # for dependent increases. The raised twin changes both sets of payers. + np.testing.assert_allclose( + run["mt_agi_joint"][raised], run["mt_agi_joint"][base], atol=TOLERANCE + ) + np.testing.assert_allclose( + deduction[raised] - deduction[base], + run["increase"][raised], + atol=TOLERANCE, + ) + + # 5. Pre-tax premiums never reach lines 2 or 3. + np.testing.assert_allclose(full[no_pre_tax], full[base], atol=TOLERANCE) + np.testing.assert_allclose( + _unit_sum(run, line_2 + line_3)[no_pre_tax], + _unit_sum(run, line_2 + line_3)[base], + atol=TOLERANCE, + ) + + # 6. Line 1 base tracks the federal definition apart from the premiums. + # The generator never supplies the subtotal, so it holds no long-term care + # premiums. + assert (run["itemized_medical_expenses_long_term_care_premiums"] == 0).all() + np.testing.assert_allclose( + _unit_sum(run, other), + run["itemized_medical_expenses"] + + run["itemized_medical_expenses_applied_exclusion"] + - _unit_sum(run, premium) + - run["itemized_medical_expenses_long_term_care_premiums"], + atol=TOLERANCE, + ) + + # 7. Single filers with person-level deductions: both variants agree. + if mode != "unit": + single = run["filing_status"] == "SINGLE" + np.testing.assert_allclose( + deduction[single], _unit_sum(run, indiv)[single], atol=TOLERANCE + ) + return run, floored, full + + +# A batch's cost is mostly the simulation build and 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(YEARS), + st.sampled_from(MODES), + st.lists(tax_units(), min_size=5, max_size=20), +) +def test_mt_medical_premium_deduction_invariants(year, mode, units): + _check(units, year, mode) + + +# Each state year takes the deduction a different way; 2024 checks the federal +# branch. +SEEDED_CASES = [(2021, "derived"), (2022, "person"), (2023, "unit"), (2024, "unit")] + + +@pytest.mark.parametrize("year,mode", SEEDED_CASES) +def test_seeded_population(year, mode): + units = _seeded_units() + result = _check(units, year, mode) + if result is None: + return + run, floored, full = result + # The slope check includes dependent-only increases: their premiums rise + # while the filers' premiums and deduction remain unchanged. + dependent_increase = np.array( + [ + sum( + sum(_increases(unit, role, values, mode, "raised")) + for role, values in _entries(unit) + if role == "dependent" + ) + for unit in units + ] + ) + assert np.any((dependent_increase > 1) & (run["increase"][: len(units)] == 0)) + # Non-vacuity: some returns deduct premiums while line 1 is below the + # floor, where flooring premiums with other expenses would cut the + # deduction; some deduct line 1 too; there are single and joint returns; + # and each mode's deduction reduces lines 2 and 3 somewhere. + agi_floor = FORM_FLOOR * run["mt_agi_joint"] + total = _unit_sum( + run, + run["other_medical_expenses"] + + run["medical_expense_health_insurance_premiums"], + ) + assert np.any((full > 0) & (total < agi_floor)) + assert np.any((full > 0) & (floored > 0)) + assert np.any(run["filing_status"] == "SINGLE") + assert np.any(run["filing_status"] == "JOINT") + deduction = run["self_employed_health_insurance_ald_person"] + premiums = run["medical_expense_health_insurance_premiums"] + long_term_care = run["long_term_health_insurance_premiums"] + line_2 = run["mt_eligible_medical_insurance_premiums"] + line_3 = run["mt_eligible_long_term_care_insurance_premiums"] + if mode == "derived": + # A deduction derived from self-employment income reduces line 2. + assert np.any((deduction > 0) & (line_2 < premiums - 1)) + if mode == "person": + # A deduction above the medical premiums reduces line 3. + assert np.any((deduction > premiums) & (line_3 < long_term_care - 1)) + if mode == "unit": + # A return-level deduction reduces the filers' lines 2 and 3. + filer = run["role"] != "dependent" + gross = _unit_sum(run, filer * (premiums + long_term_care)) + assert np.any(full < gross - 1) + _check_net_subtotal(run, units, year, mode) + + +def _check_net_subtotal(run, units, year, mode): + """9. A net Schedule A subtotal gives the computed path's deduction. + + Supply, for every return, the subtotal as Schedule A line 1 would hold it + (the computed subtotal plus the filers' long-term care premiums the + self-employed deduction did not take), the exclusion as all premiums that + deduction took (medical first), and the long-term care premiums still + inside. Montana's deduction must not change. + """ + filer = run["role"] != "dependent" + return_deduction = np.maximum(run["self_employed_health_insurance_ald"], 0) + medical_excluded = run["itemized_medical_expenses_excluded_premiums"] + filer_long_term_care = _unit_sum( + run, filer * run["long_term_health_insurance_premiums"] + ) + long_term_care_excluded = np.minimum( + filer_long_term_care, return_deduction - medical_excluded + ) + inside = filer_long_term_care - long_term_care_excluded + net = _run( + units, + year, + mode, + { + "itemized_medical_expenses": run["itemized_medical_expenses"] + inside, + "itemized_medical_expenses_applied_exclusion": medical_excluded + + long_term_care_excluded, + "itemized_medical_expenses_long_term_care_premiums": inside, + }, + ) + np.testing.assert_allclose(net["mt_agi_joint"], run["mt_agi_joint"], atol=TOLERANCE) + np.testing.assert_allclose( + _unit_sum(net, net["mt_medical_expense_deduction_joint"]), + _unit_sum(run, run["mt_medical_expense_deduction_joint"]), + atol=TOLERANCE, + ) + # Non-vacuity: with supplied deductions, some exclusions cover long-term + # care premiums on returns with dependents' premiums, where taking the + # exclusion from every member's premiums would differ. A derived + # deduction never exceeds the filers' own medical premiums. + if mode != "derived": + dependent_premiums = _unit_sum( + run, ~filer * run["medical_expense_health_insurance_premiums"] + ) + assert np.any((long_term_care_excluded > 1) & (dependent_premiums > 1)) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py index 91538c9b38a..f953f779949 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py @@ -28,7 +28,18 @@ class itemized_medical_expenses(Variable): "of whom it covers. A parent-paid dependent policy belongs on the parent. " "The exclusion is bounded to filer-paid premiums; beneficiary-attributed " "inputs with an ALD exceeding that pool are inconsistent with this contract " - "and cannot establish conservation of the supplied ALD." + "and cannot establish conservation of the supplied ALD. A supplied value " + "bypasses this formula and is used as given. By default it is taken as " + "medical costs paid before any self-employed exclusion " + "(itemized_medical_expenses_applied_exclusion is 0). If it is a Schedule " + "A line 1 amount that already leaves out premiums taken as the " + "self-employed health insurance deduction, also supply all of those " + "premiums, medical and long-term care, as " + "itemized_medical_expenses_applied_exclusion; the exclusion is taken " + "from the head's and spouse's medical insurance premiums first, as this " + "formula does. Report any long-term care premiums still inside the " + "supplied value, by that same split, in " + "itemized_medical_expenses_long_term_care_premiums." ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py index f034c37903a..e0078156600 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py @@ -14,9 +14,17 @@ class itemized_medical_expenses_applied_exclusion(Variable): documentation = ( "Premiums actually excluded by the federal medical expense formula. " "A caller-supplied subtotal bypasses that formula, so no exclusion " - "was applied. States retaining gross paid medical costs restore this " - "amount only for computed subtotals. Premium inputs use payer " - "attribution, including family coverage paid by a filer. The required " + "was applied, unless the caller also supplies this amount: all the " + "premiums, medical and long-term care, that a supplied Schedule A " + "subtotal already leaves out because they were taken as the " + "self-employed health insurance deduction, taken from the head's and " + "spouse's medical insurance premiums first. In a simulation with " + "several tax units, supply it for each of them, since a formula " + "variable supplied for some units is 0 for the rest. States retaining " + "gross paid medical costs restore this amount; Montana's 2021-2023 " + "schedule uses it to take only the medical premiums still inside the " + "subtotal out of line 1. Premium inputs use payer attribution, " + "including family coverage paid by a filer. The required " "policyengine-core>=3.32.27 clears input provenance on deletion, so " "recalculating a deleted subtotal also restores its applied exclusion." ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py index c07cab1c17e..05fdcb6893d 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py @@ -17,8 +17,9 @@ class itemized_medical_expenses_excluded_premiums(Variable): "including family coverage paid by a filer. This preserves supplied " "tax-unit ALDs without allocating them to individual filers. States " "that retain gross paid medical costs use " - "itemized_medical_expenses_applied_exclusion to restore only premiums " - "actually excluded by the federal formula." + "itemized_medical_expenses_applied_exclusion to restore the premiums " + "excluded from the subtotal: these, for a computed subtotal, or the " + "exclusion a caller supplies with a supplied one." ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py new file mode 100644 index 00000000000..b6ed728983e --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class itemized_medical_expenses_long_term_care_premiums(Variable): + value_type = float + entity = TaxUnit + label = "Long-term care premiums included in supplied itemized medical expenses" + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/213#d_10", + "https://www.irs.gov/instructions/i1040sca", + ) + documentation = ( + "The long-term care insurance premiums that a supplied " + "itemized_medical_expenses includes, such as eligible premiums " + "counted on Schedule A line 1 and not taken as the self-employed " + "health insurance deduction. The computed subtotal counts no " + "long-term care premiums, so leave this at 0 unless you supply a " + "subtotal that includes them. States that deduct long-term care " + "premiums separately remove this amount from the subtotal first. With " + "a subtotal that leaves out premiums taken as the self-employed " + "deduction, follow the split itemized_medical_expenses_applied_exclusion " + "uses, whatever the return itself did: this is the eligible long-term " + "care premiums less any part of that exclusion above the head's and " + "spouse's medical insurance premiums." + ) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_eligible_long_term_care_insurance_premiums.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_eligible_long_term_care_insurance_premiums.py new file mode 100644 index 00000000000..647f01994f4 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_eligible_long_term_care_insurance_premiums.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class mt_eligible_long_term_care_insurance_premiums(Variable): + value_type = float + entity = Person + definition_period = YEAR + label = "Montana long-term care insurance premiums deductible in full" + documentation = ( + "A person's long-term care insurance premiums that Montana's " + "2021-2023 Itemized Deductions Schedule line 3 deducts in full, " + "without the federal age-based limit, less any of those premiums the " + "person deducted through the self-employed health insurance " + "deduction. That deduction is applied first to the medical insurance " + "premiums on line 2 and the remainder to these premiums; lines 2 and " + "3 are both deducted in full, so the order does not change the total. " + "This per-person amount serves the head and spouse on separate " + "returns; a joint return nets the return's deduction in " + "mt_premium_deduction_joint." + ) + reference = ( + "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-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", + # MCA 2021 § 15-30-2131(1)(a)(iv) and (1)(g)(ii) + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + # 26 U.S.C. 162(l)(2)(C): eligible long-term care premiums can be part + # of the self-employed health insurance deduction. + "https://www.law.cornell.edu/uscode/text/26/162#l_2_C", + ) + unit = USD + defined_for = StateCode.MT + + def formula(person, period, parameters): + premiums = person("long_term_health_insurance_premiums", period) + deduction = max_(0, person("self_employed_health_insurance_ald_person", period)) + used_on_line_2 = person( + "self_employed_health_insurance_ald_excluded_premiums", period + ) + return max_(0, premiums - (deduction - used_on_line_2)) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_eligible_medical_insurance_premiums.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_eligible_medical_insurance_premiums.py new file mode 100644 index 00000000000..7f358a784ff --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_eligible_medical_insurance_premiums.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * + + +class mt_eligible_medical_insurance_premiums(Variable): + value_type = float + entity = Person + definition_period = YEAR + label = "Montana medical insurance premiums deductible in full" + documentation = ( + "A person's medical insurance premiums that Montana's 2021-2023 " + "Itemized Deductions Schedule line 2 deducts in full: the premiums " + "counted as federal medical expenses, including Medicare Part B, " + "less the premiums that person deducted through the self-employed " + "health insurance deduction. For the head and spouse that deduction " + "is part of Montana AGI; a dependent takes it on the dependent's own " + "return, so those premiums were not paid by the filer. Pre-tax " + "premiums are a separate input that never enters " + "medical_expense_health_insurance_premiums. Premium inputs use payer " + "attribution: each person reports the premiums that person paid. " + "This per-person amount serves the head and spouse on separate " + "returns; a joint return nets the return's deduction in " + "mt_premium_deduction_joint." + ) + reference = ( + "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-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", + # MCA 2021 § 15-30-2131(1)(a)(iii) and (1)(g)(i) + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + ) + unit = USD + defined_for = StateCode.MT + + def formula(person, period, parameters): + premiums = person("medical_expense_health_insurance_premiums", period) + # Premiums deducted in determining Montana AGI do not qualify: the + # self-employed health insurance deduction (Schedule 1, line 17). + deducted = person( + "self_employed_health_insurance_ald_excluded_premiums", period + ) + return premiums - deducted diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py index 5faa21eefb9..3237224cb20 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py @@ -11,33 +11,45 @@ class mt_medical_expense_deduction_indiv(Variable): reference = ( "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=36", "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/", - # MT Code § 15-30-2131 (2022) (1)(g)(i) + # PDF pages 32-33 + "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=32", + # MCA 2021 § 15-30-2131(1)(a) and (1)(g) + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", ) unit = USD defined_for = "mt_married_filing_separately_on_same_return_eligible" - documentation = ( - "Medical deduction after excluding the same person's self-employed " - "health insurance premiums and applying the medical floor. Premium " - "inputs use payer attribution: report all premiums paid by a person " - "on that person, including family coverage regardless of the covered " + "Each spouse's medical deduction when filing separately on the same " + "form. While the Itemized Deductions Schedule applies (2021-2023), " + "line 1 floors the spouse's medical and dental expenses other than " + "premiums at 7.5% of their Montana AGI, and lines 2 and 3 add their " + "medical and long-term care insurance premiums in full, net of their " + "self-employed health insurance deduction. If a reform allows " + "separate filing in a later year, the premiums net of that deduction " + "stay under the floor with the other expenses. Premium inputs use " + "payer attribution: report all premiums paid by a person on that " + "person, including family coverage regardless of the covered " "beneficiary." ) def formula(person, period, parameters): - premiums = person("medical_expense_health_insurance_premiums", period) - se_health_insurance_ald = person( - "self_employed_health_insurance_ald_person", period - ) - # Separately computed medical expenses exclude the same person's - # premiums already deducted from income. - expense = max_(0, premiums - se_health_insurance_ald) + person( - "other_medical_expenses", period - ) - p = parameters(period).gov.irs.deductions.itemized.medical + p = parameters(period).gov.states.mt.tax.income.deductions.itemized + floor = parameters(period).gov.irs.deductions.itemized.medical.floor + expense = person("other_medical_expenses", period) # Law does not define Montana AGI as the cap. # Tax form points to page 1, line 14, which is Montana AGI. - medical_floor = p.floor * person("mt_agi_indiv", period) + medical_floor = floor * person("mt_agi_indiv", period) + premiums = person("mt_eligible_medical_insurance_premiums", period) + if p.state_specific_deduction_applies: + # Line 1 floors expenses other than premiums; lines 2 and 3 + # deduct the premiums in full. + long_term_care_premiums = person( + "mt_eligible_long_term_care_insurance_premiums", period + ) + deduction = ( + max_(0, expense - medical_floor) + premiums + long_term_care_premiums + ) + else: + deduction = max_(0, premiums + expense - medical_floor) is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) - return is_head_or_spouse * max_(0, expense - medical_floor) + return is_head_or_spouse * deduction diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py index 182f474d45d..8bf845b16ac 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py @@ -8,10 +8,11 @@ class mt_medical_expense_deduction_joint(Variable): label = "Montana medical expense deduction when married couples are filing jointly" reference = ( "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", + # PDF pages 32-33 "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=32", "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", - "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", - # MT Code § 15-30-2131 (2022) (1)(a) + # MCA 2021 § 15-30-2131(1)(a) and (1)(g) + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", ) unit = USD defined_for = StateCode.MT @@ -19,13 +20,40 @@ class mt_medical_expense_deduction_joint(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.mt.tax.income.deductions.itemized if p.state_specific_deduction_applies: - expense = person.tax_unit("itemized_medical_expenses", period) + tax_unit = person.tax_unit floor = parameters(period).gov.irs.deductions.itemized.medical.floor - # Itemized Deductions Schedule line 1b takes Montana AGI from - # page 1, line 14. A joint return reports that line in a single - # column, so the floor applies to the return's pooled Montana AGI. - montana_agi = person.tax_unit("mt_agi_joint", period) - deduction = max_(0, expense - floor * montana_agi) + # Itemized Deductions Schedule line 1 takes the medical and dental + # expenses allowed under the Internal Revenue Code other than the + # insurance premiums deducted in full on lines 2 and 3. Start + # from the federal subtotal and take out the premiums still in + # it: the members' medical insurance premiums less the part of + # the self-employed exclusion applied to them, which is bounded by + # the head's and spouse's medical premiums (any excess covered + # long-term care premiums, which are not in the subtotal), and the + # long-term care premiums the subtotal includes. For a computed + # subtotal this leaves the members' other medical expenses. + premiums = add( + tax_unit, period, ["medical_expense_health_insurance_premiums"] + ) + filer_premiums = tax_unit_non_dep_sum( + "medical_expense_health_insurance_premiums", tax_unit, period + ) + excluded = tax_unit("itemized_medical_expenses_applied_exclusion", period) + premiums_in_subtotal = premiums - min_(filer_premiums, max_(0, excluded)) + expense = max_( + 0, + tax_unit("itemized_medical_expenses", period) + - premiums_in_subtotal + - tax_unit("itemized_medical_expenses_long_term_care_premiums", period), + ) + # Line 1b takes Montana AGI from page 1, line 14. A joint return + # reports that line in a single column, so the floor applies to + # the return's pooled Montana AGI. + montana_agi = tax_unit("mt_agi_joint", period) + floored_expense = max_(0, expense - floor * montana_agi) + # Lines 2 and 3: medical and long-term care insurance premiums not + # deducted elsewhere, in full. + deduction = floored_expense + tax_unit("mt_premium_deduction_joint", period) else: # From 2024, Form 2 line 2 takes federal itemized deductions # (Worksheet A), so the federal floor on federal AGI applies. diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_premium_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_premium_deduction_joint.py new file mode 100644 index 00000000000..08bcaadd5de --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_premium_deduction_joint.py @@ -0,0 +1,49 @@ +from policyengine_us.model_api import * + + +class mt_premium_deduction_joint(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Montana insurance premiums deducted in full on a joint return" + documentation = ( + "Itemized Deductions Schedule lines 2 and 3 for a single-column " + "return (2021-2023): medical and long-term care insurance premiums " + "deducted in full, except premiums already deducted. The " + "self-employed health insurance deduction reported for the return " + "(Schedule 1, line 17, for premiums the head and spouse paid) is " + "netted from their premiums once: first from medical insurance " + "premiums, by the same bounded exclusion the computed federal " + "Schedule A subtotal uses, then from long-term care premiums. A " + "deduction supplied for the return is honored. Premium inputs use " + "payer attribution: only the head's and spouse's payments count, " + "including payments for dependent coverage. A dependent's own " + "premium payments do not enter the filers' deduction." + ) + reference = ( + "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-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", + # MCA 2021 § 15-30-2131(1)(a)(iii)-(iv) and (1)(g) + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + # 26 U.S.C. 162(l)(2)(C) and (l)(3) + "https://www.law.cornell.edu/uscode/text/26/162#l_2_C", + ) + unit = USD + defined_for = StateCode.MT + + def formula(tax_unit, period, parameters): + premiums = tax_unit_non_dep_sum( + "medical_expense_health_insurance_premiums", tax_unit, period + ) + long_term_care_premiums = tax_unit_non_dep_sum( + "long_term_health_insurance_premiums", tax_unit, period + ) + deduction = max_(0, tax_unit("self_employed_health_insurance_ald", period)) + # The bounded exclusion the computed federal subtotal applies to these + # premiums: min(premiums, deduction). + used_on_line_2 = tax_unit("itemized_medical_expenses_excluded_premiums", period) + return ( + premiums + - used_on_line_2 + + max_(0, long_term_care_premiums - (deduction - used_on_line_2)) + ) diff --git a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py index 4c0fe2b3557..2330827d41a 100644 --- a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py +++ b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py @@ -15,9 +15,11 @@ class nd_renters_refund_income(Variable): documentation = ( "Renter refund income after unreimbursed medical costs actually paid. " - "The paid-cost base restores self-employed premiums only when the " - "federal formula excluded them. Caller-supplied subtotals remain " - "unchanged, including when components are supplied. Premium inputs use payer " + "The paid-cost base restores the self-employed premiums excluded from " + "the federal subtotal: by the federal formula, or as supplied with a " + "caller's subtotal in itemized_medical_expenses_applied_exclusion. A " + "caller-supplied subtotal without that amount is used unchanged, " + "including when components are supplied. Premium inputs use payer " "attribution: each person reports premiums that person paid, including " "family coverage regardless of whom it covers." ) @@ -30,7 +32,8 @@ def formula(tax_unit, period, parameters): # These premiums are disjoint from itemized_medical_expenses, # whose medical definition follows state income tax law. excluded_premiums = add(tax_unit, period, ["pre_tax_health_insurance_premiums"]) - # Restore only premiums excluded from a computed federal subtotal. + # Restore premiums excluded from the federal subtotal (computed, or + # supplied with a caller's subtotal). medical_expenses = add( tax_unit, period, diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py index 2441d3159ee..f7a54906c80 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_medical_expense_credit.py @@ -16,8 +16,10 @@ class nm_medical_expense_credit(Variable): documentation = ( "Senior medical credit based on gross unreimbursed costs paid, preserving " "premiums deducted through the federal self-employed health insurance ALD. " - "Only the exclusion applied to a computed federal medical amount is " - "restored; caller-supplied subtotals remain unchanged even with components. " + "The exclusion applied to the federal medical amount is restored: by " + "the federal formula, or as supplied with a caller's subtotal in " + "itemized_medical_expenses_applied_exclusion. A caller-supplied " + "subtotal without that amount is used unchanged even with components. " "This assumes qualifying costs were funded from income included in AGI. " "Premium inputs use payer attribution: each person reports premiums that " "person paid, including family coverage regardless of whom it covers. " @@ -28,7 +30,8 @@ def formula(tax_unit, period, parameters): pcredits = parameters(period).gov.states.nm.tax.income.credits p = pcredits.unreimbursed_medical_care_expense age = person("age", period) - # Restore only premiums excluded from a computed federal subtotal. + # Restore premiums excluded from the federal subtotal (computed, or + # supplied with a caller's subtotal). medical_expense = add( tax_unit, period, diff --git a/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py b/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py index b88e65879a3..238e35715d1 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py @@ -15,8 +15,10 @@ class nm_medical_expense_exemption(Variable): documentation = ( "Senior medical exemption based on gross unreimbursed costs paid, " "preserving premiums deducted through the federal self-employed health " - "insurance ALD. Only the exclusion applied to a computed federal medical " - "amount is restored; caller-supplied subtotals remain unchanged even " + "insurance ALD. The exclusion applied to the federal medical amount is " + "restored: by the federal formula, or as supplied with a caller's " + "subtotal in itemized_medical_expenses_applied_exclusion. A " + "caller-supplied subtotal without that amount is used unchanged even " "with components. " "This assumes qualifying costs were funded from income " "included in AGI. Premium inputs use payer attribution: each person " @@ -30,7 +32,8 @@ def formula(tax_unit, period, parameters): period ).gov.states.nm.tax.income.exemptions.unreimbursed_medical_care_expense age = person("age", period) - # Restore only premiums excluded from a computed federal subtotal. + # Restore premiums excluded from the federal subtotal (computed, or + # supplied with a caller's subtotal). medical_expense = add( tax_unit, period,