From 53fe0ac7841614626494db9c274a014ecf88220d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:21:12 -0400 Subject: [PATCH 1/3] Exclude employer-plan-eligible premiums from the self-employed health insurance deduction 26 U.S.C. 162(l)(2)(B) disallows the deduction for any month the taxpayer is eligible for a subsidized plan of their own or their spouse's employer (Form 7206 line 1). The premium base now leaves out the premiums of a person who has or is offered employer coverage, whose employer pays part of the premiums, or whose spouse on a joint return does. Co-Authored-By: Claude Opus 5.5 --- .../fix-sehi-employer-plan-exclusion.fixed.md | 1 + ...ployed_health_insurance_employer_plan.yaml | 197 ++++++++++++++++++ ...health_insurance_employer_plan_eligible.py | 55 +++++ ...self_employed_health_insurance_premiums.py | 22 +- 4 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fix-sehi-employer-plan-exclusion.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan_eligible.py diff --git a/changelog.d/fix-sehi-employer-plan-exclusion.fixed.md b/changelog.d/fix-sehi-employer-plan-exclusion.fixed.md new file mode 100644 index 00000000000..8382ec25a13 --- /dev/null +++ b/changelog.d/fix-sehi-employer-plan-exclusion.fixed.md @@ -0,0 +1 @@ +Leave out of the self-employed health insurance deduction the premiums of a person who, or whose spouse on a joint return, is eligible for a subsidized employer health plan (26 U.S.C. 162(l)(2)(B), Form 7206 line 1). diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan.yaml new file mode 100644 index 00000000000..cfffb247700 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan.yaml @@ -0,0 +1,197 @@ +# 26 U.S.C. 162(l)(2)(B) and Form 7206 line 1: premiums for any month the +# taxpayer is eligible for a subsidized plan of their own or their spouse's +# employer don't count toward the self-employed health insurance deduction. +# Profit of $50,000 keeps the earned income limit (Form 7206 line 13) above the +# $6,000 of premiums, so the deduction equals the premiums that count. + +- name: No employer plan, so all premiums count. + period: 2025 + input: + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + output: + self_employed_health_insurance_employer_plan_eligible: false + self_employed_health_insurance_premiums: 6_000 + self_employed_health_insurance_ald_person: 6_000 + +- name: Enrolled in employer coverage, so no premiums count. + period: 2025 + input: + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + has_esi: true + output: + self_employed_health_insurance_employer_plan_eligible: true + self_employed_health_insurance_premiums: 0 + self_employed_health_insurance_ald_person: 0 + +- name: Offered employer coverage without enrolling, so no premiums count. + period: 2025 + input: + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + offered_aca_disqualifying_esi: true + output: + self_employed_health_insurance_employer_plan_eligible: true + self_employed_health_insurance_premiums: 0 + self_employed_health_insurance_ald_person: 0 + +- name: An employer pays part of the premiums, so no premiums count. + period: 2025 + input: + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + employer_contribution_to_health_insurance_premiums_category: SOME + output: + self_employed_health_insurance_employer_plan_eligible: true + self_employed_health_insurance_premiums: 0 + self_employed_health_insurance_ald_person: 0 + +- name: The spouse's employer plan bars a joint filer's deduction. + period: 2025 + input: + people: + head: + age: 45 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + spouse: + age: 45 + employment_income: 40_000 + has_esi: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + self_employed_health_insurance_employer_plan_eligible: [true, true] + self_employed_health_insurance_premiums: [0, 0] + self_employed_health_insurance_ald: 0 + # Half of SE tax: 50,000 x 0.9235 x 0.153 / 2 = 3,532.3875. + # AGI: 50,000 + 40,000 - 3,532.3875 = 86,467.6125. + adjusted_gross_income: 86_467.61 + +- name: The spouse's employer plan also bars the deduction on a joint return without explicit marital units. + period: 2025 + input: + people: + head: + age: 45 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + spouse: + age: 45 + employment_income: 40_000 + has_esi: true + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + self_employed_health_insurance_premiums: [0, 0] + self_employed_health_insurance_ald: 0 + +- name: A housemate's employer plan does not bar a single filer's deduction. + period: 2025 + input: + people: + filer: + age: 45 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + housemate: + age: 45 + employment_income: 40_000 + has_esi: true + tax_units: + filer_unit: + members: [filer] + housemate_unit: + members: [housemate] + households: + household: + members: [filer, housemate] + state_code: TX + output: + self_employed_health_insurance_employer_plan_eligible: [false, true] + self_employed_health_insurance_premiums: [6_000, 0] + self_employed_health_insurance_ald: [6_000, 0] + +- name: A dependent child's own employer plan does not bar the parent's deduction. + period: 2025 + input: + people: + head: + age: 50 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 6_000 + spouse: + age: 50 + child: + age: 20 + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 5_000 + has_esi: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: TX + output: + self_employed_health_insurance_employer_plan_eligible: [false, false, true] + self_employed_health_insurance_premiums: [6_000, 0, 0] + self_employed_health_insurance_ald: 6_000 + +- name: A parent's employer plan does not bar a self-employed dependent's own deduction. + period: 2025 + input: + people: + head: + age: 50 + employment_income: 80_000 + has_esi: true + child: + age: 22 + is_tax_unit_dependent: true + is_full_time_student: true + is_self_employed: true + self_employment_income: 3_000 + health_insurance_premiums: 1_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + self_employed_health_insurance_employer_plan_eligible: [true, false] + self_employed_health_insurance_premiums: [0, 1_000] + # The child's limit is 3,000 of profit (less half its SE tax after + # 401(c)), above the 1,000 of premiums either way. + self_employed_health_insurance_ald_person: [0, 1_000] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan_eligible.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan_eligible.py new file mode 100644 index 00000000000..720a8c29e41 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_employer_plan_eligible.py @@ -0,0 +1,55 @@ +from policyengine_us.model_api import * + + +class self_employed_health_insurance_employer_plan_eligible(Variable): + value_type = bool + entity = Person + label = "Eligible for a subsidized employer health plan (self-employed health insurance deduction)" + documentation = ( + "Whether the person, or their spouse, is eligible for a subsidized " + "health plan of their employer, which bars the self-employed health " + "insurance deduction for the premiums (26 U.S.C. 162(l)(2)(B); Form " + "7206 line 1). The law applies the bar month by month; the inputs are " + "annual, so a person eligible at any point is treated as eligible for " + "the whole year. Two cases are not modeled. A spouse who files " + "separately is in another tax unit, and the model does not see their " + "employer's plan; it cannot use the filing status here, which depends " + "on dependents' incomes and so on this deduction, and the default " + "dataset has no married couple split across tax units. The plan of a " + "dependent's or under-27 child's employer bars the deduction only for " + "a taxpayer who can take part in it, and no input records that." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_2_B", + "https://www.irs.gov/pub/irs-prior/f7206--2025.pdf#page=1", # Line 1, first bullet + "https://www.irs.gov/pub/irs-prior/i7206--2025.pdf#page=2", # Other coverage + "https://www.irs.gov/pub/irs-prior/i1040gi--2022.pdf#page=88", # Line 17, before Form 7206 + ) + + def formula(person, period, parameters): + # Enrolled in, or offered, employer coverage, or the employer pays some + # or all of the premiums. Enrollment and an offer each show the person + # can take part in the plan. An employer that pays premiums subsidizes + # the plan. + contribution = person( + "employer_contribution_to_health_insurance_premiums_category", + period, + ) + contribution_status = contribution.possible_values + employer_pays = (contribution == contribution_status.SOME) | ( + contribution == contribution_status.ALL + ) + own = ( + person("has_esi", period) + | person("offered_aca_disqualifying_esi", period) + | employer_pays + ) + # The plan of the spouse's employer also bars the deduction: on a joint + # return, the other of the head and spouse. A tax unit dependent's own + # deduction is not barred by the plan of a parent's employer, which + # the statute does not list. + filer = person("is_tax_unit_head_or_spouse", period) + filer_own = own & filer + other_filer_eligible = person.tax_unit.sum(filer_own) > filer_own.astype(int) + return own | (filer & other_filer_eligible) diff --git a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py index 903b966d923..72321944561 100644 --- a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py @@ -6,7 +6,25 @@ class self_employed_health_insurance_premiums(Variable): entity = Person label = "Self-employed health insurance premiums" unit = USD - documentation = "Health insurance premiums for plans covering individuals who are not covered by any employer-sponsored health insurance." + documentation = ( + "Health insurance premiums that count toward the self-employed health " + "insurance deduction (Form 7206 line 1). Premiums are left out when " + "the person or their spouse is eligible for a subsidized employer " + "plan (26 U.S.C. 162(l)(2)(B)). Premiums are taken as supplied: for a " + "Marketplace plan, 26 U.S.C. 280C(g) allows no deduction for the part " + "the premium tax credit pays, and the model does not net the credit " + "it calculates out of these premiums." + ) definition_period = YEAR defined_for = "is_self_employed" - adds = ["health_insurance_premiums"] + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l", + "https://www.irs.gov/pub/irs-prior/f7206--2025.pdf#page=1", # Line 1 + ) + + def formula(person, period, parameters): + premiums = person("health_insurance_premiums", period) + employer_plan_eligible = person( + "self_employed_health_insurance_employer_plan_eligible", period + ) + return where(employer_plan_eligible, 0, premiums) From 508d09200cdf9d21133f2b1c8502001cfd9c024b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:29:25 -0400 Subject: [PATCH 2/3] Add Hypothesis properties for the self-employed health insurance employer-plan bar Co-Authored-By: Claude Opus 5.5 --- ...alth_insurance_employer_plan_properties.py | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 policyengine_us/tests/unit/test_self_employed_health_insurance_employer_plan_properties.py diff --git a/policyengine_us/tests/unit/test_self_employed_health_insurance_employer_plan_properties.py b/policyengine_us/tests/unit/test_self_employed_health_insurance_employer_plan_properties.py new file mode 100644 index 00000000000..2d5cf440b9b --- /dev/null +++ b/policyengine_us/tests/unit/test_self_employed_health_insurance_employer_plan_properties.py @@ -0,0 +1,275 @@ +"""Properties of the employer-plan bar on the self-employed health insurance deduction. + +26 U.S.C. 162(l)(2)(B) and Form 7206 line 1 leave out premiums for any month +the taxpayer is eligible for a subsidized plan of their own or their spouse's +employer. The model's inputs are annual, so a person who has or is offered +employer coverage (`has_esi`, `offered_aca_disqualifying_esi`), or whose +employer pays some or all of the premiums, is eligible for the year, and so is +the other of the head and spouse on a joint return. A tax unit dependent is +barred only by their own coverage, never by a parent's. + +Each batch places every drawn tax unit twice, once as drawn and once with every +coverage flag cleared, and runs as one vectorized baseline simulation. For +every tax unit and year: + +1. Reference: each person's premium base equals a closed-form reference on + the inputs: the person's premiums if they are self-employed and neither + they nor (for the head and spouse) the other of the two is eligible, and 0 + otherwise. +2. Bounds: 0 <= deduction <= premium base <= premiums. +3. Monotone: clearing the coverage flags never lowers a person's premium base + or deduction. +4. Differential: the flags move AGI only through the deduction, so the + twins' AGI difference equals the difference in the head's and spouse's + deductions. + +A deterministic grid runs every combination of the head's, the spouse's and a +dependent's flags for a joint couple with a self-employed head. +Sources: https://www.law.cornell.edu/uscode/text/26/162#l_2_B and +https://www.irs.gov/pub/irs-prior/f7206--2025.pdf#page=1 (line 1). +""" + +import itertools + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEARS = [2024, 2025, 2026] +TOLERANCE = 0.01 +CONTRIBUTIONS = ["NONE", "SOME", "ALL", "NA"] +EMPLOYER_PAYS = {"SOME", "ALL"} +NO_FLAGS = {"has_esi": False, "offered": False, "contribution": "NONE"} + + +def every_year(value): + return {year: value for year in YEARS} + + +def own_eligible(flags): + return ( + flags["has_esi"] or flags["offered"] or flags["contribution"] in EMPLOYER_PAYS + ) + + +def person_inputs(age, member, flags, dependent=False): + inputs = { + "age": every_year(age), + "is_self_employed": every_year(member["self_employed"]), + "self_employment_income": every_year(member["profit"]), + "employment_income": every_year(member["wages"]), + "health_insurance_premiums": every_year(member["premiums"]), + "has_esi": every_year(flags["has_esi"]), + "offered_aca_disqualifying_esi": every_year(flags["offered"]), + "employer_contribution_to_health_insurance_premiums_category": every_year( + flags["contribution"] + ), + } + if dependent: + inputs["is_tax_unit_dependent"] = every_year(True) + return inputs + + +def build_situation(units): + """Each unit twice: as drawn, then with every coverage flag cleared.""" + people, tax_units, households = {}, {}, {} + rows = [] # per person: (premiums, self-employed, expected base, unit, twin) + unit_index = 0 + for twin in (False, True): + for i, unit in enumerate(units): + name = f"u{i}_{'twin' if twin else 'drawn'}" + members = unit["members"] + flags = [NO_FLAGS if twin else m["flags"] for m in members] + filers = [True, unit["joint"]][: min(2, len(members))] + filers += [False] * (len(members) - len(filers)) + names = [] + for j, (member, member_flags) in enumerate(zip(members, flags)): + person = f"{name}_p{j}" + names.append(person) + is_dependent = not filers[j] + people[person] = person_inputs( + member["age"], member, member_flags, dependent=is_dependent + ) + for j, member in enumerate(members): + own = own_eligible(flags[j]) + other_filer = any( + own_eligible(flags[k]) + for k in range(len(members)) + if k != j and filers[k] + ) + eligible = own or (filers[j] and other_filer) + base = ( + member["premiums"] + if member["self_employed"] and not eligible + else 0.0 + ) + rows.append( + ( + member["premiums"], + member["self_employed"], + base, + unit_index, + twin, + ) + ) + tax_units[name] = {"members": names} + households[name] = {"members": names, "state_code": every_year("TX")} + unit_index += 1 + situation = {"people": people, "tax_units": tax_units, "households": households} + premiums, self_employed, base, unit, twin = (np.array(c) for c in zip(*rows)) + return situation, premiums.astype(float), base.astype(float), unit, twin + + +def check_batch(units): + situation, premiums, expected_base, unit, twin = build_situation(units) + sim = Simulation(situation=situation) + n = len(units) + for year in YEARS: + base = sim.calculate("self_employed_health_insurance_premiums", year) + deduction = sim.calculate("self_employed_health_insurance_ald_person", year) + # 1. Reference. + np.testing.assert_allclose(base, expected_base, atol=TOLERANCE) + # 2. Bounds. + assert np.all(deduction >= -TOLERANCE) + assert np.all(deduction <= base + TOLERANCE) + assert np.all(base <= premiums + TOLERANCE) + # 3. Monotone: the twin (no flags) of each person has at least as much. + drawn, cleared = ~twin, twin + assert np.all(base[cleared] >= base[drawn] - TOLERANCE) + assert np.all(deduction[cleared] >= deduction[drawn] - TOLERANCE) + # 4. Differential: AGI moves only through the head's and spouse's + # deductions, which `self_employed_health_insurance_ald` sums. + agi = sim.calculate("adjusted_gross_income", year) + filer_deduction = sim.calculate("self_employed_health_insurance_ald", year) + np.testing.assert_allclose( + agi[:n] - agi[n:], + filer_deduction[n:] - filer_deduction[:n], + atol=TOLERANCE, + ) + + +flags_strategy = st.fixed_dictionaries( + { + "has_esi": st.booleans(), + "offered": st.booleans(), + "contribution": st.sampled_from(CONTRIBUTIONS), + } +) +money = st.one_of(st.sampled_from([0, 1, 6_000]), st.integers(0, 40_000)).map(float) +profit = st.one_of( + st.sampled_from([-5_000, 0, 433, 434, 50_000]), st.integers(-20_000, 250_000) +).map(float) + + +def member_strategy(age): + return st.fixed_dictionaries( + { + "age": st.just(age), + "self_employed": st.booleans(), + "profit": profit, + "wages": st.one_of(st.just(0.0), money), + "premiums": money, + "flags": flags_strategy, + } + ) + + +unit_strategy = st.builds( + lambda joint, head, spouse, dependents: { + "joint": joint, + "members": [head] + ([spouse] if joint else []) + dependents, + }, + st.booleans(), + member_strategy(45), + member_strategy(43), + st.lists(member_strategy(19), max_size=2), +) +SETTINGS = dict( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(unit_strategy, min_size=4, max_size=12)) +def test_premium_base_matches_the_form_7206_reference(units): + check_batch(units) + + +ALL_FLAGS = [ + {"has_esi": has_esi, "offered": offered, "contribution": contribution} + for has_esi, offered, contribution in itertools.product( + [False, True], [False, True], CONTRIBUTIONS + ) +] + + +def grid_units(): + def member(age, self_employed, flags, premiums): + return { + "age": age, + "self_employed": self_employed, + "profit": 50_000.0 if self_employed else 0.0, + "wages": 0.0 if self_employed else 30_000.0, + "premiums": premiums, + "flags": flags, + } + + return [ + { + "joint": True, + "members": [ + member(45, True, head, 6_000.0), + member(43, False, spouse, 0.0), + member(19, False, dependent, 0.0), + ], + } + for head, spouse, dependent in itertools.product( + ALL_FLAGS, ALL_FLAGS, [NO_FLAGS, {**NO_FLAGS, "has_esi": True}] + ) + ] + + +@pytest.fixture(scope="module") +def grid(): + units = grid_units() + situation, premiums, expected_base, unit, twin = build_situation(units) + return Simulation(situation=situation), units, expected_base, twin + + +@pytest.mark.parametrize("year", YEARS) +def test_grid_bars_on_head_or_spouse_coverage_only(grid, year): + sim, units, expected_base, twin = grid + base = sim.calculate("self_employed_health_insurance_premiums", year) + np.testing.assert_allclose(base, expected_base, atol=TOLERANCE) + head_base = base[~twin].reshape(len(units), 3)[:, 0] + barred = np.array( + [ + own_eligible(u["members"][0]["flags"]) + or own_eligible(u["members"][1]["flags"]) + for u in units + ] + ) + # The head's premiums count exactly when neither filer is eligible; the + # dependent's coverage never matters. + np.testing.assert_allclose(head_base, np.where(barred, 0, 6_000), atol=TOLERANCE) + # Guard against a vacuous pass. + assert barred.any() and (~barred).any() + + +@pytest.mark.parametrize("year", YEARS) +def test_grid_deduction_moves_agi_one_for_one(grid, year): + sim, units, _, twin = grid + n = len(units) + agi = sim.calculate("adjusted_gross_income", year) + deduction = sim.calculate("self_employed_health_insurance_ald", year) + np.testing.assert_allclose( + agi[:n] - agi[n:], deduction[n:] - deduction[:n], atol=TOLERANCE + ) + # The twins all deduct the full $6,000; the earned income limit is higher. + np.testing.assert_allclose(deduction[n:], 6_000, atol=TOLERANCE) From 7cebc0f41b872754a38650bb9c2db4de51659759 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:38:26 -0400 Subject: [PATCH 3/3] Point the self-employed premium documentation at the PTC interaction issue (#10124) Co-Authored-By: Claude Opus 5.5 --- .../self_employed_health_insurance_premiums.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py index 72321944561..17289966e8c 100644 --- a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py @@ -9,11 +9,13 @@ class self_employed_health_insurance_premiums(Variable): documentation = ( "Health insurance premiums that count toward the self-employed health " "insurance deduction (Form 7206 line 1). Premiums are left out when " - "the person or their spouse is eligible for a subsidized employer " - "plan (26 U.S.C. 162(l)(2)(B)). Premiums are taken as supplied: for a " - "Marketplace plan, 26 U.S.C. 280C(g) allows no deduction for the part " - "the premium tax credit pays, and the model does not net the credit " - "it calculates out of these premiums." + "the person, or their spouse on a joint return, is eligible for a " + "subsidized employer plan (26 U.S.C. 162(l)(2)(B)). Premiums are " + "taken as supplied: for a Marketplace plan, 26 U.S.C. 280C(g) allows " + "no deduction for the part the premium tax credit pays, and the model " + "does not net the credit it calculates out of these premiums: the " + "credit depends on AGI, and so on this deduction (policyengine-us " + "#10124)." ) definition_period = YEAR defined_for = "is_self_employed"