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/docs/contributed_reforms/owners_draw.md b/docs/contributed_reforms/owners_draw.md index 8225af17561..93007403b14 100644 --- a/docs/contributed_reforms/owners_draw.md +++ b/docs/contributed_reforms/owners_draw.md @@ -117,7 +117,11 @@ on. These are the draw profits whose SECA the deduction includes. Draw profits beyond the OASDI room with HI off stay out of line 5. With `half_seca_deduction: false` the draw's SECA is not deductible, so line 5 omits the draw and the limit follows the baseline formula. Disabled years -use the baseline formula. +use the baseline formula. The draw enters line 5 only: the plan business is +chosen as in the baseline, from the Schedule C business, the farm, the +partnership or the S corporation named by +`self_employed_health_insurance_plan_business`, or the one with the largest +line 13 when none is named. ## Benefit credit and active NIIT interaction diff --git a/policyengine_us/reforms/ssa/owners_draw.py b/policyengine_us/reforms/ssa/owners_draw.py index 624da0324c4..c80c676dec4 100644 --- a/policyengine_us/reforms/ssa/owners_draw.py +++ b/policyengine_us/reforms/ssa/owners_draw.py @@ -21,6 +21,7 @@ ) from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.self_employed_health_insurance_earned_income_limit import ( self_employed_health_insurance_earned_income_limit as baseline_sehi_limit, + form_7206_line_13, ) from policyengine_us.variables.gov.ssa.social_security.ss_aime import ( ss_aime as baseline_aime, @@ -247,32 +248,11 @@ def formula(person, period, parameters): person("owners_draw_niit_exclusion", period), 0, ) - plan_business_profit = max_( - 0, person("total_self_employment_income", period) - ) - all_business_profits = ( - plan_business_profit - + max_(0, person("farm_operations_income", period)) - + max_(0, person("partnership_self_employment_net_earnings", period)) - + draw_profits_in_line_7 - ) - plan_business_share = np.divide( - plan_business_profit, - all_business_profits, - out=np.zeros_like(all_business_profits), - where=all_business_profits > 0, - ) - self_employment_tax_deduction = ( - person("self_employment_tax_ald_person", period) * plan_business_share - ) - retirement_plan_deduction = person( - "self_employed_pension_contribution_ald_person", period - ) - return max_( - 0, - plan_business_profit - - self_employment_tax_deduction - - retirement_plan_deduction, + # The draw profits enter line 5 only: the plan business is still + # chosen among the baseline's Schedule C, farm, partnership and + # S corporation businesses. + return form_7206_line_13( + person, period, other_line_5_profits=draw_profits_in_line_7 ) class ss_aime(baseline_aime): 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 index 3efb0e0e568..d8fdab012ab 100644 --- 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 @@ -162,3 +162,147 @@ # The child takes it on their own return. self_employed_health_insurance_ald: 0 dependents_self_employed_health_insurance_ald: 2_788.05675 + +- name: Case 9, 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 10, 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 11, an S corporation shareholder deducts up to their wages from the corporation. + period: 2025 + absolute_error_margin: 0.01 + input: + is_self_employed: true + # The corporation reimburses the 12,000 of premiums and reports them in + # W-2 box 1, so box 1 wages are 22,000. Box 5 Medicare wages leave them + # out (26 U.S.C. 3121(a)(2)(B)): 10,000. + employment_income: 22_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 + # 22,000 - 10,000. + adjusted_gross_income: 12_000 + +- name: Case 12, 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 + +- name: Case 13, an S corporation shareholder's wages cover the medical premiums first, then eligible long-term care premiums. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 55 + is_self_employed: true + # The corporation pays the 8,500 of premiums and reports them in W-2 + # box 1 (Form 7206 instructions), so box 1 wages are 15,500. The case + # sets box 5 Medicare wages to 7,000. + employment_income: 15_500 + s_corp_shareholder_medicare_wages: 7_000 + health_insurance_premiums: 6_000 + long_term_health_insurance_premiums: 2_500 + output: + # Line 1 is 6,000; line 2 is min(2,500, 1,860) = 1,860 (ages 51 to 60); + # line 3 is 7,860; line 13 is line 11, 7,000. + self_employed_health_insurance_ald_person: 7_000 + # 7,000 - 6,000 of medical premiums. + self_employed_health_insurance_ald_long_term_care_part: 1_000 + # Schedule A keeps line 3 less the deduction: 7,860 - 7,000. + itemized_medical_expenses: 860 + # 15,500 - 7,000. + adjusted_gross_income: 8_500 + +- name: Case 14, a farmer whose plan is established under the farm deducts eligible long-term care premiums up to farm earned income. + period: 2024 + absolute_error_margin: 0.01 + input: + age: 75 + is_self_employed: true + farm_operations_income: 20_000 + health_insurance_premiums: 15_000 + long_term_health_insurance_premiums: 7_000 + output: + # Line 2 is min(7,000, 5,880) = 5,880 (over 70); line 3 is 20,880. + # Line 13: 20,000 - 20,000 x 0.9235 x 0.153 / 2 = 18,587.045. + self_employed_health_insurance_ald_person: 18_587.045 + # 18,587.045 - 15,000 of medical premiums. + self_employed_health_insurance_ald_long_term_care_part: 3_587.045 + # 20,880 - 18,587.045. + itemized_medical_expenses: 2_292.955 + # 20,000 - 1,412.955 - 18,587.045. + adjusted_gross_income: 0 + +- name: Case 15, a partner deducts eligible long-term care premiums up to their K-1 box 14 code A earnings less half of self-employment tax. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 62 + is_self_employed: true + partnership_income: 10_000 + partnership_self_employment_net_earnings: 10_000 + health_insurance_premiums: 8_000 + long_term_health_insurance_premiums: 6_000 + output: + # Line 2 is min(6,000, 4,810) = 4,810 (ages 61 to 70); line 3 is 12,810. + # Line 13: 10,000 - 10,000 x 0.9235 x 0.153 / 2 = 9,293.5225. + self_employed_health_insurance_ald_person: 9_293.5225 + # 9,293.5225 - 8,000 of medical premiums. + self_employed_health_insurance_ald_long_term_care_part: 1_293.5225 + # 12,810 - 9,293.5225. + itemized_medical_expenses: 3_516.4775 + # 10,000 - 706.4775 - 9,293.5225. + adjusted_gross_income: 0 + +- name: Case 16, with no plan business named, the larger farm's earned income covers line 3, long-term care premiums included. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + is_self_employed: true + self_employment_income: 2_000 + farm_operations_income: 30_000 + health_insurance_premiums: 20_000 + long_term_health_insurance_premiums: 1_500 + output: + # Line 15: 32,000 x 0.9235 x 0.153 / 2 = 2,260.728. Line 13 for the farm + # is 30,000 - 30/32 x 2,260.728 = 27,880.5675, and for the Schedule C + # business 2,000 - 2/32 x 2,260.728 = 1,858.7045. The model takes the farm. + self_employed_health_insurance_earned_income_limit: 27_880.5675 + # Line 2 is min(1,500, 930) = 930 (ages 41 to 50); line 3 is 20,930. + self_employed_health_insurance_ald_person: 20_930 + self_employed_health_insurance_ald_long_term_care_part: 930 + # 20,930 - 20,930. + itemized_medical_expenses: 0 + # 32,000 - 2,260.728 - 20,930. + adjusted_gross_income: 8_809.272 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 index ab97a8aa061..0594a350c08 100644 --- 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 @@ -1,6 +1,10 @@ # 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 @@ -85,12 +89,13 @@ self_employment_tax_ald_person: 0 self_employed_health_insurance_earned_income_limit: 400 -- name: Case 8, a Schedule C loss leaves no limit. +- 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 @@ -119,3 +124,139 @@ self_employment_tax_ald_person: 1_412.955 # Lines 4 and 5 are both 20,000, so line 6 is 1; 20,000 - 1,412.955. self_employed_health_insurance_earned_income_limit: 18_587.045 + +- name: Case 11, 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: + # Schedule 1 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 12, a farm is the only business. + period: 2024 + absolute_error_margin: 0.01 + input: + farm_operations_income: 20_000 + output: + # Schedule 1 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 13, 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: + # Schedule 1 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 14, 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: + # Schedule 1 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 15, 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: + # Schedule C line 13, as in Case 14: 5,000 - 5,000 / 45,000 x 3,179.14875. + self_employed_health_insurance_earned_income_limit: 4_646.76125 + +- name: Case 16, 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: + # Schedule 1 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 17, 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: + # Schedule 1 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 18, 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 19, 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: + # Schedule 1 line 15: 10,000 x 0.9235 x 0.153 / 2 = 706.4775. Schedule C + # line 13: 10,000 - 706.4775; line 11 does not apply. + self_employment_tax_ald_person: 706.4775 + self_employed_health_insurance_earned_income_limit: 9_293.5225 + +- name: Case 20, 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: + # Schedule 1 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 21, 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/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml b/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml index 8a53763100e..13df8822512 100644 --- a/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml +++ b/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml @@ -71,7 +71,7 @@ # baseline. self_employed_health_insurance_earned_income_limit: 37_174.09 -- name: Without a Schedule C business the limit stays zero +- name: S corporation profits without box 5 wages leave the limit at zero period: 2027 absolute_error_margin: 0.02 reforms: policyengine_us.reforms.ssa.owners_draw.owners_draw @@ -85,7 +85,7 @@ self_employed_health_insurance_premiums: 8_000 output: self_employment_tax_ald_person: 3_720 - # Line 4 is zero, so line 13 and the deduction are zero. + # Lines 4 and 11 are zero, so line 13 and the deduction are zero. self_employed_health_insurance_earned_income_limit: 0 self_employed_health_insurance_ald_person: 0 @@ -253,3 +253,65 @@ # Line 5: 72,485.11; line 6: 0.5518375; 40,000 - 2,585.86 = 37,414.14. # Main gives 35,314.09; the whole S-corp profit in line 5 would give 38,214.38. self_employed_health_insurance_earned_income_limit: 37_414.14 + +- name: A farm plan business with an owners' draw takes its share of line 15 + period: 2027 + absolute_error_margin: 0.02 + reforms: policyengine_us.reforms.ssa.owners_draw.owners_draw + input: + gov.contrib.ssa.owners_draw.current_law_cap: 200_000 + gov.contrib.ssa.owners_draw.in_effect: true + gov.contrib.ssa.owners_draw.phase_in_share: 1 + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + self_employment_income: 10_000 + farm_operations_income: 60_000 + s_corp_income: 64_970.22 + self_employed_health_insurance_plan_business: SCHEDULE_F + output: + # SE tax on Schedule C and the farm: 70,000 x 0.9235 x 15.3% = + # 9,890.685. Draw OASDI: 59,999.998 x 12.4% = 7,440.00. Half of + # 17,330.685 = 8,665.34. + self_employment_tax_ald_person: 8_665.34 + # Line 5: 10,000 + 60,000 + 64,970.22 = 134,970.22. The farm's line 6: + # 60,000 / 134,970.22 = 0.4445425. Line 13: 60,000 - 8,665.34 x + # 0.4445425 = 56,147.89. + self_employed_health_insurance_earned_income_limit: 56_147.89 + +- name: With an owners' draw and no named plan business the largest business sets the limit + period: 2027 + absolute_error_margin: 0.02 + reforms: policyengine_us.reforms.ssa.owners_draw.owners_draw + input: + gov.contrib.ssa.owners_draw.current_law_cap: 200_000 + gov.contrib.ssa.owners_draw.in_effect: true + gov.contrib.ssa.owners_draw.phase_in_share: 1 + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + self_employment_income: 10_000 + farm_operations_income: 60_000 + s_corp_income: 64_970.22 + output: + # As above, the farm's line 13 is 56,147.89. The Schedule C business's + # is 10,000 - 8,665.34 x 10,000 / 134,970.22 = 9,357.98, so the farm + # sets the limit. + self_employed_health_insurance_earned_income_limit: 56_147.89 + +- name: A named Schedule C plan business with an owners' draw beside a larger farm + period: 2027 + absolute_error_margin: 0.02 + reforms: policyengine_us.reforms.ssa.owners_draw.owners_draw + input: + gov.contrib.ssa.owners_draw.current_law_cap: 200_000 + gov.contrib.ssa.owners_draw.in_effect: true + gov.contrib.ssa.owners_draw.phase_in_share: 1 + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + self_employment_income: 10_000 + farm_operations_income: 60_000 + s_corp_income: 64_970.22 + self_employed_health_insurance_plan_business: SCHEDULE_C + output: + # 10,000 - 8,665.34 x 10,000 / 134,970.22 = 9,357.98. Leaving the + # draw out of line 5 would give 10,000 - 8,665.34 / 7 = 8,762.09. + self_employed_health_insurance_earned_income_limit: 9_357.98 diff --git a/policyengine_us/tests/reforms/test_owners_draw.py b/policyengine_us/tests/reforms/test_owners_draw.py index f8e483ede75..eb94cd18480 100644 --- a/policyengine_us/tests/reforms/test_owners_draw.py +++ b/policyengine_us/tests/reforms/test_owners_draw.py @@ -36,6 +36,9 @@ from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.self_employed_health_insurance_earned_income_limit import ( self_employed_health_insurance_earned_income_limit, ) +from policyengine_us.variables.household.expense.health.self_employed_health_insurance_plan_business import ( + SelfEmployedHealthInsurancePlanBusiness, +) from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.self_employment_tax_ald_person import ( self_employment_tax_ald_person, ) @@ -150,6 +153,7 @@ def _calculate(variables, parameters, inputs, outputs=None): "farm_operations_income": 0, "payroll_tax_gross_wages": 0, "self_employed_pension_contribution_ald_person": 0, + "s_corp_shareholder_medicare_wages": 0, **inputs, } @@ -166,6 +170,16 @@ def _calculate(variables, parameters, inputs, outputs=None): "total_self_employment_income", values["self_employment_income"] + values["sstb_self_employment_income"], ) + # The oracles below take the Schedule C business as the plan business. + values.setdefault( + "self_employed_health_insurance_plan_business", + SelfEmployedHealthInsurancePlanBusiness.encode( + np.full( + np.atleast_1d(values["total_self_employment_income"]).shape, + SelfEmployedHealthInsurancePlanBusiness.SCHEDULE_C, + ) + ), + ) def person(name, instant, options=None): if name not in values: 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 index 7be27beedb6..e63832c601a 100644 --- a/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py +++ b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py @@ -4,30 +4,53 @@ 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. Form 7206 (Pub. 535 Worksheet 6-A before -2023) 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 (lines 9 and 10); the deduction is the smaller of the -premiums and that amount (line 14). The model treats a person's Schedule C -income, SSTB and non-SSTB together, as the plan business. +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 line 3 and that amount (line 14), where line 3 +adds eligible long-term care premiums (line 2) to the medical premiums (line 1). +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 treats a person's +Schedule C income, SSTB and non-SSTB together, as one business, and 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 twice, the second -time with higher premiums. For every filer and year: +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: Schedule 1 line 15 equals half of the self-employment tax - computed here from Schedule SE, and the limit equals the Form 7206 lines 4 - to 13 computed here from that line 15 and the model's line 16. The - deduction is the smaller of the premiums and that limit. -2. Bounds: 0 <= deduction <= premiums, and deduction <= Schedule C profit less - the attributable part of line 15 (earned income after the self-employment - tax deduction), which never exceeds Schedule C profit. -3. Monotone in premiums: raising premiums by d raises the deduction by at - least 0 and at most d, and leaves the limit unchanged. -4. The deduction never makes adjusted gross income negative for a filer whose - only income is Schedule C profit: such a filer with negative AGI (from the - plan deduction) has no health insurance deduction. + computed here from Schedule SE, and each limit equals a closed-form Form + 7206 computation for its plan business from that line 15 and the model's + line 16. Without a named business, the limit is the largest of the four. + The deduction is the smaller of line 3 and that reference limit. Line 3 + adds the drawn long-term care premiums, up to the limit for ages 41 to 50 + (line 2), to the medical premiums (line 1). +2. Differential: the limit without a named business equals the largest of the + limits the model computes when each business is named. With property 1, + which ties each named limit to that business's own Form 7206, one plan + never pools the earned income of two businesses. +3. Bounds: 0 <= deduction <= line 3, and the limit is at least the Schedule + C limit (widening the base never lowers the deduction). +4. Monotone in premiums: raising the medical 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. +6. Line 2 and Schedule A: the long-term care part is the smaller of line 2 + and what the deduction leaves after line 1, and Schedule A keeps line 3 + less the deduction, so each premium in line 3 counts once. The model computes in float32, whose spacing passes a cent above $131,072, so comparisons allow a relative error of 1e-6 on top of the $0.01 tolerance. @@ -40,7 +63,8 @@ from policyengine_us import Simulation TOLERANCE = 0.01 -RELATIVE_TOLERANCE = 1e-6 +# Model outputs are float32, whose spacing exceeds a cent above $131,072. +RTOL = 1e-6 YEARS = [2022, 2024, 2026] PREMIUM_RAISE = 2_500 @@ -54,6 +78,14 @@ MEDICARE_RATE = 0.029 # Contribution and benefit bases, https://www.ssa.gov/oact/cola/cbb.html. WAGE_BASE = {2022: 147_000, 2024: 168_600, 2026: 184_500} +# 26 U.S.C. 213(d)(10): eligible long-term care premiums at ages 41 to 50 +# (Rev. Proc. 2021-45, 2023-34 and 2025-32), as test data. +LONG_TERM_CARE_LIMIT_AGE_45 = {2022: 850, 2024: 880, 2026: 930} +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): @@ -61,12 +93,11 @@ def every_year(value): def build_situation(filers): - """One single-filer household per drawn filer, then again with higher premiums.""" + """One single-filer household for each copy of each drawn filer.""" people, tax_units, households = {}, {}, {} - for raised in (False, True): + for prefix, plan_business, raise_ in COPIES: for i, filer in enumerate(filers): - name = f"{'r' if raised else 'b'}{i}" - premiums = filer["premiums"] + (PREMIUM_RAISE if raised else 0) + name = f"{prefix}{i}" people[name] = { "age": every_year(45), "is_self_employed": every_year(True), @@ -76,11 +107,21 @@ def build_situation(filers): "partnership_self_employment_net_earnings": every_year( filer["partnership"] ), - "employment_income": every_year(filer["wages"]), + # 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(premiums), + "health_insurance_premiums": every_year(filer["premiums"] + raise_), + "long_term_health_insurance_premiums": every_year( + filer["long_term_care"] + ), + "self_employed_health_insurance_plan_business": every_year( + plan_business + ), } tax_units[name] = {"members": [name]} households[name] = {"members": [name], "state_code": every_year("TX")} @@ -99,78 +140,139 @@ def schedule_se_deduction(year, schedule_c, farm, partnership, wages): return tax / 2 -def form_7206_limit(schedule_c, farm, partnership, line_15, line_16): - """Form 7206 lines 4 to 10 and 13, with lines 15 and 16 as given amounts.""" - line_4 = np.maximum(schedule_c, 0) - line_5 = line_4 + np.maximum(farm, 0) + np.maximum(partnership, 0) - line_6 = np.where(line_5 > 0, line_4 / np.where(line_5 > 0, line_5, 1), 0) - line_8 = line_4 - line_15 * line_6 - return np.maximum(line_8 - line_16, 0), line_8 +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 slack(value): - return TOLERANCE + RELATIVE_TOLERANCE * np.abs(value) +def at_least(actual, bound): + return np.all(actual >= bound - TOLERANCE - RTOL * np.abs(bound)) -def column(filers, key, n_copies=2): - return np.tile(np.array([float(f[key]) for f in filers]), n_copies) +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] + # The model's Schedule C is SSTB and non-SSTB income together. schedule_c = column(filers, "schedule_c") + column(filers, "sstb") farm = column(filers, "farm") partnership = column(filers, "partnership") - wages = column(filers, "wages") + s_corp = column(filers, "s_corp") + # S corporation wages are Social Security wages too. + wages = column(filers, "wages") + s_corp + long_term_care = column(filers, "long_term_care") for year in YEARS: premiums = sim.calculate("self_employed_health_insurance_premiums", year) + # Line 2: the drawn premiums up to the age 41-50 limit, for every copy. + line_2 = np.tile( + np.minimum(long_term_care, LONG_TERM_CARE_LIMIT_AGE_45[year]), + len(COPIES), + ) + line_3 = premiums + line_2 + long_term_care_part = sim.calculate( + "self_employed_health_insurance_ald_long_term_care_part", year + ) + schedule_a = sim.calculate("itemized_medical_expenses", 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) + agi = sim.calculate("adjusted_gross_income", year) - # 1. Reference. + # Line 15 from Schedule SE, and line 16, for every copy. expected_line_15 = schedule_se_deduction( year, schedule_c, farm, partnership, wages ) - np.testing.assert_allclose( - line_15, expected_line_15, rtol=RELATIVE_TOLERANCE, atol=TOLERANCE - ) - expected_limit, after_se_tax = form_7206_limit( - schedule_c, farm, partnership, expected_line_15, line_16 - ) - np.testing.assert_allclose( - limit, expected_limit, rtol=RELATIVE_TOLERANCE, atol=TOLERANCE + for prefix, _, _ in COPIES: + close(copy(line_15, prefix), expected_line_15) + # Line 16 does not depend on premiums or the named business. + for prefix, _, _ in COPIES[1:]: + close(copy(line_16, prefix), copy(line_16, "u")) + line_13, schedule_c_line_10 = form_7206_limits( + schedule_c, + farm, + partnership, + s_corp, + expected_line_15, + copy(line_16, "u"), ) - np.testing.assert_allclose( - deduction, - np.minimum(premiums, expected_limit), - rtol=RELATIVE_TOLERANCE, - atol=TOLERANCE, + largest = np.max(np.stack(list(line_13.values())), axis=0) + # The reference limit for every copy, in the simulation's order. + reference_limit = np.concatenate( + [ + largest if business == "UNSPECIFIED" else line_13[business] + for _, business, _ in COPIES + ] ) - # 2. Bounds. + # 1. Reference. + close(deduction, np.minimum(line_3, reference_limit)) + 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 np.all(deduction <= premiums + slack(premiums)) - positive_after_se_tax = np.maximum(after_se_tax, 0) - assert np.all(deduction <= positive_after_se_tax + slack(after_se_tax)) - assert np.all(after_se_tax <= np.maximum(schedule_c, 0) + slack(schedule_c)) - - # 3. Monotone and 1-Lipschitz in premiums; the limit ignores premiums. - base, raised = deduction[:n], deduction[n:] - assert np.all(raised >= base - slack(base)) - assert np.all(raised <= base + PREMIUM_RAISE + slack(raised)) - np.testing.assert_allclose( - limit[:n], limit[n:], rtol=RELATIVE_TOLERANCE, atol=TOLERANCE + assert at_least(line_3, 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 = ( + (column(filers, "schedule_c") >= 0) + & (column(filers, "sstb") >= 0) + & (farm >= 0) + & (partnership >= 0) ) - - # 4. The deduction never takes Schedule C-only AGI below zero. - schedule_c_only = (farm == 0) & (partnership == 0) & (wages == 0) - agi = sim.calculate("adjusted_gross_income", year) - negative_agi = schedule_c_only & (agi < -slack(agi)) - assert np.all(deduction[negative_agi] <= TOLERANCE) + floor = np.minimum(0, schedule_c_line_10) + for prefix, _, _ in COPIES: + assert at_least(copy(agi, prefix)[no_losses], floor[no_losses]) + + # 6. Line 2 takes what the deduction leaves after line 1, and + # Schedule A keeps line 3 less the deduction. + close( + long_term_care_part, + np.minimum(line_2, deduction - np.minimum(premiums, deduction)), + ) + close(schedule_a, line_3 - deduction) money = st.integers(0, 120_000) @@ -183,9 +285,11 @@ def check_properties(sim, filers, n): "sstb": st.one_of(st.just(0), st.integers(-20_000, 100_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), + "long_term_care": st.one_of(st.just(0), st.integers(1, 2_000)), } ) SETTINGS = dict( @@ -203,35 +307,62 @@ def test_deduction_follows_form_7206(filers): check_properties(Simulation(situation=situation), filers, n) -def filer(schedule_c, premiums, sstb=0, farm=0, partnership=0, wages=0, plan=0): +def filer( + schedule_c=0, + sstb=0, + farm=0, + partnership=0, + s_corp=0, + wages=0, + plan=0, + premiums=0, + long_term_care=0, +): return dict( schedule_c=schedule_c, sstb=sstb, farm=farm, partnership=partnership, + s_corp=s_corp, wages=wages, plan=plan, premiums=premiums, + long_term_care=long_term_care, ) def test_reviewer_example_and_boundaries(): - """Fixed filers: the $2,000 example, the $400 threshold, and a wage-base case.""" + """Fixed filers: the $2,000 example, the $400 threshold, the new bases and + long-term care premiums.""" filers = [ - filer(2_000, 6_000), - filer(434, 6_000), - filer(433, 6_000), - filer(10_000, 20_000, wages=250_000), - filer(30_000, 40_000, farm=10_000), - filer(20_000, 5_000, plan=20_000), - filer(30_000, 30_000, sstb=-10_000), + 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), + # SSTB and non-SSTB Schedule C income netted as one business. + filer(schedule_c=30_000, sstb=-10_000, premiums=30_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), + # Long-term care premiums under an S corporation plan: in 2022, line 3 + # is 6,000 + min(2,000, 850) = 6,850, above line 11. + filer(s_corp=6_500, premiums=6_000, long_term_care=2_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) - # 2,000 - 2,000 x 0.9235 x 0.153 / 2. - assert abs(deduction[0] - 1_858.7045) < TOLERANCE + 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, 7, 8): + 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 @@ -239,3 +370,9 @@ def test_reviewer_example_and_boundaries(): assert deduction[5] <= TOLERANCE # 20,000 of netted Schedule C profit: 20,000 - 20,000 x 0.9235 x 0.153 / 2. assert abs(deduction[6] - 18_587.045) < TOLERANCE + # Line 11: the S corporation wages, with no self-employment tax. + assert abs(deduction[9] - 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[10] - (50_000 - 50_000 * 0.9235 * 0.153 / 2)) < TOLERANCE + # Line 11, 6,500, exceeds the 6,000 of medical premiums. + assert abs(deduction[11] - 6_500) < TOLERANCE 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 index c52e4883c58..4fc27185a47 100644 --- 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 @@ -10,22 +10,40 @@ class self_employed_health_insurance_earned_income_limit(Variable): "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 model treats the person's Schedule C business as " - "that business, with SSTB and non-SSTB Schedule C income as a single " - "business, so a loss in one reduces line 4. Its net profit is reduced " - "by the part of the deductible part of self-employment tax " - "attributable to it (lines 5 to 7) and by the self-employed SEP, " - "SIMPLE and qualified plan deduction (line 9), which the model also " - "bases on Schedule C income. Schedule 1 line 15 is read per person: " - "each person's own deductible self-employment tax. S corporation " - "wages (line 11) are not modeled, and self-employment income inputs " - "are already net of any section 911 exclusion (line 12)." + "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. Each kind of business is one total per person: all " + "Schedule C income (SSTB and non-SSTB), all Schedule F profit, all " + "K-1 box 14 code A earnings and all more-than-2% shareholder wages, " + "so a loss in one business of a kind reduces line 4. Schedule 1 line " + "15 is read per person: each person's own deductible self-employment " + "tax. 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. Conservation Reserve Program payments, the optional methods " + "of figuring net earnings and the personal-services test of section " + "401(c)(2)(A)(i) are not modeled, so each business's net profit enters " + "lines 4 and 5 in full. 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", + # Subsection (l)(2)(A), and (l)(5) for S corporation shareholders. + "https://www.law.cornell.edu/uscode/text/26/162#l", "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. @@ -33,35 +51,74 @@ class self_employed_health_insurance_earned_income_limit(Variable): ) def formula(person, period, parameters): - # Line 4: net profit from the business under which the plan is - # established. - plan_business_profit = max_(0, person("total_self_employment_income", period)) - # Line 5: net profits of every business whose earnings are subject to - # self-employment tax, leaving out net losses. - other_business_profits = max_( - 0, person("farm_operations_income", period) - ) + max_(0, person("partnership_self_employment_net_earnings", period)) - all_business_profits = plan_business_profit + other_business_profits - # Line 6: the plan business's share of those profits. - plan_business_share = np.divide( - plan_business_profit, + return form_7206_line_13(person, period) + + +def form_7206_line_13(person, period, other_line_5_profits=0): + """Form 7206 line 13 for the plan business. + + other_line_5_profits adds the profits of businesses that cannot + establish the plan here but whose self-employment tax is in Schedule 1 + line 15, such as the owners' draw reform's draw profits. + """ + # 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 + other_line_5_profits + ) + # 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) + + def attributable_self_employment_tax_deduction(profit): + share = np.divide( + profit, all_business_profits, out=np.zeros_like(all_business_profits), where=all_business_profits > 0, ) - # Lines 7 and 8: the deductible part of self-employment tax - # (Schedule 1 line 15) attributable to the plan business. - self_employment_tax_deduction = ( - person("self_employment_tax_ald_person", period) * plan_business_share - ) - # Lines 9 and 10: the SEP, SIMPLE and qualified plan deduction - # (Schedule 1 line 16) for the plan business. - retirement_plan_deduction = person( - "self_employed_pension_contribution_ald_person", period - ) - return max_( - 0, - plan_business_profit - - self_employment_tax_deduction - - retirement_plan_deduction, - ) + return self_employment_tax_deduction * share + + # 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 + - attributable_self_employment_tax_deduction(schedule_c_profit) + - retirement_plan_deduction, + ) + farm = max_( + 0, farm_profit - attributable_self_employment_tax_deduction(farm_profit) + ) + partnership = max_( + 0, + partnership_profit + - attributable_self_employment_tax_deduction(partnership_profit), + ) + # 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/long_term_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py index 442a9bd5ab8..8899876e842 100644 --- a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py @@ -10,7 +10,10 @@ class long_term_health_insurance_premiums(Variable): documentation = ( "Annual long-term care insurance premiums paid for this person's own " "contract, excluding pretax payroll deductions and employer-paid " - "premiums. Unlike the other premium inputs, which sit on the person " + "premiums. Premiums a partnership or S corporation pays or reimburses " + "for a partner or more-than-2% shareholder, and reports as guaranteed " + "payments or W-2 box 1 wages, count as the person's own and belong " + "here. Unlike the other premium inputs, which sit on the person " "who paid them, record these on the insured person, whoever in the " "tax unit paid them: 26 U.S.C. 213(d)(10) limits the premiums that " "count as medical care by the insured person's age. The model treats " 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..ce86f4cde10 --- /dev/null +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_plan_business.py @@ -0,0 +1,63 @@ +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). Each kind of business is one " + "total per person, because the inputs do not separate one business of " + "a kind from another: the Schedule C business is all of the person's " + "Schedule C income, SSTB and non-SSTB together, the farm all of their " + "Schedule F profit, the partnership all of their K-1 box 14 code A " + "earnings, and the S corporation all of their wages as a " + "more-than-2% shareholder. 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; enter those premiums in " + "health_insurance_premiums. A partner also includes those guaranteed " + "payments in partnership_income, and in " + "partnership_self_employment_net_earnings when the partnership " + "reports them in K-1 box 14 code A; a shareholder includes the wages " + "in employment_income (see s_corp_shareholder_medicare_wages). " + "Long-term care premiums the business pays " + "or reimburses and reports the same way go in " + "long_term_health_insurance_premiums, on the insured person. The " + "deduction counts premiums only when " + "is_self_employed is true, including for a shareholder whose only " + "earned income is S corporation 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, or when the plan is established under a " + "business other than the one with the largest earned income." + ) + 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", + # Notice 2008-1: when a plan is established by the S corporation + "https://www.irs.gov/pub/irs-drop/n-08-01.pdf#page=3", + # Guaranteed payments to partners are net earnings from self-employment + "https://www.irs.gov/pub/irs-prior/i1065--2023.pdf#page=43", + ) 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..e8ded018863 --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/s_corp_shareholder_medicare_wages.py @@ -0,0 +1,43 @@ +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. Premiums the corporation pays or reimburses for the " + "shareholder and reports in box 1 as wages count as the " + "shareholder's own: include them in employment_income and in " + "health_insurance_premiums, but not here, because box 5 leaves out " + "premiums paid under a plan for employees generally (section " + "3121(a)(2)(B)). The model has no wage input that is in box 1 but " + "not box 5, so those premiums also count as payroll tax wages. " + "Long-term care premiums the corporation pays or reimburses and " + "reports in box 1 go in employment_income and in " + "long_term_health_insurance_premiums, on the insured person. " + "The model's payroll tax wages start from employment_income, so it " + "taxes these premiums as payroll wages too. The deduction " + "counts both premium inputs only when " + "is_self_employed is true, so set it for a shareholder even when " + "these wages are their only earned income." + ) + 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.law.cornell.edu/uscode/text/26/3121#a_2", + "https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1", + # Notice 2008-1: premiums are box 1 wages but not Social Security + # or Medicare wages when section 3121(a)(2)(B) applies + "https://www.irs.gov/pub/irs-drop/n-08-01.pdf#page=2", + ) + uprating = "calibration.gov.irs.soi.employment_income"