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..c1a08655f5b --- /dev/null +++ b/changelog.d/owners-draw-sehi-line5.fixed.md @@ -0,0 +1 @@ +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..34dc304c919 100644 --- a/docs/contributed_reforms/owners_draw.md +++ b/docs/contributed_reforms/owners_draw.md @@ -104,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, @@ -118,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 diff --git a/policyengine_us/reforms/ssa/owners_draw.py b/policyengine_us/reforms/ssa/owners_draw.py index 604f324a271..8d676167dda 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,52 @@ 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 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) + ) + all_business_profits = ( + plan_business_profit + + max_(0, person("farm_operations_income", period)) + + max_(0, person("partnership_self_employment_net_earnings", period)) + + draw_profits_in_line_7 + ) + plan_business_share = np.divide( + plan_business_profit, + all_business_profits, + out=np.zeros_like(all_business_profits), + where=all_business_profits > 0, + ) + self_employment_tax_deduction = ( + person("self_employment_tax_ald_person", period) * plan_business_share + ) + retirement_plan_deduction = person( + "self_employed_pension_contribution_ald_person", period + ) + return max_( + 0, + plan_business_profit + - self_employment_tax_deduction + - retirement_plan_deduction, + ) + class ss_aime(baseline_aime): def formula(person, period, parameters): supplied = person("ss_aime_input", period) @@ -316,6 +365,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..8a53763100e --- /dev/null +++ b/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_health_insurance_earned_income_limit.yaml @@ -0,0 +1,255 @@ +# Form 7206 lines 4-13 with the owners' draw reform on. Line 5 counts the +# 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. + +- 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 + +- 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 + +- 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: 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 + 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 b1b51eb7819..f8e483ede75 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,178 @@ 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]) + + +_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, 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, +) +# 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}, +) +# 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, + 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 +): + """Form 7206 lines 4-13 with the SECA-taxed draw profits in line 5.""" + inputs = _sehi_inputs(rows) + 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( + BASELINE_VARIABLES, + _parameters(**policy), + { + **inputs, + "self_employment_tax_ald_person": enabled["self_employment_tax_ald_person"], + }, + [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"]) + + 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), + rtol=1e-9, + atol=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) + no_draw_term = draw_term == 0 + np.testing.assert_allclose( + limit[no_draw_term], main[SEHI_LIMIT][no_draw_term], atol=1e-6 + ) + + @contextmanager def _paused_collection(): """Pause automatic collection, then restore the setting found on entry."""