From 6f9c3b949a03d27807986c9c13b3f864146e9ef4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:07:00 -0400 Subject: [PATCH 01/10] Limit the self-employed health insurance deduction to 401(c) earned income 26 U.S.C. 162(l)(2)(A) caps the deduction at earned income within the meaning of section 401(c), which section 401(c)(2)(A)(v) and (vi) reduce by the section 404 plan deduction and the section 164(f) deduction for half of self-employment tax. Form 7206 (and Pub. 535 Worksheet 6-A before 2023) subtracts the share of Schedule 1 line 15 attributable to the plan business and the line 16 plan deduction from that business's net profit. The model used net profit alone. Co-Authored-By: Claude Opus 5.5 --- .../fix-sehi-earned-income-limit.fixed.md | 1 + ...lf_employed_health_insurance_ald_person.py | 20 ++++-- ...ed_health_insurance_earned_income_limit.py | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fix-sehi-earned-income-limit.fixed.md create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py 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/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..91cb5423d44 --- /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,64 @@ +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 model treats the person's Schedule C business as " + "that business. 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. " + "S corporation wages (line 11) are not modeled, and 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/401#c_2_A", + "https://www.irs.gov/pub/irs-prior/f7206--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: 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, + 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, + ) From a885a16f125c143e06aca12424630c34b24bf249 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:09:34 -0400 Subject: [PATCH 02/10] Test the self-employed health insurance earned income limit against Form 7206 Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_ald.yaml | 102 ++++++++++++++++++ ..._health_insurance_earned_income_limit.yaml | 95 ++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml create mode 100644 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 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..2937a6441e7 --- /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,102 @@ +# 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 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..4c9af5e3662 --- /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,95 @@ +# 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. + +- 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 Schedule C loss leaves no limit. + period: 2025 + absolute_error_margin: 0.01 + input: + self_employment_income: -5_000 + partnership_self_employment_net_earnings: 20_000 + output: + self_employed_health_insurance_earned_income_limit: 0 From 06f215b992e3a335e62fddc8d1df9c2f21bd93fc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:16:37 -0400 Subject: [PATCH 03/10] Add Form 7206 property tests for the self-employed health insurance deduction Co-Authored-By: Claude Opus 5.5 --- ...lf_employed_health_insurance_properties.py | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py 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..9d6cf8ffe5a --- /dev/null +++ b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py @@ -0,0 +1,199 @@ +"""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. 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 as the plan business. + +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: + +1. Reference: the limit equals a closed-form Form 7206 computation that takes + lines 15 and 16 from the model, and line 15 equals half of the + self-employment tax. +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. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 +YEARS = [2022, 2024, 2026] +PREMIUM_RAISE = 2_500 + + +def every_year(value): + return {year: value for year in YEARS} + + +def build_situation(filers): + """One single-filer household per drawn filer, then again with higher premiums.""" + people, tax_units, households = {}, {}, {} + for raised in (False, True): + for i, filer in enumerate(filers): + name = f"{'r' if raised else 'b'}{i}" + premiums = filer["premiums"] + (PREMIUM_RAISE if raised else 0) + 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"] + ), + "employment_income": every_year(filer["wages"]), + "self_employed_pension_contributions_desired": every_year( + filer["plan"] + ), + "health_insurance_premiums": every_year(premiums), + } + 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_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 column(filers, key, n_copies=2): + return np.tile(np.array([float(f[key]) for f in filers]), n_copies) + + +def check_properties(sim, filers, n): + schedule_c = column(filers, "schedule_c") + farm = column(filers, "farm") + partnership = column(filers, "partnership") + wages = column(filers, "wages") + 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) + + # 1. Reference. + np.testing.assert_allclose(line_15, se_tax / 2, atol=TOLERANCE) + expected_limit, after_se_tax = form_7206_limit( + schedule_c, farm, partnership, line_15, line_16 + ) + np.testing.assert_allclose(limit, expected_limit, atol=TOLERANCE) + np.testing.assert_allclose( + deduction, np.minimum(premiums, expected_limit), atol=TOLERANCE + ) + + # 2. Bounds. + assert np.all(deduction >= 0) + assert np.all(deduction <= premiums + TOLERANCE) + assert np.all(deduction <= np.maximum(after_se_tax, 0) + TOLERANCE) + assert np.all(after_se_tax <= np.maximum(schedule_c, 0) + TOLERANCE) + + # 3. Monotone and 1-Lipschitz in premiums; the limit ignores premiums. + base, raised = deduction[:n], deduction[n:] + assert np.all(raised >= base - TOLERANCE) + assert np.all(raised <= base + PREMIUM_RAISE + TOLERANCE) + np.testing.assert_allclose(limit[:n], limit[n:], atol=TOLERANCE) + + # 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 < -TOLERANCE) + assert np.all(deduction[negative_agi] <= TOLERANCE) + + +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)), + "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 test_reviewer_example_and_boundaries(): + """Fixed filers: the $2,000 example, the $400 threshold, and a wage-base case.""" + filers = [ + dict(schedule_c=2_000, farm=0, partnership=0, wages=0, plan=0, premiums=6_000), + dict(schedule_c=434, farm=0, partnership=0, wages=0, plan=0, premiums=6_000), + dict(schedule_c=433, farm=0, partnership=0, wages=0, plan=0, premiums=6_000), + dict( + schedule_c=10_000, + farm=0, + partnership=0, + wages=250_000, + plan=0, + premiums=20_000, + ), + dict( + schedule_c=30_000, + farm=10_000, + partnership=0, + wages=0, + plan=0, + premiums=40_000, + ), + dict( + schedule_c=20_000, + farm=0, + partnership=0, + wages=0, + plan=20_000, + premiums=5_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 + # 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 From 1bad2f16f64dc4cd23e46c01d2a3097e4dd0ef1b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:23:57 -0400 Subject: [PATCH 04/10] Update SSTB and Ohio expectations for the 401(c) earned income limit Co-Authored-By: Claude Opus 5.5 --- .../self_employment_income.yaml | 4 +++- .../oh_medical_care_insurance_premiums.yaml | 6 ++++-- ...h_uninsured_unreimbursed_medical_care_expenses.yaml | 10 ++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) 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 From 32aec888d8031574fed2c093e62d264ba8057a90 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:15:21 -0400 Subject: [PATCH 05/10] Widen the self-employed health insurance limit to farm, partnership and S corporation plans Form 7206 allows the plan to be established under a Schedule F farm, a partnership with K-1 box 14 code A earnings, or an S corporation in which the person is a more-than-2% shareholder (162(l)(5), line 11), one form per plan. Add self_employed_health_insurance_plan_business (default: the business with the largest earned income) and s_corp_shareholder_medicare_wages. Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_ald.yaml | 52 ++++++++ ..._health_insurance_earned_income_limit.yaml | 114 +++++++++++++++++- ...ed_health_insurance_earned_income_limit.py | 80 ++++++++---- ...employed_health_insurance_plan_business.py | 35 ++++++ .../s_corp_shareholder_medicare_wages.py | 25 ++++ 5 files changed, 280 insertions(+), 26 deletions(-) create mode 100644 policyengine_us/variables/household/expense/health/self_employed_health_insurance_plan_business.py create mode 100644 policyengine_us/variables/household/income/person/self_employment/s_corp_shareholder_medicare_wages.py 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 2937a6441e7..e1683e2ce58 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 @@ -100,3 +100,55 @@ 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 index 4c9af5e3662..8c2390748c0 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,11 +89,119 @@ 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 + +- 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 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 91cb5423d44..c58807e606f 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,19 +10,31 @@ 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. 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. " - "S corporation wages (line 11) are not modeled, and self-employment " + "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, as one plan established " + "under the person's most profitable business would. 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. + "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. @@ -30,35 +42,53 @@ 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 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. - 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, + 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 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. + # (Schedule 1 line 16) is attributable to the Schedule C business. retirement_plan_deduction = person( "self_employed_pension_contribution_ald_person", period ) - return max_( + # Line 10 for each self-employment business. + schedule_c = max_( 0, - plan_business_profit - - self_employment_tax_deduction + 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..1d665c415fa --- /dev/null +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_plan_business.py @@ -0,0 +1,35 @@ +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). UNSPECIFIED (the default) " + "means the business was not supplied, and the limit uses the person's " + "business with the largest earned income." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_2_A", + "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..7b1d2c67f3a --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/s_corp_shareholder_medicare_wages.py @@ -0,0 +1,25 @@ +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." + ) + 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" From 97e05a7935f3a0e6eee7f9c35fe31297b55f9f3e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:28:17 -0400 Subject: [PATCH 06/10] Extend the Form 7206 properties to farm, partnership and S corporation plans Co-Authored-By: Claude Opus 5.5 --- ...lf_employed_health_insurance_properties.py | 259 ++++++++++++------ 1 file changed, 168 insertions(+), 91 deletions(-) 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 9d6cf8ffe5a..b2e7398e6c9 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,29 +4,44 @@ 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 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 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 twice, the second -time with higher premiums. For every filer and year: - -1. Reference: the limit equals a closed-form Form 7206 computation that takes - lines 15 and 16 from the model, and line 15 equals half of the - self-employment tax. -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 +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. +3. Bounds: 0 <= deduction <= premiums; the limit is at least the Schedule C + limit (widening the base never lowers the deduction) and at most the sum + of the four businesses' limits (one plan cannot pool businesses). +4. 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. +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 plan deduction exceeds Schedule C earned income, + which the plan contribution limit (based on Schedule C profit, not + 401(c) earned income) allows; 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 @@ -36,8 +51,15 @@ 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): @@ -45,12 +67,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), @@ -59,11 +80,18 @@ 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_), + "self_employed_health_insurance_plan_business": every_year( + plan_business + ), } tax_units[name] = {"members": [name]} households[name] = {"members": [name], "state_code": every_year("TX")} @@ -71,24 +99,50 @@ def build_situation(filers): return situation, len(filers) -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 column(filers, key, n_copies=2): - return np.tile(np.array([float(f[key]) for f in filers]), n_copies) +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") - wages = column(filers, "wages") + s_corp = column(filers, "s_corp") for year in YEARS: premiums = sim.calculate("self_employed_health_insurance_premiums", year) limit = sim.calculate( @@ -98,34 +152,51 @@ def check_properties(sim, filers, n): 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) - # 1. Reference. - np.testing.assert_allclose(line_15, se_tax / 2, atol=TOLERANCE) - expected_limit, after_se_tax = form_7206_limit( - schedule_c, farm, partnership, line_15, line_16 - ) - np.testing.assert_allclose(limit, expected_limit, atol=TOLERANCE) - np.testing.assert_allclose( - deduction, np.minimum(premiums, expected_limit), atol=TOLERANCE + # 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) - # 2. Bounds. + # 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 np.all(deduction <= premiums + TOLERANCE) - assert np.all(deduction <= np.maximum(after_se_tax, 0) + TOLERANCE) - assert np.all(after_se_tax <= np.maximum(schedule_c, 0) + TOLERANCE) - - # 3. Monotone and 1-Lipschitz in premiums; the limit ignores premiums. - base, raised = deduction[:n], deduction[n:] - assert np.all(raised >= base - TOLERANCE) - assert np.all(raised <= base + PREMIUM_RAISE + TOLERANCE) - np.testing.assert_allclose(limit[:n], limit[n:], atol=TOLERANCE) - - # 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 < -TOLERANCE) - assert np.all(deduction[negative_agi] <= TOLERANCE) + assert at_least(premiums, deduction) + assert at_least(copy(limit, "u"), line_13["SCHEDULE_C"]) + assert at_least(named.sum(axis=0), copy(limit, "u")) + + # 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) @@ -137,6 +208,7 @@ def check_properties(sim, filers, n): ), "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), @@ -157,43 +229,48 @@ def test_deduction_follows_form_7206(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 a wage-base case.""" + """Fixed filers: the $2,000 example, the $400 threshold, and the new bases.""" filers = [ - dict(schedule_c=2_000, farm=0, partnership=0, wages=0, plan=0, premiums=6_000), - dict(schedule_c=434, farm=0, partnership=0, wages=0, plan=0, premiums=6_000), - dict(schedule_c=433, farm=0, partnership=0, wages=0, plan=0, premiums=6_000), - dict( - schedule_c=10_000, - farm=0, - partnership=0, - wages=250_000, - plan=0, - premiums=20_000, - ), - dict( - schedule_c=30_000, - farm=10_000, - partnership=0, - wages=0, - plan=0, - premiums=40_000, - ), - dict( - schedule_c=20_000, - farm=0, - partnership=0, - wages=0, - plan=20_000, - premiums=5_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), + # 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 at the Schedule C profit; the farm is the plan + # business, so adjusted gross income falls below zero (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) - # 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, 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 From 319445c2e7d86788e4dec8cbd4e2b7d50d27a219 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:35:37 -0400 Subject: [PATCH 07/10] Add changelog fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/sehi-plan-business-base.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/sehi-plan-business-base.fixed.md 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. From 572c882d14d1c70f96eb22bf5aaf12a49c511343 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:37:02 -0400 Subject: [PATCH 08/10] State the property 5 floor without depending on the plan contribution limit Co-Authored-By: Claude Opus 5.5 --- ...est_self_employed_health_insurance_properties.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 b2e7398e6c9..0e4a16eb642 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 @@ -37,11 +37,10 @@ 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 plan deduction exceeds Schedule C earned income, - which the plan contribution limit (based on Schedule C profit, not - 401(c) earned income) allows; 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. + 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 @@ -255,8 +254,8 @@ def test_reviewer_example_and_boundaries(): filer(partnership=2_000, premiums=6_000), # S corporation wages. filer(s_corp=5_000, premiums=6_000), - # A plan deduction at the Schedule C profit; the farm is the plan - # business, so adjusted gross income falls below zero (property 5). + # 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) From 47bba5e89375986057686ff7fc2f1952644436bc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:38:07 -0400 Subject: [PATCH 09/10] Clarify the default plan business and the S corporation premium gate Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_earned_income_limit.py | 6 +++--- .../self_employment/s_corp_shareholder_medicare_wages.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) 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 c58807e606f..9fda60d1bcb 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 @@ -23,9 +23,9 @@ class self_employed_health_insurance_earned_income_limit(Variable): "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, as one plan established " - "under the person's most profitable business would. Self-employment " - "income inputs are already net of any section 911 exclusion (line 12)." + "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 = ( 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 index 7b1d2c67f3a..c9958cc6877 100644 --- 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 @@ -14,7 +14,8 @@ class s_corp_shareholder_medicare_wages(Variable): "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." + "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 = ( From b03f96f65f592b63fc086346777a60e88ed6e35e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 14:00:14 -0400 Subject: [PATCH 10/10] Address review: name partnership and S corporation in YAML, document the default's limits - Limit YAML Cases 18 and 19 name PARTNERSHIP and S_CORPORATION. - The plan business input says when a partner's or shareholder's own policy counts, and that all Schedule C income is one business. - Cite PDF pages 1-2 of the Form 7206 instructions. - Drop the sum-of-businesses bound, which the differential property implies. Co-Authored-By: Claude Opus 5.5 --- ..._health_insurance_earned_income_limit.yaml | 26 +++++++++++++++++++ ...lf_employed_health_insurance_properties.py | 9 +++---- ...ed_health_insurance_earned_income_limit.py | 1 + ...employed_health_insurance_plan_business.py | 15 ++++++++--- 4 files changed, 43 insertions(+), 8 deletions(-) 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 8c2390748c0..1a18a962f0e 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 @@ -205,3 +205,29 @@ 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/unit/test_self_employed_health_insurance_properties.py b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py index 0e4a16eb642..1349c09c55e 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 @@ -28,10 +28,10 @@ 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. -3. Bounds: 0 <= deduction <= premiums; the limit is at least the Schedule C - limit (widening the base never lowers the deduction) and at most the sum - of the four businesses' limits (one plan cannot pool businesses). + 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 @@ -183,7 +183,6 @@ def copy(array, prefix): assert np.all(deduction >= 0) assert at_least(premiums, deduction) assert at_least(copy(limit, "u"), line_13["SCHEDULE_C"]) - assert at_least(named.sum(axis=0), copy(limit, "u")) # 4. Monotone and 1-Lipschitz in premiums; the limit ignores premiums. base, raised = copy(deduction, "u"), copy(deduction, "r") 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 9fda60d1bcb..8dd5db0052e 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 @@ -34,6 +34,7 @@ class self_employed_health_insurance_earned_income_limit(Variable): "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", 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 index 1d665c415fa..10dd54031ef 100644 --- 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 @@ -25,11 +25,20 @@ class self_employed_health_insurance_plan_business(Variable): "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). UNSPECIFIED (the default) " - "means the business was not supplied, and the limit uses the person's " - "business with the largest earned income." + "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", )