From 8f18eb19e45f6834509e82b1549c1272e61e9a2c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 19:02:50 +0000 Subject: [PATCH 1/6] Count the owners' draw in the SEHI limit's line 5 under the reform With the owners' draw reform on, the deductible SE tax on Form 7206 line 7 includes the draw's SECA, but line 5 left out the draw's profits, so the Schedule C plan business took too large a share of that tax and the limit fell (40,000 Schedule C with a 60,000 net-earnings draw: 33,454.09 instead of 37,505.61). The reform now overrides the limit, adding the draw's gross profit to line 5 when the reform is in effect; the baseline formula is untouched. Co-Authored-By: Claude Opus 5.5 --- changelog.d/owners-draw-sehi-line5.fixed.md | 1 + policyengine_us/reforms/ssa/owners_draw.py | 41 +++++++++ ..._health_insurance_earned_income_limit.yaml | 89 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 changelog.d/owners-draw-sehi-line5.fixed.md create mode 100644 policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml diff --git a/changelog.d/owners-draw-sehi-line5.fixed.md b/changelog.d/owners-draw-sehi-line5.fixed.md new file mode 100644 index 00000000000..982e6f90168 --- /dev/null +++ b/changelog.d/owners-draw-sehi-line5.fixed.md @@ -0,0 +1 @@ +Count the owners' draw in Form 7206 line 5 when the owners' draw reform computes the self-employed health insurance earned income limit. diff --git a/policyengine_us/reforms/ssa/owners_draw.py b/policyengine_us/reforms/ssa/owners_draw.py index 604f324a271..7e65f977fb3 100644 --- a/policyengine_us/reforms/ssa/owners_draw.py +++ b/policyengine_us/reforms/ssa/owners_draw.py @@ -15,6 +15,9 @@ 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 as baseline_se_deduction, ) +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, +) from policyengine_us.variables.gov.ssa.social_security.ss_aime import ( ss_aime as baseline_aime, _compute_aime, @@ -284,6 +287,43 @@ def formula(person, period, parameters): ) return deductible_tax * p.irs.ald.self_employment_tax.percent_deductible + class self_employed_health_insurance_earned_income_limit(baseline_sehi_limit): + def formula(person, period, parameters): + if not _enabled(period, parameters): + return baseline_sehi_limit.formula(person, period, parameters) + # Form 7206 as in the baseline formula, except that line 5 also + # counts the owners' draw: under this reform its profits bear SE + # tax, and line 7's deductible SE tax above includes that tax. + # Line 5 sums net profits before the net-earnings factor, so the + # draw enters as its gross amount, like line 4's Schedule C profit. + 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)) + + max_(0, person("owners_draw_gross", period)) + ) + 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, + ) + class ss_aime(baseline_aime): def formula(person, period, parameters): supplied = person("ss_aime_input", period) @@ -316,6 +356,7 @@ def apply(self): self.update_variable(self_employment_social_security_tax) self.update_variable(self_employment_medicare_tax) self.update_variable(self_employment_tax_ald_person) + self.update_variable(self_employed_health_insurance_earned_income_limit) self.update_variable(ss_aime) return reform 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 new file mode 100644 index 00000000000..54e66854a25 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml @@ -0,0 +1,89 @@ +# Form 7206 lines 4-13 with the owners' draw reform on. Line 5 counts the +# net profits of every business subject to SE tax, so it includes the draw's +# gross profit, and line 7's deductible SE tax includes the draw's SECA. +# Synthetic caps follow owners_draw.yaml; q = 1 - 0.5 * (0.124 + 0.029) = +# 0.9235, and HI on the draw defaults off. Expected values are hand-computed. + +- name: Schedule C plan business with an owners' draw counts the draw in line 5 + 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: 40_000 + # 64,970.22 x 0.9235 = 59,999.998 of draw net earnings. + s_corp_income: 64_970.22 + self_employed_health_insurance_premiums: 8_000 + output: + owners_draw_gross: 64_970.22 + owners_draw_net_earnings: 59_999.998 + # Schedule C SE tax: 40,000 x 0.9235 x 15.3% = 5,651.82. Draw OASDI: + # 59,999.998 x 12.4% = 7,440.00. Half of 13,091.82 = 6,545.91. + self_employment_tax_ald_person: 6_545.91 + # Line 5: 40,000 + 64,970.22 = 104,970.22. Line 6: 0.3810605. + # Line 7: 6,545.91 x 0.3810605 = 2,494.39. Line 13: 40,000 - 2,494.39. + # Without the draw in line 5 the share would be 1: 40,000 - 6,545.91 = + # 33,454.09. + self_employed_health_insurance_earned_income_limit: 37_505.61 + # The $8,000 of premiums is below either limit. + self_employed_health_insurance_ald_person: 8_000 + +- name: Premiums above the old limit show the deduction effect + 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: 40_000 + s_corp_income: 64_970.22 + self_employed_health_insurance_premiums: 36_000 + output: + self_employed_health_insurance_earned_income_limit: 37_505.61 + # Below 37,505.61 but above the 33,454.09 that omitting the draw gives. + self_employed_health_insurance_ald_person: 36_000 + +- name: Without a draw the reform leaves the limit unchanged + 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: 40_000 + farm_operations_income: 10_000 + output: + owners_draw_gross: 0 + # 50,000 x 0.9235 x 15.3% / 2 = 3,532.39. + self_employment_tax_ald_person: 3_532.39 + # 40,000 / 50,000 = 0.8; 40,000 - 0.8 x 3,532.39 = 37,174.09, as at + # baseline. + self_employed_health_insurance_earned_income_limit: 37_174.09 + +- name: Without a Schedule C business the limit stays zero + 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 + s_corp_income: 64_970.22 + 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. + self_employed_health_insurance_earned_income_limit: 0 + self_employed_health_insurance_ald_person: 0 From 30fd2d7f6ac30f225b4bcf52c8a3990d1955d47d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 19:14:56 +0000 Subject: [PATCH 2/6] Test the reform's SEHI limit against Form 7206 with generated vectors Disabled, the reform's limit must equal the baseline formula; enabled, it must match an independent lines 4-13 oracle with the draw's gross profit in line 5, sit at or above main's draw-free result, and never exceed line 4 less the line 6 share of deductible SE tax. The pure harness now stores integer money inputs as floats, as Core does. Co-Authored-By: Claude Opus 5.5 --- .../tests/reforms/test_owners_draw.py | 108 +++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/reforms/test_owners_draw.py b/policyengine_us/tests/reforms/test_owners_draw.py index b1b51eb7819..4d96db223d2 100644 --- a/policyengine_us/tests/reforms/test_owners_draw.py +++ b/policyengine_us/tests/reforms/test_owners_draw.py @@ -33,6 +33,9 @@ from policyengine_us.variables.gov.contrib.congress.s770.s770_active_business_income import ( s770_active_business_income, ) +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.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.self_employment_tax_ald_person import ( self_employment_tax_ald_person, ) @@ -66,6 +69,7 @@ self_employment_medicare_tax, self_employment_tax, self_employment_tax_ald_person, + self_employed_health_insurance_earned_income_limit, ) } PERIOD = period("2027") @@ -145,10 +149,23 @@ def _calculate(variables, parameters, inputs, outputs=None): "sstb_self_employment_income": 0, "farm_operations_income": 0, "payroll_tax_gross_wages": 0, + "self_employed_pension_contribution_ald_person": 0, **inputs, } - values = {name: np.asarray(value) for name, value in values.items()} + # Core stores these money inputs as floats; integer literals would make + # the limit's zeros_like() output integer and reject its float division. + values = { + name: np.asarray(value, dtype=float) + if np.asarray(value).dtype.kind in "iu" + else np.asarray(value) + for name, value in values.items() + } + # Form 7206 line 4 reads this adds-variable; mirror its two components. + values.setdefault( + "total_self_employment_income", + values["self_employment_income"] + values["sstb_self_employment_income"], + ) def person(name, instant, options=None): if name not in values: @@ -691,6 +708,95 @@ def test_generated_deduction_tracks_existing_deductible_share( ) +_SEHI_ROWS = st.lists( + st.tuples( + st.integers(-200_000, 500_000), # Schedule C (plan business) + st.integers(-200_000, 500_000), # farm + st.integers(-200_000, 500_000), # partnership SE earnings + st.integers(-500_000, 1_000_000), # S-corporation profits + st.integers(0, 1_000_000), # wages + st.integers(0, 60_000), # SEP/SIMPLE/qualified plan deduction + ), + min_size=1, + max_size=12, +) + + +def _sehi_inputs(rows): + sched_c, farm, partnership_se, s_corp, wages, pension = np.asarray( + rows, dtype=float + ).T + return { + "self_employment_income": sched_c, + "farm_operations_income": farm, + "partnership_self_employment_net_earnings": partnership_se, + "partnership_income": partnership_se, + "s_corp_income": s_corp, + "payroll_tax_gross_wages": wages, + "self_employed_pension_contribution_ald_person": pension, + } + + +SEHI_LIMIT = "self_employed_health_insurance_earned_income_limit" +# Form 7206 line 4 net profit 40,000 and a draw with 60,000 of net earnings. +SEHI_HAND_CASE = [(40_000, 0, 0, 64_970.22, 0, 0)] + + +@settings(**SETTINGS) +@given(rows=_SEHI_ROWS, rates=_RATE_POLICY) +@example(rows=SEHI_HAND_CASE, rates=(124, 29, 500)) +def test_generated_disabled_sehi_limit_equals_baseline(reform_variables, rows, rates): + inputs = _sehi_inputs(rows) + parameters = _parameters(in_effect=False, **_policy_rates(rates)) + baseline = _calculate(BASELINE_VARIABLES, parameters, inputs, [SEHI_LIMIT]) + reformed = _calculate(reform_variables, parameters, inputs, [SEHI_LIMIT]) + np.testing.assert_array_equal(reformed[SEHI_LIMIT], baseline[SEHI_LIMIT]) + + +@settings(**SETTINGS) +@given(rows=_SEHI_ROWS, rates=_RATE_POLICY, include_hi=st.booleans()) +@example(rows=SEHI_HAND_CASE, rates=(124, 29, 500), include_hi=False) +def test_generated_sehi_limit_counts_the_draw_in_line_5( + reform_variables, rows, rates, include_hi +): + """Form 7206 lines 4-13 with every SE-taxed business in line 5.""" + inputs = _sehi_inputs(rows) + policy = dict(**_policy_rates(rates), include_hi=include_hi) + enabled = _calculate(reform_variables, _parameters(**policy), inputs) + # Main's formula on the same reformed SE tax omits the draw from line 5. + main = _calculate( + BASELINE_VARIABLES, + _parameters(**policy), + { + **inputs, + "self_employment_tax_ald_person": enabled["self_employment_tax_ald_person"], + }, + [SEHI_LIMIT], + ) + line_4 = np.maximum(0, inputs["self_employment_income"]) + line_5 = ( + line_4 + + np.maximum(0, inputs["farm_operations_income"]) + + np.maximum(0, inputs["partnership_self_employment_net_earnings"]) + + np.maximum(0, enabled["owners_draw_gross"]) + ) + share = np.divide(line_4, line_5, out=np.zeros_like(line_5), where=line_5 > 0) + plan_share_of_se_tax = enabled["self_employment_tax_ald_person"] * share + pension = inputs["self_employed_pension_contribution_ald_person"] + limit = enabled[SEHI_LIMIT] + np.testing.assert_allclose( + limit, np.maximum(0, line_4 - plan_share_of_se_tax - pension), atol=1e-6 + ) + # Never above line 4 less the plan business's line 6 share of the + # deductible SE tax (line 8), and never below main's draw-free share. + assert np.all(limit <= np.maximum(0, line_4 - plan_share_of_se_tax) + 1e-6) + assert np.all(limit >= main[SEHI_LIMIT] - 1e-6) + assert np.all(limit <= np.maximum(0, line_4 - pension) + 1e-6) + # With no draw, the reform's line 5 equals main's. + no_draw = enabled["owners_draw_gross"] == 0 + np.testing.assert_allclose(limit[no_draw], main[SEHI_LIMIT][no_draw], atol=1e-6) + + @contextmanager def _paused_collection(): """Pause automatic collection, then restore the setting found on entry.""" From 8d96db39acbf1129f20c036d9dab0c8b17998565 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 06:05:04 +0000 Subject: [PATCH 3/6] Count only SECA-taxed, deductible draw profits in Form 7206 line 5 Line 5 apportions line 7's deductible SE tax by business profits. Adding the whole gross draw raised the limit above main when the draw's SECA was not in line 7: with half_seca_deduction off, and for draw profits beyond the OASDI room with HI off. Line 5 now adds owners_draw_niit_exclusion, the gross profits that bear SECA, only when half_seca_deduction is on. YAML cases cover the three setups, the generated test varies half_seca_deduction, the cap, room_cap_basis and phase_in_share with an oracle that derives the draw term from the rule, and the docs describe the override. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6H6zhX5KYCDLztuSH4 --- changelog.d/owners-draw-sehi-line5.fixed.md | 2 +- docs/contributed_reforms/owners_draw.md | 13 +++ policyengine_us/reforms/ssa/owners_draw.py | 19 ++-- ..._health_insurance_earned_income_limit.yaml | 76 +++++++++++++++- .../tests/reforms/test_owners_draw.py | 87 +++++++++++++++---- 5 files changed, 174 insertions(+), 23 deletions(-) diff --git a/changelog.d/owners-draw-sehi-line5.fixed.md b/changelog.d/owners-draw-sehi-line5.fixed.md index 982e6f90168..c1a08655f5b 100644 --- a/changelog.d/owners-draw-sehi-line5.fixed.md +++ b/changelog.d/owners-draw-sehi-line5.fixed.md @@ -1 +1 @@ -Count the owners' draw in Form 7206 line 5 when the owners' draw reform computes the self-employed health insurance earned income limit. +Count the owners' draw profits whose SECA is in the deductible self-employment tax in Form 7206 line 5 when the owners' draw reform computes the self-employed health insurance earned income limit. diff --git a/docs/contributed_reforms/owners_draw.md b/docs/contributed_reforms/owners_draw.md index 4118ea5d0f4..da627e2f39f 100644 --- a/docs/contributed_reforms/owners_draw.md +++ b/docs/contributed_reforms/owners_draw.md @@ -96,6 +96,19 @@ Outputs include `owners_draw_active_income`, `owners_draw_planned_gross`, `owners_draw_net_earnings`, `owners_draw_oasdi_room`, `owners_draw_oasdi_base`, `owners_draw_oasdi_tax`, and `owners_draw_hi_tax`. +## Self-employed health insurance limit + +The reform also replaces `self_employed_health_insurance_earned_income_limit` +(Form 7206 line 13). Line 5 sums the profits of businesses subject to SE tax, +and line 6 apportions the deductible SE tax on line 7 to the plan business by +its share of line 5. With `half_seca_deduction: true`, line 5 adds +`owners_draw_niit_exclusion`: `draw_oasdi_base / q` with HI off, or the whole +actual gross draw D with HI 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. + For the current-law cap basis, the taxed draw is nonnegative and no larger than remaining room, adjusted earnings, or actual gross draw. Adding the draw cannot push otherwise capped covered earnings beyond C. Increasing the diff --git a/policyengine_us/reforms/ssa/owners_draw.py b/policyengine_us/reforms/ssa/owners_draw.py index 7e65f977fb3..8d676167dda 100644 --- a/policyengine_us/reforms/ssa/owners_draw.py +++ b/policyengine_us/reforms/ssa/owners_draw.py @@ -292,10 +292,19 @@ def formula(person, period, parameters): if not _enabled(period, parameters): return baseline_sehi_limit.formula(person, period, parameters) # Form 7206 as in the baseline formula, except that line 5 also - # counts the owners' draw: under this reform its profits bear SE - # tax, and line 7's deductible SE tax above includes that tax. - # Line 5 sums net profits before the net-earnings factor, so the - # draw enters as its gross amount, like line 4's Schedule C profit. + # counts the gross owners' profits that bear SECA under this + # reform (owners_draw_niit_exclusion: the OASDI base divided by + # the net-earnings factor, or the whole gross draw with HI on), + # and only when half_seca_deduction puts that SECA into + # self_employment_tax_ald_person, which line 7 apportions. Draw + # profits above the OASDI room with HI off, or whose SECA is not + # deductible, stay out of line 5, as on main. + draw = parameters(period).gov.contrib.ssa.owners_draw + draw_profits_in_line_7 = where( + draw.half_seca_deduction, + person("owners_draw_niit_exclusion", period), + 0, + ) plan_business_profit = max_( 0, person("total_self_employment_income", period) ) @@ -303,7 +312,7 @@ def formula(person, period, parameters): plan_business_profit + max_(0, person("farm_operations_income", period)) + max_(0, person("partnership_self_employment_net_earnings", period)) - + max_(0, person("owners_draw_gross", period)) + + draw_profits_in_line_7 ) plan_business_share = np.divide( plan_business_profit, 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 54e66854a25..b476c91f079 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 @@ -1,6 +1,7 @@ # Form 7206 lines 4-13 with the owners' draw reform on. Line 5 counts the -# net profits of every business subject to SE tax, so it includes the draw's -# gross profit, and line 7's deductible SE tax includes the draw's SECA. +# net profits of every business subject to SE tax. The reform adds the gross +# draw profits that bear SECA (owners_draw_niit_exclusion), and only when +# half_seca_deduction puts that SECA into line 7's deductible SE tax. # Synthetic caps follow owners_draw.yaml; q = 1 - 0.5 * (0.124 + 0.029) = # 0.9235, and HI on the draw defaults off. Expected values are hand-computed. @@ -87,3 +88,74 @@ # Line 4 is zero, so line 13 and the deduction are zero. self_employed_health_insurance_earned_income_limit: 0 self_employed_health_insurance_ald_person: 0 + +- name: Nondeductible draw SECA keeps the draw out of line 5 + 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.contrib.ssa.owners_draw.half_seca_deduction: false + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + owners_draw_oasdi_tax: 7_440 + # Only the Schedule C SE tax is deductible: 5,651.82 / 2 = 2,825.91. + self_employment_tax_ald_person: 2_825.91 + # Line 5 is 40,000, as on main, so line 6 is 1: 40,000 - 2,825.91. + # Counting the draw would give 40,000 - 0.3810605 x 2,825.91 = 38,923.16. + self_employed_health_insurance_earned_income_limit: 37_174.09 + +- name: A draw above the OASDI cap with HI off stays out of line 5 + 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 + payroll_tax_gross_wages: 300_000 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + owners_draw_oasdi_base: 0 + owners_draw_oasdi_tax: 0 + owners_draw_niit_exclusion: 0 + # Wages use the OASDI cap, so Schedule C pays HI only: + # 36,940 x 2.9% / 2 = 535.63. + self_employment_tax_ald_person: 535.63 + # Line 5 is 40,000: 40,000 - 535.63. Counting the gross draw would give + # 40,000 - 0.3810605 x 535.63 = 39,795.89. + self_employed_health_insurance_earned_income_limit: 39_464.37 + +- name: A draw partly within the OASDI cap counts only its taxed profits + 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 + payroll_tax_gross_wages: 150_000 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + # Room: 200,000 - 150,000 - 36,940 = 13,060 of the 60,000 net earnings. + owners_draw_oasdi_base: 13_060 + owners_draw_oasdi_tax: 1_619.44 + # 13,060 / 0.9235 = 14,141.85 of gross draw profits bear SECA. + owners_draw_niit_exclusion: 14_141.85 + # (5,651.82 + 1,619.44) / 2 = 3,635.63. + self_employment_tax_ald_person: 3_635.63 + # Line 5: 40,000 + 14,141.85 = 54,141.85; line 6: 0.7387999. + # Line 7: 3,635.63 x 0.7387999 = 2,686.00; line 13: 40,000 - 2,686.00. + # Main gives 36,364.37 and the whole gross draw 38,614.61. + self_employed_health_insurance_earned_income_limit: 37_314.00 diff --git a/policyengine_us/tests/reforms/test_owners_draw.py b/policyengine_us/tests/reforms/test_owners_draw.py index 4d96db223d2..deb6b2cb60b 100644 --- a/policyengine_us/tests/reforms/test_owners_draw.py +++ b/policyengine_us/tests/reforms/test_owners_draw.py @@ -753,15 +753,51 @@ def test_generated_disabled_sehi_limit_equals_baseline(reform_variables, rows, r np.testing.assert_array_equal(reformed[SEHI_LIMIT], baseline[SEHI_LIMIT]) +_SEHI_POLICY = st.fixed_dictionaries( + { + "include_hi": st.booleans(), + "half_seca_deduction": st.booleans(), + "room_cap_basis": st.sampled_from([0, 1]), + "phase_in_share": st.sampled_from([0, 0.5, 1]), + "cap": st.integers(1, 400_000), + } +) +_SEHI_WITNESS_POLICY = dict( + include_hi=False, + half_seca_deduction=True, + room_cap_basis=0, + phase_in_share=1, + cap=CAP, +) + + @settings(**SETTINGS) -@given(rows=_SEHI_ROWS, rates=_RATE_POLICY, include_hi=st.booleans()) -@example(rows=SEHI_HAND_CASE, rates=(124, 29, 500), include_hi=False) -def test_generated_sehi_limit_counts_the_draw_in_line_5( - reform_variables, rows, rates, include_hi +@given(rows=_SEHI_ROWS, rates=_RATE_POLICY, policy=_SEHI_POLICY) +@example(rows=SEHI_HAND_CASE, rates=(124, 29, 500), policy=_SEHI_WITNESS_POLICY) +# The draw's SECA is not deductible, so line 7 holds none of it. +@example( + rows=SEHI_HAND_CASE, + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY | {"half_seca_deduction": False}, +) +# Wages use the OASDI room, so with HI off the draw bears no SECA. +@example( + rows=[(40_000, 0, 0, 64_970.22, 300_000, 0)], + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY, +) +# 13,060 of the draw's 60,000 net earnings fits in the OASDI room. +@example( + rows=[(40_000, 0, 0, 64_970.22, 150_000, 0)], + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY, +) +def test_generated_sehi_limit_counts_seca_taxed_draw_in_line_5( + reform_variables, rows, rates, policy ): - """Form 7206 lines 4-13 with every SE-taxed business in line 5.""" + """Form 7206 lines 4-13 with the SECA-taxed draw profits in line 5.""" inputs = _sehi_inputs(rows) - policy = dict(**_policy_rates(rates), include_hi=include_hi) + policy = dict(**_policy_rates(rates), **policy) enabled = _calculate(reform_variables, _parameters(**policy), inputs) # Main's formula on the same reformed SE tax omits the draw from line 5. main = _calculate( @@ -773,28 +809,49 @@ def test_generated_sehi_limit_counts_the_draw_in_line_5( }, [SEHI_LIMIT], ) + # Line 5's draw term from the rule: the gross draw profits that bear + # SECA, counted only when that SECA is in line 7's deductible SE tax. + # With HI on the whole gross draw bears SECA; with HI off only the part + # whose net earnings fit in the OASDI room left by wages and existing SE. + gross = enabled["owners_draw_gross"] + factor = 1 - policy["employer_share"] * (policy["oasdi_rate"] + policy["hi_rate"]) + if policy["room_cap_basis"] == 1: + room = np.inf + else: + room = np.maximum( + 0, + policy["cap"] + - enabled["taxable_earnings_for_social_security"] + - enabled["social_security_taxable_self_employment_income"], + ) + seca_taxed_draw = ( + gross if policy["include_hi"] else np.minimum(gross * factor, room) / factor + ) + draw_term = seca_taxed_draw if policy["half_seca_deduction"] else 0 * gross line_4 = np.maximum(0, inputs["self_employment_income"]) line_5 = ( line_4 + np.maximum(0, inputs["farm_operations_income"]) + np.maximum(0, inputs["partnership_self_employment_net_earnings"]) - + np.maximum(0, enabled["owners_draw_gross"]) + + draw_term ) share = np.divide(line_4, line_5, out=np.zeros_like(line_5), where=line_5 > 0) plan_share_of_se_tax = enabled["self_employment_tax_ald_person"] * share pension = inputs["self_employed_pension_contribution_ald_person"] limit = enabled[SEHI_LIMIT] np.testing.assert_allclose( - limit, np.maximum(0, line_4 - plan_share_of_se_tax - pension), atol=1e-6 + limit, + np.maximum(0, line_4 - plan_share_of_se_tax - pension), + rtol=1e-9, + atol=1e-6, ) - # Never above line 4 less the plan business's line 6 share of the - # deductible SE tax (line 8), and never below main's draw-free share. - assert np.all(limit <= np.maximum(0, line_4 - plan_share_of_se_tax) + 1e-6) + # Checks against main's separately computed formula: a draw in line 5 + # can only lower line 6, and a draw term of zero leaves main's limit. assert np.all(limit >= main[SEHI_LIMIT] - 1e-6) - assert np.all(limit <= np.maximum(0, line_4 - pension) + 1e-6) - # With no draw, the reform's line 5 equals main's. - no_draw = enabled["owners_draw_gross"] == 0 - np.testing.assert_allclose(limit[no_draw], main[SEHI_LIMIT][no_draw], atol=1e-6) + no_draw_term = draw_term == 0 + np.testing.assert_allclose( + limit[no_draw_term], main[SEHI_LIMIT][no_draw_term], atol=1e-6 + ) @contextmanager From 80f3fd9dffda49f2df32673958085636d5266316 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:17:35 +0000 Subject: [PATCH 4/6] Test the SEHI limit with HI on and with a half phase-in The round-2 review found that no test reached HI on with half_seca_deduction false, HI on with OASDI room used, or phase_in_share 0.5 with HI off, so a mutant counting the whole draw whenever HI is on survived. Add the three YAML cases and matching @examples so each setup is pinned. Co-Authored-By: Claude Opus 5.5 --- ..._health_insurance_earned_income_limit.yaml | 69 +++++++++++++++++++ .../tests/reforms/test_owners_draw.py | 19 +++++ 2 files changed, 88 insertions(+) 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 b476c91f079..eaa66e33548 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 @@ -159,3 +159,72 @@ # Line 7: 3,635.63 x 0.7387999 = 2,686.00; line 13: 40,000 - 2,686.00. # Main gives 36,364.37 and the whole gross draw 38,614.61. self_employed_health_insurance_earned_income_limit: 37_314.00 + +- name: With HI on, nondeductible draw SECA still keeps the draw out of line 5 + 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.contrib.ssa.owners_draw.include_hi: true + gov.contrib.ssa.owners_draw.half_seca_deduction: false + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + payroll_tax_gross_wages: 300_000 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + # 59,999.998 x 2.9% = 1,740.00 of draw HI, none of it deductible. + owners_draw_hi_tax: 1_740 + # Schedule C HI only: 36,940 x 2.9% / 2 = 535.63. + self_employment_tax_ald_person: 535.63 + # Line 5 is 40,000: 40,000 - 535.63. Counting the draw would give 39,795.89. + self_employed_health_insurance_earned_income_limit: 39_464.37 + +- name: With HI on and no OASDI room, the whole gross draw is in line 5 + 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.contrib.ssa.owners_draw.include_hi: true + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + payroll_tax_gross_wages: 300_000 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + owners_draw_oasdi_base: 0 + owners_draw_hi_tax: 1_740 + owners_draw_niit_exclusion: 64_970.22 + # (1,071.26 + 1,740.00) / 2 = 1,405.63. + self_employment_tax_ald_person: 1_405.63 + # Line 6: 40,000 / 104,970.22 = 0.3810605; 40,000 - 535.63 = 39,464.37. + # Main (draw left out of line 5) gives 38,594.37. + self_employed_health_insurance_earned_income_limit: 39_464.37 + +- name: A half phase-in counts the same half in line 5 as line 7 taxes + 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: 0.5 + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + owners_draw_gross: 32_485.11 + owners_draw_oasdi_base: 30_000 + owners_draw_niit_exclusion: 32_485.11 + # (5,651.82 + 30,000 x 12.4%) / 2 = 4,685.91. + self_employment_tax_ald_person: 4_685.91 + # 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 diff --git a/policyengine_us/tests/reforms/test_owners_draw.py b/policyengine_us/tests/reforms/test_owners_draw.py index deb6b2cb60b..f2da26f26ab 100644 --- a/policyengine_us/tests/reforms/test_owners_draw.py +++ b/policyengine_us/tests/reforms/test_owners_draw.py @@ -792,6 +792,25 @@ def test_generated_disabled_sehi_limit_equals_baseline(reform_variables, rows, r rates=(124, 29, 500), policy=_SEHI_WITNESS_POLICY, ) +# HI on with nondeductible draw SECA: line 7 holds none of the draw's HI, +# so line 5 omits the draw even though the whole draw bears HI. +@example( + rows=[(40_000, 0, 0, 64_970.22, 300_000, 0)], + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY | {"include_hi": True, "half_seca_deduction": False}, +) +# HI on with no OASDI room: the whole gross draw bears HI and is in line 5. +@example( + rows=[(40_000, 0, 0, 64_970.22, 300_000, 0)], + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY | {"include_hi": True}, +) +# A half phase-in with HI off: line 5 counts the same half line 7 taxes. +@example( + rows=SEHI_HAND_CASE, + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY | {"phase_in_share": 0.5}, +) def test_generated_sehi_limit_counts_seca_taxed_draw_in_line_5( reform_variables, rows, rates, policy ): From bed691b7ff86066710cc469a62c608abc309f447 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:17:36 +0000 Subject: [PATCH 5/6] Move the SEHI docs section below the cap paragraph it split The section sat above the paragraph that closes the cap-priority section. Also say the OASDI base is divided by the net-earnings factor (q by default), since an explicit net_earnings_factor overrides q. Co-Authored-By: Claude Opus 5.5 --- docs/contributed_reforms/owners_draw.md | 51 +++++++++++++------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/contributed_reforms/owners_draw.md b/docs/contributed_reforms/owners_draw.md index da627e2f39f..34dc304c919 100644 --- a/docs/contributed_reforms/owners_draw.md +++ b/docs/contributed_reforms/owners_draw.md @@ -96,19 +96,6 @@ Outputs include `owners_draw_active_income`, `owners_draw_planned_gross`, `owners_draw_net_earnings`, `owners_draw_oasdi_room`, `owners_draw_oasdi_base`, `owners_draw_oasdi_tax`, and `owners_draw_hi_tax`. -## Self-employed health insurance limit - -The reform also replaces `self_employed_health_insurance_earned_income_limit` -(Form 7206 line 13). Line 5 sums the profits of businesses subject to SE tax, -and line 6 apportions the deductible SE tax on line 7 to the plan business by -its share of line 5. With `half_seca_deduction: true`, line 5 adds -`owners_draw_niit_exclusion`: `draw_oasdi_base / q` with HI off, or the whole -actual gross draw D with HI 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. - For the current-law cap basis, the taxed draw is nonnegative and no larger than remaining room, adjusted earnings, or actual gross draw. Adding the draw cannot push otherwise capped covered earnings beyond C. Increasing the @@ -117,6 +104,21 @@ fixed. Already-SE income is removed before computing the draw, and disabled years retain baseline tax and deduction behavior. The `1` cap option intentionally permits earnings above C. +## Self-employed health insurance limit + +The reform also replaces +`self_employed_health_insurance_earned_income_limit` (Form 7206 line 13). +Line 5 sums the profits of businesses subject to SE tax, and line 6 +apportions the deductible SE tax on line 7 to the plan business by its share +of line 5. With `half_seca_deduction: true`, line 5 adds +`owners_draw_niit_exclusion`: `draw_oasdi_base` divided by the net-earnings +factor (q by default) with HI off, or the whole actual gross draw D with HI +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. + ## Benefit credit and active NIIT interaction `owners_draw_credited_earnings` exposes N_D when benefit credit is enabled, @@ -131,17 +133,18 @@ synchronize that path with the current tax cap. Mini-PIA integration and a complete annual-history ledger remain separate future work. `owners_draw_niit_exclusion` is the matching gross-profit exclusion for a -S.770 active-business NIIT option. With OASDI only it is -`draw_oasdi_base / q`: only profits whose adjusted earnings actually face -SECA are excluded. With HI enabled it is the entire actual gross draw D, -because N_D faces uncapped HI. Profits beyond OASDI room remain in the active -base when HI is off. The undrawn share and any floor-rejected planned draw -also remain. Existing already-SE gross K_active must separately be excluded -regardless of the OASDI cap because existing SE earnings face uncapped HI. -When both reforms are active, `s770_active_business_income` consumes this -calculated exclusion, including an explicit net-factor override. When owners -draw is disabled, before its start year, or uninstalled, the S.770 manual -draw hooks retain their original behavior. +S.770 active-business NIIT option. With OASDI only it is `draw_oasdi_base` +divided by the net-earnings factor (q by default): only profits whose +adjusted earnings actually face SECA are excluded. With HI enabled it is the +entire actual gross draw D, because N_D faces uncapped HI. Profits beyond +OASDI room remain in the active base when HI is off. The undrawn share and +any floor-rejected planned draw also remain. Existing already-SE gross +K_active must separately be excluded regardless of the OASDI cap because +existing SE earnings face uncapped HI. When both reforms are active, +`s770_active_business_income` consumes this calculated exclusion, including +an explicit net-factor override. When owners draw is disabled, before its +start year, or uninstalled, the S.770 manual draw hooks retain their +original behavior. The automatic joint route requires `material_participation_only: true`. Passive-inclusive draws remain supported by the standalone owners' draw From 8e10a070d9474f0df7090701d50065cd220101a7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 23:07:57 -0500 Subject: [PATCH 6/6] Test the SEHI limit with HI on and part of the draw in the OASDI room The round 3 review's witness: with HI on, line 5 holds the whole gross draw even when 13,060 of OASDI room remains. A mutant that counts only the OASDI-taxed part passed every earlier test and fails this one. Co-Authored-By: Claude Opus 5.5 --- ..._health_insurance_earned_income_limit.yaml | 25 +++++++++++++++++++ .../tests/reforms/test_owners_draw.py | 7 ++++++ 2 files changed, 32 insertions(+) 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 eaa66e33548..8a53763100e 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 @@ -207,6 +207,31 @@ # Main (draw left out of line 5) gives 38,594.37. self_employed_health_insurance_earned_income_limit: 39_464.37 +- name: With HI on and part of the draw in the OASDI room, the whole gross draw is in line 5 + 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.contrib.ssa.owners_draw.include_hi: true + gov.irs.payroll.social_security.cap: 200_000 + gov.simulation.aime_lookback_years: 0 + payroll_tax_gross_wages: 150_000 + self_employment_income: 40_000 + s_corp_income: 64_970.22 + output: + # Room 200,000 - 150,000 - 36,940 = 13,060 of the draw's 59,999.998. + owners_draw_oasdi_base: 13_060 + owners_draw_hi_tax: 1_740 + owners_draw_niit_exclusion: 64_970.22 + # (4,580.56 + 1,071.26 + 1,619.44 + 1,740.00) / 2 = 4,505.63. + self_employment_tax_ald_person: 4_505.63 + # Line 5: 104,970.22; line 6: 0.3810605; 40,000 - 1,716.92 = 38,283.08. + # Counting only the OASDI-taxed 14,141.85 would give 36,671.24; main 35,494.37. + self_employed_health_insurance_earned_income_limit: 38_283.08 + - name: A half phase-in counts the same half in line 5 as line 7 taxes period: 2027 absolute_error_margin: 0.02 diff --git a/policyengine_us/tests/reforms/test_owners_draw.py b/policyengine_us/tests/reforms/test_owners_draw.py index f2da26f26ab..f8e483ede75 100644 --- a/policyengine_us/tests/reforms/test_owners_draw.py +++ b/policyengine_us/tests/reforms/test_owners_draw.py @@ -805,6 +805,13 @@ def test_generated_disabled_sehi_limit_equals_baseline(reform_variables, rows, r rates=(124, 29, 500), policy=_SEHI_WITNESS_POLICY | {"include_hi": True}, ) +# HI on with 13,060 of OASDI room: line 5 still holds the whole gross draw, +# not just the 14,141.85 the OASDI tax reaches. +@example( + rows=[(40_000, 0, 0, 64_970.22, 150_000, 0)], + rates=(124, 29, 500), + policy=_SEHI_WITNESS_POLICY | {"include_hi": True}, +) # A half phase-in with HI off: line 5 counts the same half line 7 taxes. @example( rows=SEHI_HAND_CASE,