From 6f9c3b949a03d27807986c9c13b3f864146e9ef4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:07:00 -0400 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 7713afc88a644010404d517568ce761e6aee216a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 12:20:04 -0400 Subject: [PATCH 5/6] Address review: independent Schedule SE reference, SSTB draws, moved expectations - The property test now computes Schedule 1 line 15 from Schedule SE (0.9235, the $400 floor, the wage base, 12.4% and 2.9%) instead of reading it from the model, draws SSTB Schedule C income, and allows float32 rounding (rtol 1e-6) on top of the cent tolerance. - The limit's documentation states that SSTB and non-SSTB Schedule C income are one plan business and that Schedule 1 line 15 is read per person; YAML cases pin both the SSTB netting and a plan deduction that floors the limit at zero. - YAML comments name Schedule 1 lines 15 and 16, and the deduction file's header names Pub. 535 Worksheet 6-A as what Form 7206 replaced. - Tests merged since the branch was cut move with the corrected limit: two Schedule A cases from #10055 (1,000 -> 929.35225), the conservation check's exact equality (now a cent), and the Michigan comment and deduction assertion (3,000 -> 2,788.05675; Michigan resources unchanged). Co-Authored-By: Claude Opus 5.5 --- ...st_medical_expense_premium_conservation.py | 6 +- .../self_employed_health_insurance_ald.yaml | 7 +- ..._health_insurance_earned_income_limit.yaml | 40 +++++- .../itemizing/medical_expense_deduction.yaml | 19 ++- .../mi/tax/income/mi_household_resources.yaml | 19 +-- .../test_mi_household_resources_exclusions.py | 9 +- ...lf_employed_health_insurance_properties.py | 130 ++++++++++++------ ...ed_health_insurance_earned_income_limit.py | 15 +- 8 files changed, 160 insertions(+), 85 deletions(-) diff --git a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py index 5734523889c..d2c50fc2053 100644 --- a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py +++ b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py @@ -142,10 +142,12 @@ def _assert_conservation(members, simulation): # non-premium expenses. Check the bound separately from conservation. assert (schedule_a_premium_use >= 0).all() assert (schedule_a_premium_use >= dependent_premiums).all() - assert (schedule_a_premium_use + se_deduction <= paid).all() + # The Form 7206 earnings limit leaves cents in the derived deduction, so + # the sums carry float32 rounding: allow a cent. + assert (schedule_a_premium_use + se_deduction <= paid + 0.01).all() # All premiums here are otherwise eligible: unused premiums remain in # Schedule A's base, even when earnings cap the SE deduction. - np.testing.assert_array_equal(schedule_a_premium_use + se_deduction, paid) + np.testing.assert_allclose(schedule_a_premium_use + se_deduction, paid, atol=0.01) EXAMPLE_UNITS = [ 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..061dc600a9c 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 @@ -1,6 +1,7 @@ -# 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. +# Form 7206 line 14 (Pub. 535 Worksheet 6-A line 14 before 2023; line 3 of the +# Form 1040 instructions worksheet for filers with one source of +# self-employment income): 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 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..ab97a8aa061 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 @@ -8,7 +8,7 @@ input: self_employment_income: 2_000 output: - # Line 15: 2,000 x 0.9235 x 0.153 / 2 = 141.2955. + # Schedule 1 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 @@ -20,9 +20,9 @@ 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. + # Schedule 1 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). + # Schedule 1 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 @@ -34,7 +34,7 @@ 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. + # Schedule 1 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 @@ -46,7 +46,7 @@ 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. + # Schedule 1 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 @@ -58,7 +58,7 @@ 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. + # Schedule 1 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 @@ -70,7 +70,7 @@ employment_income: 200_000 self_employment_income: 10_000 output: - # Wages exceed the $176,100 wage base, so line 15 is + # Wages exceed the $176,100 wage base, so Schedule 1 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 @@ -93,3 +93,29 @@ partnership_self_employment_net_earnings: 20_000 output: self_employed_health_insurance_earned_income_limit: 0 + +- name: Case 9, a plan deduction as large as net profit leaves no limit. + period: 2023 + absolute_error_margin: 0.01 + input: + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 20_000 + output: + # Schedule 1 line 15: 20,000 x 0.9235 x 0.153 / 2 = 1,412.955. + self_employment_tax_ald_person: 1_412.955 + # Schedule 1 line 16: the model caps the plan deduction at net profit. + self_employed_pension_contribution_ald_person: 20_000 + # Lines 8 to 13: max(0, 20,000 - 1,412.955 - 20,000). + self_employed_health_insurance_earned_income_limit: 0 + +- name: Case 10, SSTB and non-SSTB Schedule C income are one plan business, so an SSTB loss reduces line 4. + period: 2025 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + sstb_self_employment_income: -10_000 + output: + # Schedule 1 line 15: 20,000 x 0.9235 x 0.153 / 2 = 1,412.955. + self_employment_tax_ald_person: 1_412.955 + # Lines 4 and 5 are both 20,000, so line 6 is 1; 20,000 - 1,412.955. + self_employed_health_insurance_earned_income_limit: 18_587.045 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml index 1228ba0acf8..9b50c3d5da0 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml @@ -22,6 +22,7 @@ - name: An earnings-limited derived SE deduction leaves unused premiums on Schedule A. period: 2025 + absolute_error_margin: 0.01 input: is_self_employed: true health_insurance_premiums: 6_000 @@ -29,9 +30,12 @@ other_medical_expenses: 500 adjusted_gross_income: 30_000 output: - self_employed_health_insurance_ald: 1_000 - itemized_medical_expenses: 5_500 - medical_expense_deduction: 3_250 + # Form 7206 line 13: 1,000 - 1,000 x 0.9235 x 0.153 / 2 = 929.35225. + self_employed_health_insurance_ald: 929.35225 + # 6,000 - 929.35225 + 500. + itemized_medical_expenses: 5_570.64775 + # 5,570.64775 - 7.5% of 30,000. + medical_expense_deduction: 3_320.64775 - name: Fully deducted premiums leave only non-premium medical expenses. period: 2025 @@ -87,6 +91,7 @@ - name: Each filing unit excludes its own SE deduction independently of pretax premiums. period: 2025 + absolute_error_margin: 0.01 input: people: person1: @@ -116,9 +121,11 @@ members: [person3] adjusted_gross_income: 30_000 output: - self_employed_health_insurance_ald: [1_000, 1_000, 0] - itemized_medical_expenses: [5_500, 5_500, 6_500] - medical_expense_deduction: [3_250, 3_250, 4_250] + # Form 7206 line 13: 1,000 - 1,000 x 0.9235 x 0.153 / 2 = 929.35225. + self_employed_health_insurance_ald: [929.35225, 929.35225, 0] + # 6,000 - 929.35225 + 500; the pretax premiums are not on Schedule A. + itemized_medical_expenses: [5_570.64775, 5_570.64775, 6_500] + medical_expense_deduction: [3_320.64775, 3_320.64775, 4_250] - name: A filer SE deduction cannot consume premiums attributed to a dependent. period: 2025 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml index 03c3a767355..63c026b3e91 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml @@ -1125,18 +1125,13 @@ state_code: MI output: # Line 14: 40,000. Line 16: 3,000. - # PolicyEngine's pre-existing federal simplification limits the health - # insurance deduction to net self-employment income before deductible - # self-employment tax: 3,000 here. - # Line 30 in PolicyEngine: self-employment tax deduction, half of 3,000 x - # 0.9235 x 15.3% = 211.94, plus health insurance 3,000 = 3,211.94. - # Line 31: premiums of 5,000, less the 3,000 deducted on line 30 = 2,000. - # Line 33: 43,000 - 3,211.94 - 2,000 = 37,788.06. The premiums come off - # once in all: 3,000 on line 30 and 2,000 on line 31. - # 2025 Form 7206 lines 7-8 instead limit the health insurance deduction - # to 3,000 - 211.94 = 2,788.06. Then line 30 is 3,000 and line 31 is - # 5,000 - 2,788.06 = 2,211.94: 43,000 - 3,000 - 2,211.94 = 37,788.06. - # The Michigan total is unchanged because every premium comes off once. + # Line 30: the self-employment tax deduction, half of 3,000 x 0.9235 x + # 15.3% = 211.94, plus the health insurance deduction, which 2025 Form + # 7206 lines 7-8 limit to 3,000 - 211.94 = 2,788.06: 3,000 in all. + # Line 31: premiums of 5,000, less the 2,788.06 deducted on line 30 = + # 2,211.94. + # Line 33: 43,000 - 3,000 - 2,211.94 = 37,788.06. The premiums come off + # once in all: 2,788.06 on line 30 and 2,211.94 on line 31. mi_household_resources: 37_788.06 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/test_mi_household_resources_exclusions.py b/policyengine_us/tests/test_mi_household_resources_exclusions.py index 5d38793260d..5c9796aa37a 100644 --- a/policyengine_us/tests/test_mi_household_resources_exclusions.py +++ b/policyengine_us/tests/test_mi_household_resources_exclusions.py @@ -270,11 +270,10 @@ def test_premiums_move_to_line_31_without_the_line_30_deduction(michigan_only): ) # The deduction is still computed under either list reform. deduction = baseline.calculate("self_employed_health_insurance_ald", YEAR) - # These are PolicyEngine's federal amounts: its pre-existing earnings - # limit is before deductible self-employment tax. Form 7206 gives - # 3,000 - 211.94 = 2,788.06 in the second case. Michigan resources are - # unchanged: line 30 is 3,000 and line 31 is 2,211.94, totaling 5,211.94. - assert deduction == pytest.approx([5_000, 3_000], abs=TOLERANCE) + # Form 7206 lines 7-8 limit the second case to 3,000 - 211.94 = 2,788.06. + # Michigan line 30 is then 3,000 and line 31 is 2,211.94, totaling + # 5,211.94. + assert deduction == pytest.approx([5_000, 2_788.05675], abs=TOLERANCE) agi_change = reformed.calculate("adjusted_gross_income", YEAR) - baseline.calculate( "adjusted_gross_income", YEAR ) 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..7be27beedb6 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,15 +10,16 @@ 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. +income, SSTB and non-SSTB together, 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. +1. Reference: Schedule 1 line 15 equals half of the self-employment tax + computed here from Schedule SE, and the limit equals the Form 7206 lines 4 + to 13 computed here from that line 15 and the model's line 16. The + deduction is the smaller of the premiums and that limit. 2. Bounds: 0 <= deduction <= premiums, and deduction <= Schedule C profit less the attributable part of line 15 (earned income after the self-employment tax deduction), which never exceeds Schedule C profit. @@ -27,6 +28,9 @@ 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. + +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. """ import numpy as np @@ -36,9 +40,21 @@ from policyengine_us import Simulation TOLERANCE = 0.01 +RELATIVE_TOLERANCE = 1e-6 YEARS = [2022, 2024, 2026] PREMIUM_RAISE = 2_500 +# Schedule SE, as test data rather than model parameters. +# 26 U.S.C. 1402(a)(12): net earnings are 92.35% of net profit. +NET_EARNINGS_SHARE = 0.9235 +# 26 U.S.C. 1402(b)(2): no net earnings below $400. +NET_EARNINGS_EXEMPTION = 400 +# 26 U.S.C. 1401(a) and (b). +SOCIAL_SECURITY_RATE = 0.124 +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} + def every_year(value): return {year: value for year in YEARS} @@ -55,6 +71,7 @@ def build_situation(filers): "age": every_year(45), "is_self_employed": every_year(True), "self_employment_income": every_year(filer["schedule_c"]), + "sstb_self_employment_income": every_year(filer["sstb"]), "farm_operations_income": every_year(filer["farm"]), "partnership_self_employment_net_earnings": every_year( filer["partnership"] @@ -71,6 +88,17 @@ def build_situation(filers): return situation, len(filers) +def schedule_se_deduction(year, schedule_c, farm, partnership, wages): + """Schedule 1 line 15: half of Schedule SE self-employment tax.""" + net_earnings = (schedule_c + farm + partnership) * NET_EARNINGS_SHARE + net_earnings = np.where(net_earnings < NET_EARNINGS_EXEMPTION, 0, net_earnings) + social_security_room = np.maximum(WAGE_BASE[year] - wages, 0) + tax = SOCIAL_SECURITY_RATE * np.minimum( + net_earnings, social_security_room + ) + MEDICARE_RATE * np.maximum(net_earnings, 0) + return tax / 2 + + def form_7206_limit(schedule_c, farm, partnership, line_15, line_16): """Form 7206 lines 4 to 10 and 13, with lines 15 and 16 as given amounts.""" line_4 = np.maximum(schedule_c, 0) @@ -80,12 +108,17 @@ def form_7206_limit(schedule_c, farm, partnership, line_15, line_16): return np.maximum(line_8 - line_16, 0), line_8 +def slack(value): + return TOLERANCE + RELATIVE_TOLERANCE * np.abs(value) + + 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") + # The model's Schedule C is SSTB and non-SSTB income together. + schedule_c = column(filers, "schedule_c") + column(filers, "sstb") farm = column(filers, "farm") partnership = column(filers, "partnership") wages = column(filers, "wages") @@ -97,34 +130,46 @@ def check_properties(sim, filers, n): 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_line_15 = schedule_se_deduction( + year, schedule_c, farm, partnership, wages + ) + np.testing.assert_allclose( + line_15, expected_line_15, rtol=RELATIVE_TOLERANCE, atol=TOLERANCE + ) expected_limit, after_se_tax = form_7206_limit( - schedule_c, farm, partnership, line_15, line_16 + schedule_c, farm, partnership, expected_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 + limit, expected_limit, rtol=RELATIVE_TOLERANCE, atol=TOLERANCE + ) + np.testing.assert_allclose( + deduction, + np.minimum(premiums, expected_limit), + rtol=RELATIVE_TOLERANCE, + 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) + assert np.all(deduction <= premiums + slack(premiums)) + positive_after_se_tax = np.maximum(after_se_tax, 0) + assert np.all(deduction <= positive_after_se_tax + slack(after_se_tax)) + assert np.all(after_se_tax <= np.maximum(schedule_c, 0) + slack(schedule_c)) # 3. Monotone and 1-Lipschitz in premiums; the limit ignores premiums. base, raised = deduction[:n], deduction[n:] - assert np.all(raised >= base - TOLERANCE) - assert np.all(raised <= base + PREMIUM_RAISE + TOLERANCE) - np.testing.assert_allclose(limit[:n], limit[n:], atol=TOLERANCE) + assert np.all(raised >= base - slack(base)) + assert np.all(raised <= base + PREMIUM_RAISE + slack(raised)) + np.testing.assert_allclose( + limit[:n], limit[n:], rtol=RELATIVE_TOLERANCE, atol=TOLERANCE + ) # 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) + negative_agi = schedule_c_only & (agi < -slack(agi)) assert np.all(deduction[negative_agi] <= TOLERANCE) @@ -135,6 +180,7 @@ def check_properties(sim, filers, n): st.sampled_from([-5_000, 0, 400, 434, 2_000, 160_000, 250_000]), st.integers(-20_000, 300_000), ), + "sstb": st.one_of(st.just(0), st.integers(-20_000, 100_000)), "farm": st.one_of(st.just(0), st.integers(-30_000, 100_000)), "partnership": st.one_of(st.just(0), st.integers(-30_000, 100_000)), "wages": st.one_of(st.just(0), st.integers(0, 250_000)), @@ -157,36 +203,28 @@ def test_deduction_follows_form_7206(filers): check_properties(Simulation(situation=situation), filers, n) +def filer(schedule_c, premiums, sstb=0, farm=0, partnership=0, wages=0, plan=0): + return dict( + schedule_c=schedule_c, + sstb=sstb, + farm=farm, + partnership=partnership, + 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.""" 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(2_000, 6_000), + filer(434, 6_000), + filer(433, 6_000), + filer(10_000, 20_000, wages=250_000), + filer(30_000, 40_000, farm=10_000), + filer(20_000, 5_000, plan=20_000), + filer(30_000, 30_000, sstb=-10_000), ] situation, n = build_situation(filers) sim = Simulation(situation=situation) @@ -197,3 +235,7 @@ def test_reviewer_example_and_boundaries(): # 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 + # A plan deduction equal to net profit leaves no limit. + assert deduction[5] <= TOLERANCE + # 20,000 of netted Schedule C profit: 20,000 - 20,000 x 0.9235 x 0.153 / 2. + assert abs(deduction[6] - 18_587.045) < TOLERANCE 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..c52e4883c58 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 @@ -11,12 +11,15 @@ class self_employed_health_insurance_earned_income_limit(Variable): "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)." + "that business, with SSTB and non-SSTB Schedule C income as a single " + "business, so a loss in one reduces line 4. Its net profit is reduced " + "by the part of the deductible part of self-employment tax " + "attributable to it (lines 5 to 7) and by the self-employed SEP, " + "SIMPLE and qualified plan deduction (line 9), which the model also " + "bases on Schedule C income. Schedule 1 line 15 is read per person: " + "each person's own deductible self-employment tax. S corporation " + "wages (line 11) are not modeled, and self-employment income inputs " + "are already net of any section 911 exclusion (line 12)." ) definition_period = YEAR reference = ( From 343c325710da4e350fe5ea75848cddaa54beb18b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 12:59:58 -0400 Subject: [PATCH 6/6] Cover spouses across the wage base and a dependent; enforce the cent Round-2 review: add YAML cases for spouses on either side of the Social Security wage base (each uses their own Schedule 1 line 15) and for a dependent whose deduction is capped by their own earned income and stays off the filer's return, and pass rtol=0 so the conservation check allows exactly a cent. Co-Authored-By: Claude Opus 5.5 --- ...st_medical_expense_premium_conservation.py | 4 +- .../self_employed_health_insurance_ald.yaml | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py index d2c50fc2053..f5be96ce466 100644 --- a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py +++ b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py @@ -147,7 +147,9 @@ def _assert_conservation(members, simulation): assert (schedule_a_premium_use + se_deduction <= paid + 0.01).all() # All premiums here are otherwise eligible: unused premiums remain in # Schedule A's base, even when earnings cap the SE deduction. - np.testing.assert_allclose(schedule_a_premium_use + se_deduction, paid, atol=0.01) + np.testing.assert_allclose( + schedule_a_premium_use + se_deduction, paid, rtol=0, atol=0.01 + ) EXAMPLE_UNITS = [ 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 061dc600a9c..3efb0e0e568 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 @@ -101,3 +101,64 @@ health_insurance_premiums: 3_000 output: self_employed_health_insurance_ald_person: 0 + +- name: Case 7, spouses on either side of the Social Security wage base each use their own self-employment tax deduction. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + is_self_employed: true + employment_income: 200_000 + self_employment_income: 10_000 + health_insurance_premiums: 20_000 + person2: + is_self_employed: true + self_employment_income: 10_000 + health_insurance_premiums: 20_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's wages exceed the $176,100 wage base, so Schedule 1 line 15 is + # 10,000 x 0.9235 x 0.029 / 2 = 133.9075; person2's is + # 10,000 x 0.9235 x 0.153 / 2 = 706.4775. + self_employment_tax_ald_person: [133.9075, 706.4775] + # Form 7206 line 13 for each: 10,000 less their own line 15. + self_employed_health_insurance_ald_person: [9_866.0925, 9_293.5225] + self_employed_health_insurance_ald: 19_159.615 + +- name: Case 8, a dependent's deduction is capped by their own earned income and stays off the filer's return. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + employment_income: 50_000 + child: + age: 17 + is_tax_unit_dependent: true + is_self_employed: true + self_employment_income: 3_000 + health_insurance_premiums: 5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # child: 3,000 - 3,000 x 0.9235 x 0.153 / 2 = 3,000 - 211.94325. + self_employed_health_insurance_ald_person: [0, 2_788.05675] + # The child takes it on their own return. + self_employed_health_insurance_ald: 0 + dependents_self_employed_health_insurance_ald: 2_788.05675