diff --git a/changelog.d/fix-sehi-earned-income-limit.fixed.md b/changelog.d/fix-sehi-earned-income-limit.fixed.md new file mode 100644 index 00000000000..23e7a80bdb3 --- /dev/null +++ b/changelog.d/fix-sehi-earned-income-limit.fixed.md @@ -0,0 +1 @@ +Limit the self-employed health insurance deduction to net profit less the deductible part of self-employment tax and self-employed retirement plan deductions, as 26 U.S.C. 162(l)(2)(A) and Form 7206 require. diff --git a/changelog.d/sehi-plan-business-base.fixed.md b/changelog.d/sehi-plan-business-base.fixed.md new file mode 100644 index 00000000000..bc168c32248 --- /dev/null +++ b/changelog.d/sehi-plan-business-base.fixed.md @@ -0,0 +1 @@ +Let the self-employed health insurance deduction's earned income limit come from a Schedule F farm, a partnership's K-1 box 14 code A earnings, or a more-than-2% S corporation shareholder's wages when the plan is established under that business (26 U.S.C. 162(l)(2)(A) and (l)(5), Form 7206); the business can be named, and otherwise the person's business with the largest earned income is used. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml new file mode 100644 index 00000000000..e1683e2ce58 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml @@ -0,0 +1,154 @@ +# Form 7206 line 14 (the Form 1040 instructions worksheet line 3 before 2023): +# the smaller of the premiums and the earned income limit. Expected values are +# hand-computed from the form. + +- name: Case 1, premiums above the limit are capped at net profit less half of self-employment tax. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + is_self_employed: true + self_employment_income: 2_000 + health_insurance_premiums: 6_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + # 2022 Form 1040 instructions worksheet: line 1 is 6,000; line 2 is + # 2,000 - 141.2955 (Schedule 1 line 15) = 1,858.7045. + self_employed_health_insurance_ald_person: [1_858.7045] + self_employed_health_insurance_ald: 1_858.7045 + # 2,000 - 141.2955 - 1,858.7045. + adjusted_gross_income: 0 + +- name: Case 2, premiums below the limit are deducted in full. + period: 2022 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + output: + # Line 2: 50,000 - 3,532.3875 = 46,467.6125. + self_employed_health_insurance_earned_income_limit: 46_467.6125 + self_employed_health_insurance_ald_person: 6_000 + +- name: Case 3, the plan deduction and half of self-employment tax both reduce the cap. + period: 2023 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 3_000 + health_insurance_premiums: 20_000 + output: + # Form 7206: line 3 is 20,000; line 13 is 20,000 - 1,412.955 - 3,000. + self_employed_health_insurance_ald_person: 15_587.045 + +- name: Case 4, a farm business takes its share of the self-employment tax deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: 30_000 + farm_operations_income: 10_000 + health_insurance_premiums: 40_000 + output: + # Form 7206 line 13: 30,000 - 0.75 x 2,825.91 = 27,880.5675. + self_employed_health_insurance_ald_person: 27_880.5675 + +- name: Case 5, each spouse's deduction is capped by their own earned income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + is_self_employed: true + self_employment_income: 3_000 + health_insurance_premiums: 8_000 + person2: + is_self_employed: true + self_employment_income: 40_000 + health_insurance_premiums: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # person1: 3,000 - 3,000 x 0.9235 x 0.153 / 2 = 3,000 - 211.94325. + # person2: premiums of 2,000 are under the limit. + self_employed_health_insurance_ald_person: [2_788.05675, 2_000] + self_employed_health_insurance_ald: 4_788.05675 + +- name: Case 6, no deduction with a Schedule C loss. + period: 2025 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: -5_000 + health_insurance_premiums: 3_000 + output: + self_employed_health_insurance_ald_person: 0 + +- name: Case 7, a farmer whose plan is established under the farm deducts up to farm earned income. + period: 2024 + absolute_error_margin: 0.01 + input: + is_self_employed: true + farm_operations_income: 20_000 + health_insurance_premiums: 25_000 + output: + # Form 7206: line 3 is 25,000; line 13 is 20,000 - 1,412.955. + self_employed_health_insurance_ald_person: 18_587.045 + # 20,000 - 1,412.955 - 18,587.045. + adjusted_gross_income: 0 + +- name: Case 8, a partner deducts up to their K-1 box 14 code A earnings less half of self-employment tax. + period: 2025 + absolute_error_margin: 0.01 + input: + is_self_employed: true + partnership_income: 30_000 + partnership_self_employment_net_earnings: 30_000 + health_insurance_premiums: 40_000 + output: + # Form 7206 line 13: 30,000 - 2,119.4325. + self_employed_health_insurance_ald_person: 27_880.5675 + adjusted_gross_income: 0 + +- name: Case 9, an S corporation shareholder deducts up to their wages from the corporation. + period: 2025 + absolute_error_margin: 0.01 + input: + is_self_employed: true + employment_income: 10_000 + s_corp_shareholder_medicare_wages: 10_000 + health_insurance_premiums: 12_000 + output: + # Form 7206: line 3 is 12,000; line 13 is line 11, 10,000. + self_employed_health_insurance_ald_person: 10_000 + adjusted_gross_income: 0 + +- name: Case 10, a plan named under the smaller farm is limited to the farm's earned income. + period: 2024 + absolute_error_margin: 0.01 + input: + is_self_employed: true + self_employment_income: 30_000 + farm_operations_income: 10_000 + self_employed_health_insurance_plan_business: SCHEDULE_F + health_insurance_premiums: 20_000 + output: + # Form 7206 line 13: 10,000 - 0.25 x 2,825.91. + self_employed_health_insurance_ald_person: 9_293.5225 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml new file mode 100644 index 00000000000..1a18a962f0e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml @@ -0,0 +1,233 @@ +# Form 7206 lines 4-13 (Pub. 535 Worksheet 6-A before 2023). Expected values +# are hand-computed from the form: the deductible part of self-employment tax +# is net profit x 0.9235 x 15.3% / 2 below the Social Security wage base. +# One Form 7206 per plan: the limit is the earned income of the business under +# which the plan is established (self_employed_health_insurance_plan_business), +# or of the person's business with the largest earned income when that input +# is not supplied. + +- name: Case 1, one business, the limit is net profit less half of self-employment tax. + period: 2022 + absolute_error_margin: 0.01 + input: + self_employment_income: 2_000 + output: + # Line 15: 2,000 x 0.9235 x 0.153 / 2 = 141.2955. + self_employment_tax_ald_person: 141.2955 + # Line 13: 2,000 - 141.2955. + self_employed_health_insurance_earned_income_limit: 1_858.7045 + +- name: Case 2, the SEP, SIMPLE and qualified plan deduction also reduces the limit. + period: 2023 + absolute_error_margin: 0.01 + input: + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 3_000 + output: + # Line 15: 20,000 x 0.9235 x 0.153 / 2 = 1,412.955. + self_employment_tax_ald_person: 1_412.955 + # Line 16 (within the $66,000 annual additions limit). + self_employed_pension_contribution_ald_person: 3_000 + # Lines 8 to 10: 20,000 - 1,412.955 - 3,000. + self_employed_health_insurance_earned_income_limit: 15_587.045 + +- name: Case 3, a farm business takes its share of the self-employment tax deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + farm_operations_income: 10_000 + output: + # Line 15: 40,000 x 0.9235 x 0.153 / 2 = 2,825.91. + self_employment_tax_ald_person: 2_825.91 + # Lines 5 to 8: 30,000 / 40,000 = 0.75; 30,000 - 0.75 x 2,825.91. + self_employed_health_insurance_earned_income_limit: 27_880.5675 + +- name: Case 4, a partnership's self-employment earnings take their share too. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + partnership_self_employment_net_earnings: 30_000 + output: + # Line 15: 60,000 x 0.9235 x 0.153 / 2 = 4,238.865. + self_employment_tax_ald_person: 4_238.865 + # Lines 5 to 8: 30,000 / 60,000 = 0.5; 30,000 - 0.5 x 4,238.865. + self_employed_health_insurance_earned_income_limit: 27_880.5675 + +- name: Case 5, a farm loss is left out of line 5, so the plan business takes the whole deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + farm_operations_income: -10_000 + output: + # Line 15: (30,000 - 10,000) x 0.9235 x 0.153 / 2 = 1,412.955. + self_employment_tax_ald_person: 1_412.955 + # Lines 5 to 8: line 5 is 30,000, so line 6 is 1; 30,000 - 1,412.955. + self_employed_health_insurance_earned_income_limit: 28_587.045 + +- name: Case 6, wages above the Social Security wage base leave only the Medicare part. + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 200_000 + self_employment_income: 10_000 + output: + # Wages exceed the $176,100 wage base, so line 15 is + # 10,000 x 0.9235 x 0.029 / 2 = 133.9075. + self_employment_tax_ald_person: 133.9075 + self_employed_health_insurance_earned_income_limit: 9_866.0925 + +- name: Case 7, net earnings under $400 owe no self-employment tax, so the limit is net profit. + period: 2025 + absolute_error_margin: 0.01 + input: + self_employment_income: 400 + output: + # 400 x 0.9235 = 369.40, under the $400 threshold of section 1402(b)(2). + self_employment_tax_ald_person: 0 + self_employed_health_insurance_earned_income_limit: 400 + +- name: Case 8, a plan established under a Schedule C business with a loss has no limit. + period: 2025 + absolute_error_margin: 0.01 + input: + self_employment_income: -5_000 + partnership_self_employment_net_earnings: 20_000 + self_employed_health_insurance_plan_business: SCHEDULE_C + output: + self_employed_health_insurance_earned_income_limit: 0 + +- name: Case 9, a Schedule C loss leaves the partnership as the plan business. + period: 2025 + absolute_error_margin: 0.01 + input: + self_employment_income: -5_000 + partnership_self_employment_net_earnings: 20_000 + output: + # Line 15: (20,000 - 5,000) x 0.9235 x 0.153 / 2 = 1,059.71625. + self_employment_tax_ald_person: 1_059.71625 + # Line 5 leaves out the loss, so line 6 is 1: 20,000 - 1,059.71625. + self_employed_health_insurance_earned_income_limit: 18_940.28375 + +- name: Case 10, a farm is the only business. + period: 2024 + absolute_error_margin: 0.01 + input: + farm_operations_income: 20_000 + output: + # Line 15: 20,000 x 0.9235 x 0.153 / 2 = 1,412.955. + self_employment_tax_ald_person: 1_412.955 + # Lines 4 to 8 for Schedule F: 20,000 - 1,412.955. + self_employed_health_insurance_earned_income_limit: 18_587.045 + +- name: Case 11, a partnership with K-1 box 14 code A earnings is the only business. + period: 2025 + absolute_error_margin: 0.01 + input: + partnership_self_employment_net_earnings: 30_000 + output: + # Line 15: 30,000 x 0.9235 x 0.153 / 2 = 2,119.4325. + self_employment_tax_ald_person: 2_119.4325 + self_employed_health_insurance_earned_income_limit: 27_880.5675 + +- name: Case 12, without a named business the larger farm is the plan business. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 5_000 + farm_operations_income: 40_000 + output: + # Line 15: 45,000 x 0.9235 x 0.153 / 2 = 3,179.14875. + self_employment_tax_ald_person: 3_179.14875 + # Farm: 40,000 - 40,000 / 45,000 x 3,179.14875 = 37,174.09. + # Schedule C: 5,000 - 5,000 / 45,000 x 3,179.14875 = 4,646.76125. + self_employed_health_insurance_earned_income_limit: 37_174.09 + +- name: Case 13, a named Schedule C business sets the limit even when the farm earns more. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 5_000 + farm_operations_income: 40_000 + self_employed_health_insurance_plan_business: SCHEDULE_C + output: + self_employed_health_insurance_earned_income_limit: 4_646.76125 + +- name: Case 14, a named farm business takes its own share of the self-employment tax deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + farm_operations_income: 10_000 + self_employed_health_insurance_plan_business: SCHEDULE_F + output: + # Line 15: 40,000 x 0.9235 x 0.153 / 2 = 2,825.91. + # Lines 5 to 8: 10,000 / 40,000 = 0.25; 10,000 - 0.25 x 2,825.91. + self_employed_health_insurance_earned_income_limit: 9_293.5225 + +- name: Case 15, the plan deduction is attributable to Schedule C, so the partnership earns more. + period: 2023 + absolute_error_margin: 0.01 + input: + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 15_000 + partnership_self_employment_net_earnings: 10_000 + output: + # Line 15: 30,000 x 0.9235 x 0.153 / 2 = 2,119.4325. + self_employment_tax_ald_person: 2_119.4325 + self_employed_pension_contribution_ald_person: 15_000 + # Schedule C: 20,000 - 2 / 3 x 2,119.4325 - 15,000 = 3,587.045. + # Partnership (line 9 is 0): 10,000 - 1 / 3 x 2,119.4325 = 9,293.5225. + self_employed_health_insurance_earned_income_limit: 9_293.5225 + +- name: Case 16, an S corporation shareholder's wages are the earned income. + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 50_000 + s_corp_shareholder_medicare_wages: 50_000 + self_employment_income: 10_000 + output: + # Line 11: 50,000. The Schedule C business would give + # 10,000 - 10,000 x 0.9235 x 0.153 / 2 = 9,293.5225. + self_employed_health_insurance_earned_income_limit: 50_000 + +- name: Case 17, a named Schedule C business ignores the S corporation wages. + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 50_000 + s_corp_shareholder_medicare_wages: 50_000 + self_employment_income: 10_000 + self_employed_health_insurance_plan_business: SCHEDULE_C + output: + self_employment_tax_ald_person: 706.4775 + self_employed_health_insurance_earned_income_limit: 9_293.5225 + +- name: Case 18, a named partnership sets the limit even when the farm earns more. + period: 2025 + absolute_error_margin: 0.01 + input: + farm_operations_income: 40_000 + partnership_self_employment_net_earnings: 10_000 + self_employed_health_insurance_plan_business: PARTNERSHIP + output: + # Line 15: 50,000 x 0.9235 x 0.153 / 2 = 3,532.3875. + self_employment_tax_ald_person: 3_532.3875 + # Lines 5 to 8: 10,000 / 50,000 = 0.2; 10,000 - 0.2 x 3,532.3875. + self_employed_health_insurance_earned_income_limit: 9_293.5225 + +- name: Case 19, a named S corporation sets the limit even when the Schedule C business earns more. + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 10_000 + s_corp_shareholder_medicare_wages: 10_000 + self_employment_income: 50_000 + self_employed_health_insurance_plan_business: S_CORPORATION + output: + # Line 11: 10,000. The Schedule C business would give + # 50,000 - 50,000 x 0.9235 x 0.153 / 2 = 46,467.6125. + self_employed_health_insurance_earned_income_limit: 10_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml index 526b7ca01e0..11b233376f0 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml @@ -8,11 +8,13 @@ - name: Self-employed health insurance ALD uses SSTB income period: 2024 + absolute_error_margin: 0.01 input: sstb_self_employment_income: 10_000 self_employed_health_insurance_premiums: 12_000 output: - self_employed_health_insurance_ald_person: 10_000 + # Form 7206 line 13: 10,000 - 10,000 x 0.9235 x 0.153 / 2. + self_employed_health_insurance_ald_person: 9_293.5225 - name: Self-employed pension ALD uses SSTB income period: 2024 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml index 36c1c46fd9c..77175d10352 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml @@ -33,14 +33,16 @@ - name: Premiums deducted as self-employed health insurance are left out period: 2025 + absolute_error_margin: 0.01 input: state_code: OH is_self_employed: true self_employment_income: 3_000 health_insurance_premiums: 5_000 output: - # 5,000 - min(3,000, 5,000). - oh_medical_care_insurance_premiums: 2_000 + # The federal deduction is min(5,000, 3,000 - 3,000 x 0.9235 x 0.153 / 2) + # = 2,788.05675 (Form 7206 line 14), so 5,000 - 2,788.05675 remain. + oh_medical_care_insurance_premiums: 2_211.94325 - name: The self-employed deduction cannot make premiums negative period: 2025 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml index d9eac668dff..b6853f57ac3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml @@ -179,6 +179,7 @@ # Worksheet line 1 note: "You must reduce the amount you enter on this line # by your federal self-employed health insurance deduction". period: 2025 + absolute_error_margin: 0.01 input: people: person1: @@ -194,7 +195,8 @@ members: [person1] state_code: OH output: - # min(3,000 of earnings, 5,000 of premiums). - self_employed_health_insurance_ald: 3_000 - # 5,000 - 3,000. - oh_uninsured_unreimbursed_medical_care_expenses: 2_000 + # Form 7206 line 14: min(5,000 of premiums, 3,000 of earnings less the + # 211.94325 deductible part of self-employment tax). + self_employed_health_insurance_ald: 2_788.05675 + # 5,000 - 2,788.05675. + oh_uninsured_unreimbursed_medical_care_expenses: 2_211.94325 diff --git a/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py new file mode 100644 index 00000000000..1349c09c55e --- /dev/null +++ b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py @@ -0,0 +1,274 @@ +"""Properties of the self-employed health insurance deduction. + +26 U.S.C. 162(l)(2)(A) limits the deduction to the taxpayer's earned income, +within the meaning of section 401(c), from the business under which the plan +is established. Section 401(c)(2)(A)(v) and (vi) determine that earned income +with regard to the section 404 plan deduction and the section 164(f) deduction +for half of self-employment tax, and section 162(l)(5) makes an S corporation +shareholder's wages from the corporation their earned income. Form 7206 (Pub. +535 Worksheet 6-A before 2023), one form per plan, computes it as the plan +business's net profit (line 4), less the part of Schedule 1 line 15 +attributable to that business by its share of all net profits subject to +self-employment tax (lines 5 to 8), less the Schedule 1 line 16 plan deduction +attributable to it (lines 9 and 10), or as the S corporation wages (line 11); +the deduction is the smaller of the premiums and that amount (line 14). The +plan business may be a Schedule C business, a Schedule F farm, a partnership +(K-1 box 14 code A) or an S corporation. When +self_employed_health_insurance_plan_business is not supplied, the model uses +the business with the largest earned income. The model bases the line 16 plan +deduction on Schedule C income, so only the Schedule C business subtracts it. + +Hypothesis draws batches of self-employed single filers and runs each batch as +one vectorized baseline simulation. Each drawn filer appears six times: with +no named plan business, again with higher premiums, and once naming each of +the four businesses. For every filer and year: + +1. Reference: each limit equals a closed-form Form 7206 computation for its + plan business that takes lines 15 and 16 from the model, and line 15 + equals half of the self-employment tax. Without a named business, the + limit is the largest of the four. +2. Differential: the limit without a named business equals the largest of the + limits the model computes when each business is named, so one plan never + pools the earned income of two businesses. +3. Bounds: 0 <= deduction <= premiums, and the limit is at least the Schedule + C limit (widening the base never lowers the deduction). +4. Monotone in premiums: raising premiums by d raises the deduction by at + least 0 and at most d, and leaves the limit unchanged. +5. For a filer with no business losses whose income is wages and + self-employment income, the deduction never takes adjusted gross income + below the Schedule C line 10 amount before its floor at zero. That amount + is negative only when the Schedule 1 line 16 plan deduction exceeds the + Schedule C business's net profit less its share of line 15; otherwise + adjusted gross income stays at or above zero. Intended: a plan under the + farm or partnership does not subtract the Schedule C plan deduction. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 +# Model outputs are float32, whose spacing exceeds a cent above $131,072. +RTOL = 1e-6 +YEARS = [2022, 2024, 2026] +PREMIUM_RAISE = 2_500 +BUSINESSES = ["SCHEDULE_C", "SCHEDULE_F", "PARTNERSHIP", "S_CORPORATION"] +# (name prefix, named plan business, premium raise) for each copy of a filer. +COPIES = [("u", "UNSPECIFIED", 0), ("r", "UNSPECIFIED", PREMIUM_RAISE)] + [ + (business.lower(), business, 0) for business in BUSINESSES +] + + +def every_year(value): + return {year: value for year in YEARS} + + +def build_situation(filers): + """One single-filer household for each copy of each drawn filer.""" + people, tax_units, households = {}, {}, {} + for prefix, plan_business, raise_ in COPIES: + for i, filer in enumerate(filers): + name = f"{prefix}{i}" + people[name] = { + "age": every_year(45), + "is_self_employed": every_year(True), + "self_employment_income": every_year(filer["schedule_c"]), + "farm_operations_income": every_year(filer["farm"]), + "partnership_self_employment_net_earnings": every_year( + filer["partnership"] + ), + # The partnership's earnings also enter gross income. + "partnership_income": every_year(filer["partnership"]), + # S corporation wages are part of employment income. + "employment_income": every_year(filer["wages"] + filer["s_corp"]), + "s_corp_shareholder_medicare_wages": every_year(filer["s_corp"]), + "self_employed_pension_contributions_desired": every_year( + filer["plan"] + ), + "health_insurance_premiums": every_year(filer["premiums"] + raise_), + "self_employed_health_insurance_plan_business": every_year( + plan_business + ), + } + tax_units[name] = {"members": [name]} + households[name] = {"members": [name], "state_code": every_year("TX")} + situation = {"people": people, "tax_units": tax_units, "households": households} + return situation, len(filers) + + +def form_7206_limits(schedule_c, farm, partnership, s_corp, line_15, line_16): + """Form 7206 line 13 for each plan business, with lines 15 and 16 given. + + Also returns the Schedule C line 10 amount before its floor at zero. + """ + line_4 = { + "SCHEDULE_C": np.maximum(schedule_c, 0), + "SCHEDULE_F": np.maximum(farm, 0), + "PARTNERSHIP": np.maximum(partnership, 0), + } + line_5 = sum(line_4.values()) + safe_line_5 = np.where(line_5 > 0, line_5, 1) + line_10 = {} + for business, profit in line_4.items(): + line_6 = np.where(line_5 > 0, profit / safe_line_5, 0) + line_8 = profit - line_15 * line_6 + line_9 = line_16 if business == "SCHEDULE_C" else 0 + line_10[business] = line_8 - line_9 + line_13 = {business: np.maximum(amount, 0) for business, amount in line_10.items()} + line_13["S_CORPORATION"] = np.maximum(s_corp, 0) + return line_13, line_10["SCHEDULE_C"] + + +def close(actual, expected): + np.testing.assert_allclose(actual, expected, rtol=RTOL, atol=TOLERANCE) + + +def at_least(actual, bound): + return np.all(actual >= bound - TOLERANCE - RTOL * np.abs(bound)) + + +def column(filers, key): + return np.array([float(f[key]) for f in filers]) + + +def check_properties(sim, filers, n): + def copy(array, prefix): + index = [p for p, _, _ in COPIES].index(prefix) + return array[index * n : (index + 1) * n] + + schedule_c = column(filers, "schedule_c") + farm = column(filers, "farm") + partnership = column(filers, "partnership") + s_corp = column(filers, "s_corp") + for year in YEARS: + premiums = sim.calculate("self_employed_health_insurance_premiums", year) + limit = sim.calculate( + "self_employed_health_insurance_earned_income_limit", year + ) + deduction = sim.calculate("self_employed_health_insurance_ald_person", year) + line_15 = sim.calculate("self_employment_tax_ald_person", year) + line_16 = sim.calculate("self_employed_pension_contribution_ald_person", year) + se_tax = sim.calculate("self_employment_tax", year) + agi = sim.calculate("adjusted_gross_income", year) + + # Lines 15 and 16 and the deduction for every copy. + close(line_15, se_tax / 2) + close(deduction, np.minimum(premiums, limit)) + # Lines 15 and 16 do not depend on premiums or the named business. + for prefix, _, _ in COPIES[1:]: + close(copy(line_15, prefix), copy(line_15, "u")) + close(copy(line_16, prefix), copy(line_16, "u")) + line_13, schedule_c_line_10 = form_7206_limits( + schedule_c, + farm, + partnership, + s_corp, + copy(line_15, "u"), + copy(line_16, "u"), + ) + largest = np.max(np.stack(list(line_13.values())), axis=0) + + # 1. Reference. + close(copy(limit, "u"), largest) + for business in BUSINESSES: + close(copy(limit, business.lower()), line_13[business]) + + # 2. Differential: the default equals the best named business. + named = np.stack([copy(limit, b.lower()) for b in BUSINESSES]) + close(copy(limit, "u"), named.max(axis=0)) + + # 3. Bounds. + assert np.all(deduction >= 0) + assert at_least(premiums, deduction) + assert at_least(copy(limit, "u"), line_13["SCHEDULE_C"]) + + # 4. Monotone and 1-Lipschitz in premiums; the limit ignores premiums. + base, raised = copy(deduction, "u"), copy(deduction, "r") + assert at_least(raised, base) + assert at_least(base + PREMIUM_RAISE, raised) + close(copy(limit, "u"), copy(limit, "r")) + + # 5. Adjusted gross income floor for filers with no business losses. + no_losses = (schedule_c >= 0) & (farm >= 0) & (partnership >= 0) + floor = np.minimum(0, schedule_c_line_10) + for prefix, _, _ in COPIES: + assert at_least(copy(agi, prefix)[no_losses], floor[no_losses]) + + +money = st.integers(0, 120_000) +filer_strategy = st.fixed_dictionaries( + { + "schedule_c": st.one_of( + st.sampled_from([-5_000, 0, 400, 434, 2_000, 160_000, 250_000]), + st.integers(-20_000, 300_000), + ), + "farm": st.one_of(st.just(0), st.integers(-30_000, 100_000)), + "partnership": st.one_of(st.just(0), st.integers(-30_000, 100_000)), + "s_corp": st.one_of(st.just(0), st.integers(0, 150_000)), + "wages": st.one_of(st.just(0), st.integers(0, 250_000)), + "plan": st.one_of(st.just(0), money), + "premiums": st.one_of(st.sampled_from([0, 1_000, 6_000]), money), + } +) +SETTINGS = dict( + max_examples=12, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(filer_strategy, min_size=4, max_size=20)) +def test_deduction_follows_form_7206(filers): + situation, n = build_situation(filers) + check_properties(Simulation(situation=situation), filers, n) + + +def filer(schedule_c=0, farm=0, partnership=0, s_corp=0, wages=0, plan=0, premiums=0): + return dict( + schedule_c=schedule_c, + farm=farm, + partnership=partnership, + s_corp=s_corp, + wages=wages, + plan=plan, + premiums=premiums, + ) + + +def test_reviewer_example_and_boundaries(): + """Fixed filers: the $2,000 example, the $400 threshold, and the new bases.""" + filers = [ + filer(schedule_c=2_000, premiums=6_000), + filer(schedule_c=434, premiums=6_000), + filer(schedule_c=433, premiums=6_000), + filer(schedule_c=10_000, wages=250_000, premiums=20_000), + filer(schedule_c=30_000, farm=10_000, premiums=40_000), + filer(schedule_c=20_000, plan=20_000, premiums=5_000), + # A farm or a partnership as the only business. + filer(farm=2_000, premiums=6_000), + filer(partnership=2_000, premiums=6_000), + # S corporation wages. + filer(s_corp=5_000, premiums=6_000), + # A plan deduction as large as the Schedule C profit, with the farm as + # the plan business (property 5). + filer(schedule_c=20_000, plan=20_000, farm=50_000, premiums=100_000), + ] + situation, n = build_situation(filers) + sim = Simulation(situation=situation) + check_properties(sim, filers, n) + deduction = sim.calculate("self_employed_health_insurance_ald_person", 2022)[:n] + # 2,000 - 2,000 x 0.9235 x 0.153 / 2, for a Schedule C business, a farm and + # a partnership alike. + for i in (0, 6, 7): + assert abs(deduction[i] - 1_858.7045) < TOLERANCE + # 434 x 0.9235 = 400.80 owes self-employment tax; 433 x 0.9235 = 399.88 does not. + assert deduction[1] < 434 - TOLERANCE + assert abs(deduction[2] - 433) < TOLERANCE + # Line 11: the S corporation wages, with no self-employment tax. + assert abs(deduction[8] - 5_000) < TOLERANCE + # Farm line 13: 50,000 - 50,000 / 70,000 x (70,000 x 0.9235 x 0.153 / 2). + assert abs(deduction[9] - (50_000 - 50_000 * 0.9235 * 0.153 / 2)) < TOLERANCE diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py index 8ecda97461a..939b3ba1e96 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py @@ -6,11 +6,23 @@ class self_employed_health_insurance_ald_person(Variable): entity = Person label = "Self-employed health insurance ALD for each person" unit = USD - documentation = "Personal above-the-line deduction for self-employed health insurance contributions." + documentation = ( + "Personal above-the-line deduction for self-employed health insurance " + "premiums: the smaller of the premiums and the earned income from the " + "business under which the plan is established (Form 7206 line 14). " + "The premiums exclude qualified long-term care insurance (line 2), " + "which the model does not include in self-employed premiums." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/162#l" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l", + "https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/i1040gi--2022.pdf#page=89", + ) def formula(person, period, parameters): - earnings = max_(0, person("total_self_employment_income", period)) premiums = person("self_employed_health_insurance_premiums", period) - return min_(earnings, premiums) + earned_income_limit = person( + "self_employed_health_insurance_earned_income_limit", period + ) + return min_(premiums, earned_income_limit) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py new file mode 100644 index 00000000000..8dd5db0052e --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py @@ -0,0 +1,95 @@ +from policyengine_us.model_api import * + + +class self_employed_health_insurance_earned_income_limit(Variable): + value_type = float + entity = Person + label = "Self-employed health insurance deduction earned income limit" + unit = USD + documentation = ( + "Earned income, within the meaning of 26 U.S.C. 401(c), from the " + "business under which the health plan is established; the " + "self-employed health insurance deduction cannot exceed it (Form " + "7206 line 13). The plan may be established under a Schedule C " + "business, a Schedule F farm, a partnership that reports net earnings " + "on Schedule K-1 box 14 code A, or an S corporation in which the " + "person is a more-than-2% shareholder, and earned income from other " + "businesses does not count. For each of the first three, earned income " + "is net profit less the part of the deductible part of " + "self-employment tax attributable to that business (lines 4 to 8) and " + "less the self-employed SEP, SIMPLE and qualified plan deduction " + "attributable to it (lines 9 and 10); the model bases that plan " + "deduction on Schedule C income, so only the Schedule C business " + "subtracts it. For an S corporation it is the shareholder's wages " + "from the corporation (line 11). self_employed_health_insurance_plan_" + "business names the business; when it is not supplied, the limit uses " + "the business with the largest earned income, the most that one plan " + "can deduct. Self-employment income inputs are already net of any " + "section 911 exclusion (line 12)." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_2_A", + "https://www.law.cornell.edu/uscode/text/26/162#l_5", + "https://www.law.cornell.edu/uscode/text/26/401#c_2_A", + "https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1", + # Where a plan may be established, and one Form 7206 per plan. + # PDF pages 1-2 + "https://www.irs.gov/pub/irs-prior/i7206--2023.pdf#page=1", + # The worksheet Form 7206 replaced, used before 2023. + "https://www.irs.gov/pub/irs-prior/p535--2022.pdf#page=23", + # The single-business worksheet in the Form 1040 instructions. + "https://www.irs.gov/pub/irs-prior/i1040gi--2022.pdf#page=89", + ) + + def formula(person, period, parameters): + # Line 4 for each business that can establish a plan: its net profit, + # or a partner's net earnings from self-employment. + schedule_c_profit = max_(0, person("total_self_employment_income", period)) + farm_profit = max_(0, person("farm_operations_income", period)) + partnership_profit = max_( + 0, person("partnership_self_employment_net_earnings", period) + ) + # Line 5: net profits of every business whose earnings are subject to + # self-employment tax, leaving out net losses. + all_business_profits = schedule_c_profit + farm_profit + partnership_profit + # Lines 6 and 7: each business takes the deductible part of + # self-employment tax (Schedule 1 line 15) in proportion to its share + # of line 5. + self_employment_tax_deduction = person("self_employment_tax_ald_person", period) + self_employment_tax_deduction_rate = np.divide( + self_employment_tax_deduction, + all_business_profits, + out=np.zeros_like(all_business_profits), + where=all_business_profits > 0, + ) + # Lines 9 and 10: the SEP, SIMPLE and qualified plan deduction + # (Schedule 1 line 16) is attributable to the Schedule C business. + retirement_plan_deduction = person( + "self_employed_pension_contribution_ald_person", period + ) + # Line 10 for each self-employment business. + schedule_c = max_( + 0, + schedule_c_profit * (1 - self_employment_tax_deduction_rate) + - retirement_plan_deduction, + ) + farm = max_(0, farm_profit * (1 - self_employment_tax_deduction_rate)) + partnership = max_( + 0, partnership_profit * (1 - self_employment_tax_deduction_rate) + ) + # Line 11: an S corporation shareholder's wages from the corporation. + s_corporation = max_(0, person("s_corp_shareholder_medicare_wages", period)) + plan_business = person("self_employed_health_insurance_plan_business", period) + businesses = plan_business.possible_values + return select( + [ + plan_business == businesses.SCHEDULE_C, + plan_business == businesses.SCHEDULE_F, + plan_business == businesses.PARTNERSHIP, + plan_business == businesses.S_CORPORATION, + ], + [schedule_c, farm, partnership, s_corporation], + # UNSPECIFIED: the business with the largest earned income. + default=max_(max_(schedule_c, farm), max_(partnership, s_corporation)), + ) diff --git a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_plan_business.py b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_plan_business.py new file mode 100644 index 00000000000..10dd54031ef --- /dev/null +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_plan_business.py @@ -0,0 +1,44 @@ +from policyengine_us.model_api import * + + +class SelfEmployedHealthInsurancePlanBusiness(Enum): + SCHEDULE_C = "Schedule C business" + SCHEDULE_F = "Schedule F farm" + PARTNERSHIP = "Partnership" + S_CORPORATION = "S corporation" + UNSPECIFIED = "Unspecified" + + +class self_employed_health_insurance_plan_business(Variable): + value_type = Enum + entity = Person + possible_values = SelfEmployedHealthInsurancePlanBusiness + default_value = SelfEmployedHealthInsurancePlanBusiness.UNSPECIFIED + definition_period = YEAR + label = ( + "Business under which the self-employed health insurance plan is established" + ) + documentation = ( + "The trade or business under which the person's health insurance plan " + "is established, or considered to be established: the Schedule C " + "business, the Schedule F farm, a partnership in which the person has " + "Schedule K-1 box 14 code A net earnings, or an S corporation in which " + "the person is a more-than-2% shareholder. The self-employed health " + "insurance deduction is limited to the earned income from that " + "business alone (one Form 7206 per plan). The Schedule C business is " + "all of the person's Schedule C income, SSTB and non-SSTB together, " + "because the inputs do not separate one Schedule C business from " + "another. A partner's or shareholder's own policy counts as " + "established under the partnership or S corporation only when that " + "business pays or reimburses the premiums and reports them as " + "guaranteed payments or wages. UNSPECIFIED (the default) means the " + "business was not supplied, and the limit uses the person's business " + "with the largest earned income; name the business when a partnership " + "or S corporation with the largest earned income does not pay or " + "reimburse the premiums." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_2_A", + # PDF pages 1-2 + "https://www.irs.gov/pub/irs-prior/i7206--2023.pdf#page=1", + ) diff --git a/policyengine_us/variables/household/income/person/self_employment/s_corp_shareholder_medicare_wages.py b/policyengine_us/variables/household/income/person/self_employment/s_corp_shareholder_medicare_wages.py new file mode 100644 index 00000000000..c9958cc6877 --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/s_corp_shareholder_medicare_wages.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class s_corp_shareholder_medicare_wages(Variable): + value_type = float + entity = Person + label = "S corporation shareholder Medicare wages" + unit = USD + documentation = ( + "Medicare wages (Form W-2 box 5) from an S corporation in which the " + "person is a more-than-2% shareholder (section 1372(b)). Section " + "162(l)(5) treats these wages as the shareholder's earned income for " + "the self-employed health insurance deduction (Form 7206 line 11). " + "Like the other income inputs, enter the amount left after any " + "section 911 foreign earned income exclusion attributable to these " + "wages (line 12). They are part of employment_income, not in addition " + "to it. As for other self-employed people, the deduction counts " + "health_insurance_premiums only when is_self_employed is true." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_5", + "https://www.law.cornell.edu/uscode/text/26/1372", + "https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1", + ) + uprating = "calibration.gov.irs.soi.employment_income"