From 6f9c3b949a03d27807986c9c13b3f864146e9ef4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:07:00 -0400 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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", ) From 4c3a6c79a77db98da821c434ddb7301bb38aa82e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:13:47 +0000 Subject: [PATCH 11/18] Allocate line 15 by each business's share, as main does The per-business rewrite computed profit x (1 - line 15 / line 5), which adds two float32 roundings. For one-business filers it moved the limit from main's value by up to $0.03 (92 of 400 Schedule C-only test filers). Each business now subtracts line 15 x its own share of line 5, the same operations main uses, so Schedule C-only limits match main bit for bit. The limit also documents that each kind of business is one total per person, and that CRP payments, the optional methods and the personal-services test are not modeled. The two 162 references are one source, so they become one entry. Co-Authored-By: Claude Opus 5.5 --- ...ed_health_insurance_earned_income_limit.py | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 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 b1a30ed7146..5f1c19768e3 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 @@ -20,21 +20,25 @@ class self_employed_health_insurance_earned_income_limit(Variable): "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. The Schedule C business is all of the person's " - "Schedule C income, SSTB and non-SSTB together, so a loss in one " - "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. " - "Self-employment income inputs are already net of any section 911 " - "exclusion (line 12)." + "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", - "https://www.law.cornell.edu/uscode/text/26/162#l_5", + # 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. @@ -61,12 +65,16 @@ def formula(person, period, parameters): # 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, - ) + + 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, + ) + 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( @@ -75,12 +83,17 @@ def formula(person, period, parameters): # Line 10 for each self-employment business. schedule_c = max_( 0, - schedule_c_profit * (1 - self_employment_tax_deduction_rate) + schedule_c_profit + - attributable_self_employment_tax_deduction(schedule_c_profit) - retirement_plan_deduction, ) - farm = max_(0, farm_profit * (1 - self_employment_tax_deduction_rate)) + farm = max_( + 0, farm_profit - attributable_self_employment_tax_deduction(farm_profit) + ) partnership = max_( - 0, partnership_profit * (1 - self_employment_tax_deduction_rate) + 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)) From 3019e6c741c61ee15560ff4a8b8bb9da366086fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:13:47 +0000 Subject: [PATCH 12/18] Say where entity-paid premiums go and that every business kind is pooled The plan business input now says that a farm, a partnership and an S corporation are each one total per person, like the Schedule C business, and that premiums a partnership or S corporation pays or reimburses go in health_insurance_premiums. The S corporation input says those premiums are box 1 wages but not box 5 wages (26 U.S.C. 3121(a)(2)(B)), so they belong in employment_income but not here, and that is_self_employed must be set even when the shareholder has no other earned income. Co-Authored-By: Claude Opus 5.5 --- ...employed_health_insurance_plan_business.py | 20 ++++++++++++------- .../s_corp_shareholder_medicare_wages.py | 13 ++++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) 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 10dd54031ef..76b435eb4c5 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,13 +25,19 @@ 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). 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 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. 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 " 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 c9958cc6877..e8d32d14e4e 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,13 +14,22 @@ 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. As for other self-employed people, the deduction counts " - "health_insurance_premiums only when is_self_employed is true." + "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. The " + "deduction counts health_insurance_premiums 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", ) uprating = "calibration.gov.irs.soi.employment_income" From 95b566c37a95a33db3251c819fd4be7a3c20621f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:13:47 +0000 Subject: [PATCH 13/18] Make the S corporation case a valid return and check the reference limit ALD Case 11 kept the reimbursed premiums out of box 1 wages, which the Form 7206 instructions require for the plan to count as established under the S corporation, so its AGI of 0 couldn't occur. Box 1 wages are now 22,000 and AGI 12,000. The property test compared the deduction with the model's own limit; it now uses the reference limit for each copy. Limit Cases 15 and 19 gain their hand computations. Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_ald.yaml | 8 ++++++-- ...ed_health_insurance_earned_income_limit.yaml | 3 +++ ...self_employed_health_insurance_properties.py | 17 +++++++++++++---- 3 files changed, 22 insertions(+), 6 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_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 bbbc5d7e289..8cc9658a4be 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 @@ -194,13 +194,17 @@ absolute_error_margin: 0.01 input: is_self_employed: true - employment_income: 10_000 + # 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 - adjusted_gross_income: 0 + # 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 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 f8de1fb7653..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 @@ -179,6 +179,7 @@ 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. @@ -229,6 +230,8 @@ 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 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 a230c10777e..9da77e8c0e8 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 @@ -29,9 +29,11 @@ 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 the premiums and that reference limit. 2. Differential: the limit without a named business equals the largest of the - limits the model computes when each business is named, so one plan never - pools the earned income of two businesses. + 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 <= 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 @@ -183,13 +185,12 @@ def copy(array, prefix): line_16 = sim.calculate("self_employed_pension_contribution_ald_person", year) agi = sim.calculate("adjusted_gross_income", year) - # Line 15 from Schedule SE, line 16, and the deduction for every copy. + # Line 15 from Schedule SE, and line 16, for every copy. expected_line_15 = schedule_se_deduction( year, schedule_c, farm, partnership, wages ) for prefix, _, _ in COPIES: close(copy(line_15, prefix), expected_line_15) - close(deduction, np.minimum(premiums, limit)) # 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")) @@ -202,8 +203,16 @@ def copy(array, prefix): copy(line_16, "u"), ) 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 + ] + ) # 1. Reference. + close(deduction, np.minimum(premiums, reference_limit)) close(copy(limit, "u"), largest) for business in BUSINESSES: close(copy(limit, business.lower()), line_13[business]) From da6240b48953b02d28713610dcbfb55b079a7258 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 20:29:24 +0000 Subject: [PATCH 14/18] Carry the plan business choice into the owners' draw reform's SEHI limit #10571 overrode self_employed_health_insurance_earned_income_limit with a copy of main's Schedule C-only formula plus the draw profits in line 5, so with the reform on, the farm, partnership and S corporation plan businesses this branch adds were ignored. The baseline formula is now a module-level Form 7206 line 13 helper that takes extra line 5 profits, and the reform calls it with the SECA-taxed draw profits. Three new reform cases cover a named farm, the unnamed default, and a named Schedule C beside a larger farm. The pure harness names the Schedule C business, which its oracle assumes, and supplies S corporation wages of zero. Co-Authored-By: Claude Opus 5.5 --- docs/contributed_reforms/owners_draw.md | 6 +- policyengine_us/reforms/ssa/owners_draw.py | 32 +---- ..._health_insurance_earned_income_limit.yaml | 62 +++++++++ .../tests/reforms/test_owners_draw.py | 14 ++ ...ed_health_insurance_earned_income_limit.py | 124 ++++++++++-------- 5 files changed, 155 insertions(+), 83 deletions(-) diff --git a/docs/contributed_reforms/owners_draw.md b/docs/contributed_reforms/owners_draw.md index 34dc304c919..381054a0cc5 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 8d676167dda..e341114e371 100644 --- a/policyengine_us/reforms/ssa/owners_draw.py +++ b/policyengine_us/reforms/ssa/owners_draw.py @@ -17,6 +17,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, @@ -305,32 +306,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/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..5f37c8f4582 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 @@ -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/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 5f1c19768e3..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 @@ -51,62 +51,74 @@ class self_employed_health_insurance_earned_income_limit(Variable): ) def formula(person, period, parameters): - # Line 4 for each business that can establish a plan: its net profit, - # or a partner's net earnings from self-employment. - schedule_c_profit = max_(0, person("total_self_employment_income", period)) - farm_profit = max_(0, person("farm_operations_income", period)) - partnership_profit = max_( - 0, person("partnership_self_employment_net_earnings", period) - ) - # Line 5: net profits of every business whose earnings are subject to - # self-employment tax, leaving out net losses. - all_business_profits = schedule_c_profit + farm_profit + partnership_profit - # Lines 6 and 7: each business takes the deductible part of - # self-employment tax (Schedule 1 line 15) in proportion to its share - # of line 5. - self_employment_tax_deduction = person("self_employment_tax_ald_person", period) + return form_7206_line_13(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, - ) - 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)), +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, ) + 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)), + ) From 735cb03ef8237cb00951eaad57a14116098a3cd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 12:57:47 +0000 Subject: [PATCH 15/18] Name the smaller-plan-business case in the plan business input's docs The default takes the business with the largest earned income, so it also overstates when the plan sits under a smaller business (issue #10528's Case 3). The documentation named only the partnership or S corporation case; it now tells users to name the business in both. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HCapAFW41PEgRBjqB --- .../health/self_employed_health_insurance_plan_business.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 76b435eb4c5..329892469e7 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 @@ -41,7 +41,8 @@ class self_employed_health_insurance_plan_business(Variable): "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." + "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", From 69a8b3b48b807d0525c0e78f7682a83f67069c95 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 14:27:33 -0500 Subject: [PATCH 16/18] Test long-term care premiums under every plan business Since #10265, the deduction is the smaller of Form 7206 line 3 (the medical premiums plus eligible long-term care premiums) and line 13. The property test still capped it at the medical premiums, and no test put long-term care premiums under a farm, partnership or S corporation plan. - ALD YAML Cases 13 to 16 add long-term care premiums under an S corporation, a farm, a partnership and the default business. Each checks the long-term care part and Schedule A as well as the deduction. - The property test draws long-term care premiums, checks the deduction against line 3 and adds property 6: the long-term care part and Schedule A. - The plan business, S corporation wage and long-term care premium inputs say that long-term care premiums a partnership or S corporation pays or reimburses go in long_term_health_insurance_premiums, on the insured person. - The reform case with S corporation profits but no box 5 wages now says so in its name. Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_ald.yaml | 88 +++++++++++++++++++ ..._health_insurance_earned_income_limit.yaml | 2 +- ...lf_employed_health_insurance_properties.py | 60 ++++++++++--- .../long_term_health_insurance_premiums.py | 5 +- ...employed_health_insurance_plan_business.py | 5 +- .../s_corp_shareholder_medicare_wages.py | 7 +- 6 files changed, 152 insertions(+), 15 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_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 8cc9658a4be..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 @@ -218,3 +218,91 @@ 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/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 5f37c8f4582..9dd44a6167f 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 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 9da77e8c0e8..d23dd7b7604 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 @@ -10,9 +10,10 @@ 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 +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 @@ -29,15 +30,17 @@ 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 the premiums and that reference limit. + 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 <= premiums, and the limit is at least the Schedule +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 premiums by d raises the deduction by at - least 0 and at most d, and leaves the limit unchanged. +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 @@ -45,6 +48,9 @@ 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 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. @@ -72,6 +78,9 @@ 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)] + [ @@ -107,6 +116,9 @@ def build_situation(filers): filer["plan"] ), "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 ), @@ -175,8 +187,19 @@ def copy(array, prefix): 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 ) @@ -212,7 +235,7 @@ def copy(array, prefix): ) # 1. Reference. - close(deduction, np.minimum(premiums, reference_limit)) + 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]) @@ -223,7 +246,7 @@ def copy(array, prefix): # 3. Bounds. assert np.all(deduction >= 0) - assert at_least(premiums, deduction) + 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. @@ -243,6 +266,14 @@ def copy(array, prefix): 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) filer_strategy = st.fixed_dictionaries( @@ -258,6 +289,7 @@ def copy(array, prefix): "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( @@ -284,6 +316,7 @@ def filer( wages=0, plan=0, premiums=0, + long_term_care=0, ): return dict( schedule_c=schedule_c, @@ -294,11 +327,13 @@ def filer( 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 the new bases.""" + """Fixed filers: the $2,000 example, the $400 threshold, the new bases and + long-term care premiums.""" filers = [ filer(schedule_c=2_000, premiums=6_000), filer(schedule_c=434, premiums=6_000), @@ -316,6 +351,9 @@ def test_reviewer_example_and_boundaries(): # 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: 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) @@ -336,3 +374,5 @@ def test_reviewer_example_and_boundaries(): 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/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 index 329892469e7..9a151a6057f 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 @@ -35,7 +35,10 @@ class self_employed_health_insurance_plan_business(Variable): "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. The deduction counts premiums only when " + "health_insurance_premiums. 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 " 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 e8d32d14e4e..8ac2b63c45a 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 @@ -20,8 +20,11 @@ class s_corp_shareholder_medicare_wages(Variable): "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. The " - "deduction counts health_insurance_premiums only when " + "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 " + "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." ) From 031c6bcfda648c14ff5f063c7bebb72f17b455ae Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 15:23:33 -0500 Subject: [PATCH 17/18] Say the model taxes reimbursed long-term care premiums as payroll wages The S corporation wage input's documentation now says the model's payroll tax wages start from employment_income, so they include the long-term care premiums it routes there. Three test comments name the year of the $850 limit, the premiums property 6 counts, and both zero lines in the owners' draw case. Co-Authored-By: Claude Opus 5.5 --- ..._self_employed_health_insurance_earned_income_limit.yaml | 2 +- .../unit/test_self_employed_health_insurance_properties.py | 6 +++--- .../self_employment/s_corp_shareholder_medicare_wages.py | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) 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 9dd44a6167f..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 @@ -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 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 d23dd7b7604..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 @@ -50,7 +50,7 @@ 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 counts once. + 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. @@ -351,8 +351,8 @@ def test_reviewer_example_and_boundaries(): # 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: line 3 is - # 6,000 + min(2,000, 850) = 6,850, above line 11. + # 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) 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 8ac2b63c45a..e1328700ce1 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 @@ -23,8 +23,10 @@ class s_corp_shareholder_medicare_wages(Variable): "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 " - "deduction counts both premium inputs only when " + "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." ) From eddc35a698de9650691a027011e59c62b885f0d4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 16:25:20 -0500 Subject: [PATCH 18/18] Tell partners where guaranteed payments go and cite Notice 2008-1 The plan business input now says a partner also enters premium guaranteed payments in partnership_income, and in partnership_self_employment_net_earnings when the partnership reports them in K-1 box 14 code A, matching the S corporation input's employment_income sentence. Both inputs cite Notice 2008-1 for when a plan is established by the S corporation and for the box 1 vs box 5 treatment, and the plan business input cites the Form 1065 instructions' line 4c note that guaranteed payments are net earnings from self-employment. Documentation and references only. Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_plan_business.py | 11 ++++++++++- .../s_corp_shareholder_medicare_wages.py | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) 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 9a151a6057f..ce86f4cde10 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 @@ -35,7 +35,12 @@ class self_employed_health_insurance_plan_business(Variable): "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. Long-term care premiums the business pays " + "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 " @@ -51,4 +56,8 @@ class self_employed_health_insurance_plan_business(Variable): "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 index e1328700ce1..e8ded018863 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 @@ -36,5 +36,8 @@ class s_corp_shareholder_medicare_wages(Variable): "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"