diff --git a/changelog.d/fix-se-pension-earned-income-limit.fixed.md b/changelog.d/fix-se-pension-earned-income-limit.fixed.md new file mode 100644 index 00000000000..a6a53eca924 --- /dev/null +++ b/changelog.d/fix-se-pension-earned-income-limit.fixed.md @@ -0,0 +1 @@ +Limit the self-employed SEP, SIMPLE and qualified plan deduction to the Pub. 560 maximum for a SEP or a qualified defined contribution plan, based on Schedule C net profit less its share of the deductible part of self-employment tax, so the deduction can no longer exceed that earned income. A SIMPLE IRA can allow slightly more than this limit in a narrow band, which the model doesn't yet capture. diff --git a/docs/contributed_reforms/owners_draw.md b/docs/contributed_reforms/owners_draw.md index 34dc304c919..34d5019be6d 100644 --- a/docs/contributed_reforms/owners_draw.md +++ b/docs/contributed_reforms/owners_draw.md @@ -119,6 +119,11 @@ profits beyond the OASDI room with HI off stay out of line 5. With omits the draw and the limit follows the baseline formula. Disabled years use the baseline formula. +The reform also replaces `self_employed_pension_earned_income` (Pub. 560 +Deduction Worksheet for Self-Employed, step 3). Step 2 takes the Schedule C +business's share of the deductible SE tax, split by net profit as above, and +the same draw profits join the split. + ## Benefit credit and active NIIT interaction `owners_draw_credited_earnings` exposes N_D when benefit credit is enabled, diff --git a/policyengine_us/parameters/gov/irs/gross_income/retirement_contributions/limit/profit_sharing_deduction_rate.yaml b/policyengine_us/parameters/gov/irs/gross_income/retirement_contributions/limit/profit_sharing_deduction_rate.yaml new file mode 100644 index 00000000000..ebb046eb324 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/gross_income/retirement_contributions/limit/profit_sharing_deduction_rate.yaml @@ -0,0 +1,16 @@ +description: The US limits the deduction for employer contributions to a SEP or profit-sharing plan to this share of participants' compensation. +values: + 2002-01-01: 0.25 +metadata: + unit: /1 + period: year + label: SEP and profit-sharing plan employer deduction limit rate + reference: + - title: 26 U.S. Code § 404(a)(3)(A)(i)(I) and (h)(1)(C) - Profit-sharing trusts and simplified employee pensions + href: https://www.law.cornell.edu/uscode/text/26/404#a_3_A_i_I + - title: Economic Growth and Tax Relief Reconciliation Act of 2001, Pub. L. 107-16, § 616 (15 to 25 percent, years beginning after 2001) + href: https://www.congress.gov/107/plaws/publ16/PLAW-107publ16.pdf#page=66 + - title: IRS Publication 560 (2025), Rate Table for Self-Employed + href: https://www.irs.gov/pub/irs-prior/p560--2025.pdf#page=40 + - title: IRS Publication 560 (2022), Rate Table for Self-Employed + href: https://www.irs.gov/pub/irs-prior/p560--2022.pdf#page=35 diff --git a/policyengine_us/reforms/ssa/owners_draw.py b/policyengine_us/reforms/ssa/owners_draw.py index 8d676167dda..e41ba70bd03 100644 --- a/policyengine_us/reforms/ssa/owners_draw.py +++ b/policyengine_us/reforms/ssa/owners_draw.py @@ -18,6 +18,10 @@ 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.household.expense.retirement.self_employed_pension_earned_income import ( + self_employed_pension_earned_income as baseline_pension_earned_income, + pension_plan_earned_income, +) from policyengine_us.variables.gov.ssa.social_security.ss_aime import ( ss_aime as baseline_aime, _compute_aime, @@ -333,6 +337,25 @@ def formula(person, period, parameters): - retirement_plan_deduction, ) + class self_employed_pension_earned_income(baseline_pension_earned_income): + def formula(person, period, parameters): + if not _enabled(period, parameters): + return baseline_pension_earned_income.formula( + person, period, parameters + ) + # Pub. 560 step 2 takes the Schedule C business's share of + # Schedule 1 line 15, split by net profit as on Form 7206 lines 5 + # to 8. As in the health insurance limit above, the draw profits + # whose SECA is in line 15 join the profits it is split over. + draw_profits_in_line_15 = where( + parameters(period).gov.contrib.ssa.owners_draw.half_seca_deduction, + person("owners_draw_niit_exclusion", period), + 0, + ) + return pension_plan_earned_income( + person, period, other_line_5_profits=draw_profits_in_line_15 + ) + class ss_aime(baseline_aime): def formula(person, period, parameters): supplied = person("ss_aime_input", period) @@ -366,6 +389,7 @@ def apply(self): 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(self_employed_pension_earned_income) self.update_variable(ss_aime) return reform diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml index 15d986812cd..715d4dbcc83 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml @@ -10,7 +10,9 @@ roth_ira_contributions_desired: 1_000 roth_401k_contributions_desired: 1_000 roth_403b_contributions_desired: 1_000 - self_employment_income: 1_000 + # Enough profit for the self-employed plan limit (earned income of + # 9,293.52) to allow the full 1,000. + self_employment_income: 10_000 self_employed_pension_contributions_desired: 1_000 able_contributions_person: 100 retirement_distributions: 1_000 @@ -36,7 +38,9 @@ - name: Self-employed pension contributions count after applying the annual additions cap period: 2026 input: - self_employment_income: 100_000 + # 20% of earned income plus the 24,500 deferral limit exceeds the 72,000 + # annual additions limit at this profit. + self_employment_income: 500_000 self_employed_pension_contributions_desired: 100_000 output: self_employed_pension_contributions: 72_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml index ab97a8aa061..b3dc39c9877 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml @@ -22,7 +22,7 @@ output: # Schedule 1 line 15: 20,000 x 0.9235 x 0.153 / 2 = 1,412.955. self_employment_tax_ald_person: 1_412.955 - # Schedule 1 line 16 (within the $66,000 annual additions limit). + # Schedule 1 line 16: 3,000, within the Pub. 560 maximum of 18,587.045. self_employed_pension_contribution_ald_person: 3_000 # Lines 8 to 10: 20,000 - 1,412.955 - 3,000. self_employed_health_insurance_earned_income_limit: 15_587.045 @@ -94,7 +94,7 @@ output: self_employed_health_insurance_earned_income_limit: 0 -- name: Case 9, a plan deduction as large as net profit leaves no limit. +- name: Case 9, a plan deduction as large as earned income leaves no limit. period: 2023 absolute_error_margin: 0.01 input: @@ -103,9 +103,10 @@ output: # Schedule 1 line 15: 20,000 x 0.9235 x 0.153 / 2 = 1,412.955. self_employment_tax_ald_person: 1_412.955 - # Schedule 1 line 16: the model caps the plan deduction at net profit. - self_employed_pension_contribution_ald_person: 20_000 - # Lines 8 to 13: max(0, 20,000 - 1,412.955 - 20,000). + # Schedule 1 line 16: the Pub. 560 maximum, which elective deferrals let + # reach all of earned income: 20,000 - 1,412.955 = 18,587.045. + self_employed_pension_contribution_ald_person: 18_587.045 + # Lines 8 to 13: max(0, 20,000 - 1,412.955 - 18,587.045). self_employed_health_insurance_earned_income_limit: 0 - name: Case 10, SSTB and non-SSTB Schedule C income are one plan business, so an SSTB loss reduces line 4. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.yaml new file mode 100644 index 00000000000..8c61e21b77c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.yaml @@ -0,0 +1,55 @@ +- name: Case 1, the deduction cannot make AGI negative when Schedule C profit is the only income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 20_000 + output: + # Half of self-employment tax: 20,000 x 92.35% x 15.3% / 2 = 1,412.955. + # Pub. 560 Deduction Worksheet step 3: 20,000 - 1,412.955 = 18,587.045, + # the most the plan deduction can be (26 U.S.C. 404(a)(8)(C)). + self_employment_tax_ald: 1_412.96 + self_employed_pension_contribution_ald_person: [18_587.05] + self_employed_pension_contribution_ald: 18_587.05 + adjusted_gross_income: 0 + +- name: Case 2, contributions supplied directly are still limited. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 20_000 + self_employed_pension_contributions: 20_000 + output: + self_employed_pension_contribution_ald_person: [18_587.05] + adjusted_gross_income: 0 + +- name: Case 3, a married couple's limits are each spouse's own. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 200_000 + self_employed_pension_contributions_desired: 100_000 + person2: + age: 40 + self_employment_income: 30_000 + self_employed_pension_contributions_desired: 30_000 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + output: + # Person 1: 20% x 186,403.65 (step 3 in the Pub. 560 2025 example) + + # 23,500 = 60,780.73. Person 2: (27,880.5675 + 23,500) / 2 = 25,690.28. + self_employed_pension_contribution_ald_person: [60_780.73, 25_690.28] + self_employed_pension_contribution_ald: 86_471.01 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml index 4110518fd83..0e714dc6cdd 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml @@ -18,11 +18,14 @@ - name: Self-employed pension ALD uses SSTB income period: 2024 + absolute_error_margin: 0.01 input: sstb_self_employment_income: 8_000 self_employed_pension_contributions_desired: 9_000 output: - self_employed_pension_contribution_ald_person: 8_000 + # Earned income: 8,000 - 7,388 x 15.3% / 2 = 7,434.82, all of which + # elective deferrals can cover (Pub. 560 Deduction Worksheet steps 9-19). + self_employed_pension_contribution_ald_person: 7_434.82 - name: Loss ALD includes SSTB self-employment losses period: 2024 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief.yaml index 4471bfc9683..57144640fae 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/property/homeowner_renter_relief/ut_homeowner_renter_relief.yaml @@ -215,6 +215,7 @@ - name: Case 12, nontaxable income uses self-employed pension contributions. period: 2026 + absolute_error_margin: 0.01 input: people: person1: @@ -230,5 +231,7 @@ members: [person1] state_code: UT output: - self_employed_pension_contributions: 72_000 - ut_homeowner_renter_relief_nontaxable_income: 72_000 + # Pub. 560 worksheet: 20% x 92,935.23 of earned income + 24,500 of + # deferrals + the 8,000 catch-up at age 66. + self_employed_pension_contributions: 51_087.05 + ut_homeowner_renter_relief_nontaxable_income: 51_087.05 diff --git a/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_contribution_limit.yaml b/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_contribution_limit.yaml index 885e4783141..5428b074081 100644 --- a/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_contribution_limit.yaml +++ b/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_contribution_limit.yaml @@ -1,29 +1,50 @@ +# Expected values follow the Pub. 560 Deduction Worksheet for Self-Employed +# (2025: https://www.irs.gov/pub/irs-prior/p560--2025.pdf#page=39; 2022: +# https://www.irs.gov/pub/irs-prior/p560--2022.pdf#page=34), with the 25% plan +# rate (step 4 rate 0.2) and, unless the case says otherwise, elective +# deferrals (step 9) and catch-up contributions (step 17) equal to the +# person's remaining section 402(g) room. Step 3 is Schedule C net profit less +# its share of the deductible part of self-employment tax: half of 15.3% (12.4% +# only up to the Social Security wage base less wages) of 92.35% of all net +# profit, split by each business's share of the positive net profits. With more +# than one business, step 2 is that share, as on Form 7206 lines 5 to 8; the +# worksheet as printed enters all of Schedule 1 line 15. + - name: Self-employed pension contributions are limited by the 2026 annual additions dollar limit. period: 2026 + absolute_error_margin: 0.01 input: people: person1: - self_employment_income: 100_000 + self_employment_income: 500_000 self_employed_pension_contributions_desired: 100_000 output: + # Step 3: 500,000 - (184,500 x 12.4% + 461,750 x 2.9%) / 2 = 481,865.63. + # Step 5: 20% x 481,865.63 = 96,373.13; step 6 caps it at 360,000 x 25% = + # 90,000 (step 7). Steps 13 + 15 stop at the $72,000 dollar limit. self_employed_pension_contribution_limit: 72_000 self_employed_pension_contributions: 72_000 self_employed_pension_contribution_ald_person: 72_000 - name: Self-employed pension contributions are also limited by self-employment compensation. period: 2026 + absolute_error_margin: 0.01 input: people: person1: self_employment_income: 50_000 self_employed_pension_contributions_desired: 100_000 output: - self_employed_pension_contribution_limit: 50_000 - self_employed_pension_contributions: 50_000 - self_employed_pension_contribution_ald_person: 50_000 + # Step 3: 50,000 - 46,175 x 15.3% / 2 = 46,467.61. Step 7: 9,293.52. + # Step 9: 24,500. Step 12: (46,467.61 - 24,500) / 2 = 10,983.81. + # Steps 13 + 15: 9,293.52 + 24,500 = 33,793.52. + self_employed_pension_contribution_limit: 33_793.52 + self_employed_pension_contributions: 33_793.52 + self_employed_pension_contribution_ald_person: 33_793.52 -- name: Unrelated employee elective deferrals do not reduce the self-employed pension limit. +- name: Elective deferrals to an employer's 401(k) use up the deferral room, leaving the 20% SEP and profit-sharing rate. period: 2026 + absolute_error_margin: 0.01 input: people: person1: @@ -35,11 +56,16 @@ output: elective_deferral_limit: 35_750 traditional_401k_contributions: 35_750 - self_employed_pension_contribution_limit: 72_000 - self_employed_pension_contributions: 72_000 + # Wages of 100,000 leave 84,500 of the 184,500 wage base. Step 3: + # 100,000 - (84,500 x 12.4% + 92,350 x 2.9%) / 2 = 93,421.93. + # No deferral room is left, so the limit is step 7: 20% x 93,421.93. + self_employed_pension_earned_income: 93_421.93 + self_employed_pension_contribution_limit: 18_684.39 + self_employed_pension_contributions: 18_684.39 - name: Annual additions cap does not apply to IRA contributions. period: 2026 + absolute_error_margin: 0.01 input: people: person1: @@ -50,16 +76,292 @@ output: ira_contribution_limit: 8_600 traditional_ira_contributions: 8_600 - self_employed_pension_contribution_limit: 72_000 - self_employed_pension_contributions: 72_000 + # Step 3: 100,000 - 92,350 x 15.3% / 2 = 92,935.23. Steps 13 + 15: + # 18,587.05 + 24,500 = 43,087.05. Step 18: the 8,000 catch-up. + self_employed_pension_contribution_limit: 51_087.05 + self_employed_pension_contributions: 51_087.05 - name: Self-employed pension contributions use desired input. period: 2026 + absolute_error_margin: 0.01 input: people: person1: self_employment_income: 100_000 self_employed_pension_contributions_desired: 100_000 output: - self_employed_pension_contribution_limit: 72_000 - self_employed_pension_contributions: 72_000 + self_employed_pension_contribution_limit: 43_087.05 + self_employed_pension_contributions: 43_087.05 + +- name: Pub. 560 2025 example net profit, with elective deferrals at the 402(g) limit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 200_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Steps 1 to 3 as in the Pub. 560 example: 200,000 - 13,596.35 = 186,403.65. + self_employment_tax_ald_person: 13_596.35 + self_employed_pension_earned_income: 186_403.65 + # Step 7: 20% x 186,403.65 = 37,280.73. Step 9: 23,500. Step 10: 46,500. + # Step 12: (186,403.65 - 23,500) / 2 = 81,451.83. Step 13: 37,280.73. + # Step 15: 23,500. Step 19: 60,780.73. + self_employed_pension_contribution_limit: 60_780.73 + self_employed_pension_contribution_ald_person: 60_780.73 + +- name: Pub. 560 2025 example net profit, with no elective deferral room left (SEP and profit-sharing path). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 23_500 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 200_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Wages of 23,500 leave 152,600 of the 176,100 wage base. Step 2: + # (152,600 x 12.4% + 184,700 x 2.9%) / 2 = 12,139.35. + self_employed_pension_earned_income: 187_860.65 + # Steps 9 to 20 do not apply: steps 7 and 8 give 20% x 187,860.65. + self_employed_pension_contribution_limit: 37_572.13 + +- name: Catch-up contributions at age 55 add to the limit (steps 16 to 18). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 55 + self_employment_income: 200_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Steps 13 and 15 as in the age 40 case (60,780.73). Step 16: + # 186,403.65 - 37,280.73 - 23,500 = 125,622.92. Step 18: 7,500. + self_employed_pension_contribution_limit: 68_280.73 + +- name: The age 60 to 63 catch-up amount applies from 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 61 + self_employment_income: 200_000 + self_employed_pension_contributions_desired: 100_000 + output: + self_employed_pension_contribution_limit: 72_030.73 + +- name: Catch-up contributions add to the dollar limit when it binds. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 55 + self_employment_income: 500_000 + self_employed_pension_contributions_desired: 100_000 + output: + self_employed_pension_contribution_limit: 80_000 + +- name: Catch-up contributions can't take the limit above earned income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 55 + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 30_000 + output: + # Step 3: 20,000 - 18,470 x 15.3% / 2 = 18,587.05. Step 9 at 18,587.05 + # leaves steps 11 to 13 at zero, and step 16 is 18,587.05 - 18,587.05 = 0, + # so step 18 adds none of the 7,500 catch-up: step 19 is 18,587.05 + # (sections 404(a)(8)(C) and 414(v)(2)(A)(ii)). + self_employed_pension_earned_income: 18_587.05 + self_employed_pension_contribution_limit: 18_587.05 + self_employed_pension_contributions: 18_587.05 + +- name: With low profit, deferrals can reach all of earned income but no more. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 20_000 + output: + # Step 3: 20,000 - 18,470 x 15.3% / 2 = 18,587.05. Step 9 at 18,587.05 + # leaves steps 11 to 13 at zero, so step 19 equals step 3. + self_employed_pension_earned_income: 18_587.05 + self_employed_pension_contribution_limit: 18_587.05 + self_employed_pension_contributions: 18_587.05 + +- name: The 100%-of-compensation limit binds when deferrals are large relative to profit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 30_000 + self_employed_pension_contributions_desired: 30_000 + output: + # Step 3: 30,000 - 27,705 x 15.3% / 2 = 27,880.57. Step 7: 5,576.11. + # Step 12: (27,880.57 - 23,500) / 2 = 2,190.28, the smallest, so step 13 + # is 2,190.28. Step 14: 25,690.28. Step 15: 23,500. Step 19: 25,690.28. + self_employed_pension_earned_income: 27_880.57 + self_employed_pension_contribution_limit: 25_690.28 + +- name: Deferrals to an employer's plan use the regular limit first and leave the catch-up room. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 52 + employment_income: 50_000 + traditional_401k_contributions_desired: 20_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 100_000 + output: + # 402(g) limit 24,500 + 8,000 catch-up = 32,500, less 20,000 of deferrals: + # 4,500 of regular room and the 8,000 catch-up. Step 3: 92,935.23. + # Steps 13 + 15: 18,587.05 + 4,500. Step 18: 8,000. + self_employed_pension_earned_income: 92_935.23 + self_employed_pension_contribution_limit: 31_087.05 + +- name: Deferrals to another employer's 403(b) plan use up the deferral room too. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 60_000 + traditional_403b_contributions_desired: 20_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Section 402(g) aggregates 401(k) and 403(b) deferrals: 24,500 - 20,000 + # leaves 4,500 of room. Wages of 60,000 leave 124,500 of the wage base, so + # step 3 is 100,000 - 92,350 x 15.3% / 2 = 92,935.23. Steps 13 + 15: + # 18,587.05 + 4,500 = 23,087.05. + self_employed_pension_earned_income: 92_935.23 + self_employed_pension_contribution_limit: 23_087.05 + +- name: With deferral room close to earned income, the limit is step 12, which a SIMPLE IRA can exceed. + period: 2023 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 17_500 + self_employment_income: 5_600 + self_employed_pension_contributions_desired: 10_000 + output: + # 402(g) limit 22,500 less 17,500 deferred leaves 5,000 of room. Schedule + # SE line 4: 5,600 x 0.9235 = 5,171.60. Line 15: 5,171.60 x 15.3% / 2 = + # 395.63. Step 3: 5,600 - 395.63 = 5,204.37. Step 7: 867.40. Step 12: + # (5,204.37 - 5,000) / 2 = 102.19, the smallest, so steps 13 + 15 are + # (5,204.37 + 5,000) / 2 = 5,102.19. + # A SIMPLE IRA would allow more (Pub. 560 (2025), page 15; section + # 415(a)(2)): the 5,000 deferral, under its 15,500 limit for 2023, plus a + # 3% match of Schedule SE line 4 before the contributions (0.03 x + # 5,171.60 = 155.15), 5,155.15 in all. This limit cuts that by 52.96. + self_employed_pension_earned_income: 5_204.37 + self_employed_pension_contribution_limit: 5_102.19 + self_employed_pension_contributions: 5_102.19 + +- name: Farm earnings take part of the self-employment tax deduction, which is split by net profit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 60_000 + farm_operations_income: 40_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Self-employment tax deduction: 92,350 x 15.3% / 2 = 7,064.78, of which + # 60% (4,238.87) is attributable to the Schedule C business. + self_employment_tax_ald_person: 7_064.78 + self_employed_pension_earned_income: 55_761.14 + # Steps 13 + 15: 20% x 55,761.14 + 23,500 = 34,652.23. + self_employed_pension_contribution_limit: 34_652.23 + +- name: A plan under the Schedule C business counts only that business's earned income, not a partnership's. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 1_000 + partnership_self_employment_net_earnings: 50_000 + self_employed_pension_contributions_desired: 5_000 + output: + # Schedule 1 line 15: 51,000 x 0.9235 x 0.153 / 2 = 3,603.04, of which + # 1 / 51 (70.65) is the Schedule C business's. Step 3: 1,000 - 70.65 = + # 929.35, which elective deferrals can cover in full. Pub. 560 (2025), + # page 7: with one plan, only that business's earned income counts. + self_employment_tax_ald_person: 3_603.04 + self_employed_pension_earned_income: 929.35 + self_employed_pension_contribution_limit: 929.35 + self_employed_pension_contributions: 929.35 + +- name: Net profit under the self-employment tax threshold is all earned income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 400 + self_employed_pension_contributions_desired: 1_000 + output: + # 92.35% of 400 is under $400, so there is no self-employment tax. + self_employment_tax_ald_person: 0 + self_employed_pension_earned_income: 400 + self_employed_pension_contribution_limit: 400 + +- name: Pub. 560 2022 example net profit, with elective deferrals at the 402(g) limit. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 200_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Step 2: (147,000 x 12.4% + 184,700 x 2.9%) / 2 = 11,792.15. The Pub. + # 560 (2022) example's step 21, 0.078 x step 3, is 14,680. + self_employment_tax_ald_person: 11_792.15 + self_employed_pension_earned_income: 188_207.85 + # Step 7: 37,641.57. Step 9: 20,500. Steps 13 + 15: 58,141.57, under the + # $61,000 dollar limit. + self_employed_pension_contribution_limit: 58_141.57 + +- name: A Schedule C loss allows no contribution. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: -5_000 + self_employed_pension_contributions_desired: 5_000 + output: + self_employed_pension_earned_income: 0 + self_employed_pension_contribution_limit: 0 + self_employed_pension_contributions: 0 + self_employed_pension_contribution_ald_person: 0 diff --git a/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_pension_earned_income.yaml b/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_pension_earned_income.yaml new file mode 100644 index 00000000000..174577d748c --- /dev/null +++ b/policyengine_us/tests/policy/contrib/ssa/owners_draw_self_employed_pension_earned_income.yaml @@ -0,0 +1,47 @@ +# Pub. 560 Deduction Worksheet for Self-Employed, step 3, with the owners' +# draw reform on. Step 2 takes the Schedule C business's share of Schedule 1 +# line 15, split by net profit as on Form 7206 lines 5 to 8. The reform adds +# the gross draw profits that bear SECA when half_seca_deduction puts that +# SECA into line 15, as it does for the self-employed health insurance limit. +# Synthetic caps follow owners_draw.yaml; q = 0.9235. Hand-computed. + +- name: The draw's profits join the profits line 15 is split over + 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 + output: + # 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 + # Share 40,000 / 104,970.22 = 0.3810605; step 2 = 2,494.39; step 3 = + # 40,000 - 2,494.39. The whole line 15 would give 33,454.09. + self_employed_pension_earned_income: 37_505.61 + +- name: Nondeductible draw SECA keeps the draw out of the split + 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: + # Only the Schedule C SE tax is deductible: 5,651.82 / 2 = 2,825.91. + self_employment_tax_ald_person: 2_825.91 + # The Schedule C business is the only profit in the split: 40,000 - + # 2,825.91. + self_employed_pension_earned_income: 37_174.09 diff --git a/policyengine_us/tests/reforms/test_owners_draw.py b/policyengine_us/tests/reforms/test_owners_draw.py index f8e483ede75..2413846fedc 100644 --- a/policyengine_us/tests/reforms/test_owners_draw.py +++ b/policyengine_us/tests/reforms/test_owners_draw.py @@ -39,6 +39,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, ) +from policyengine_us.variables.household.expense.retirement.self_employed_pension_earned_income import ( + self_employed_pension_earned_income, +) from policyengine_us.variables.gov.irs.tax.payroll.social_security.taxable_earnings_for_social_security import ( taxable_earnings_for_social_security, ) @@ -70,6 +73,7 @@ self_employment_tax, self_employment_tax_ald_person, self_employed_health_insurance_earned_income_limit, + self_employed_pension_earned_income, ) } PERIOD = period("2027") diff --git a/policyengine_us/tests/unit/test_self_employed_pension_limit_properties.py b/policyengine_us/tests/unit/test_self_employed_pension_limit_properties.py new file mode 100644 index 00000000000..670d4b06637 --- /dev/null +++ b/policyengine_us/tests/unit/test_self_employed_pension_limit_properties.py @@ -0,0 +1,360 @@ +"""Properties of the self-employed SEP, SIMPLE and qualified plan deduction limit. + +A self-employed person's plan compensation is earned income under 26 U.S.C. +401(c)(2), which is net of the deductible part of self-employment tax and of +the plan deduction itself, and 404(a)(8)(C) caps the deduction at that earned +income before the plan deduction. IRS Publication 560 turns this into the +Deduction Worksheet for Self-Employed (2025 edition, page 39). The model's +single input does not say which plan the person has, so the model's limit is +the worksheet's largest total over the elective deferrals and catch-up +contributions the person could still make (`self_employed_pension_contribution_limit`). + +The reference below runs worksheet steps 4 to 19 literally, with the IRS's +published dollar amounts rather than model parameters, and takes the largest +step 19 over a grid of elective deferrals (step 9) and catch-up contributions +(step 17). Schedule 1 line 15 is computed here from Schedule SE with the +published wage bases, and step 2 is Schedule C's share of it, split by net +profit as on Form 7206 lines 5 to 8. Hypothesis draws batches of single filers +across three years, and each batch runs as one vectorized simulation. For +every person and year: + +1. Line 15 equals half of the Schedule SE tax, the limit equals the worksheet + maximum, and step 3 equals Schedule C net profit less step 2. +2. The deduction is min(desired contributions, limit), is at most earned + income (404(a)(8)(C)), which is at most Schedule C net profit, and is at + most the dollar limit plus the person's catch-up amount, which is all + 414(v)(3)(A) puts outside 415(c). +3. For a filer whose only income is Schedule C profit, AGI is never negative + when the profit is not, because the deduction is at most profit less line + 15. + +A deterministic grid checks, at its points, that the limit rises with Schedule +C profit (it dips just past $433 of profit, where self-employment tax starts) +and falls as deferrals to an employer's plan use up the room. + +The model computes in float32, so comparisons allow a relative error of 1e-6 +on top of the $0.01 tolerance. +""" + +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 + +TOLERANCE = 0.01 +RTOL = 1e-6 +# Plan contribution rate: 26 U.S.C. 404(a)(3)(A)(i)(I) and 404(h)(1)(C). +PLAN_RATE = 0.25 +# Published amounts: IRS Notices 2021-61 (2022), 2024-80 (2025) and 2025-67 +# (2026); section 414(v)(2)(E) for ages 60 to 63 from 2025. +PUBLISHED = { + 2022: dict( + annual_additions=61_000, + compensation_limit=305_000, + deferral=20_500, + catch_up=6_500, + catch_up_60_to_63=6_500, + ), + 2025: dict( + annual_additions=70_000, + compensation_limit=350_000, + deferral=23_500, + catch_up=7_500, + catch_up_60_to_63=11_250, + ), + 2026: dict( + annual_additions=72_000, + compensation_limit=360_000, + deferral=24_500, + catch_up=8_000, + catch_up_60_to_63=11_250, + ), +} +YEARS = list(PUBLISHED) + +# Schedule SE, as test data rather than model parameters. +# 26 U.S.C. 1402(a)(12): net earnings are 92.35% of net profit. +NET_EARNINGS_SHARE = 0.9235 +# 26 U.S.C. 1402(b)(2): no net earnings below $400. +NET_EARNINGS_EXEMPTION = 400 +# 26 U.S.C. 1401(a) and (b). +SOCIAL_SECURITY_RATE = 0.124 +MEDICARE_RATE = 0.029 +# Contribution and benefit bases, https://www.ssa.gov/oact/cola/cbb.html. +WAGE_BASE = {2022: 147_000, 2025: 176_100, 2026: 184_500} + + +def schedule_se_deduction(year, net_profit, wages): + """Schedule 1 line 15: half of Schedule SE self-employment tax.""" + net_earnings = net_profit * NET_EARNINGS_SHARE + net_earnings = np.where(net_earnings < NET_EARNINGS_EXEMPTION, 0, net_earnings) + social_security_room = np.maximum(WAGE_BASE[year] - wages, 0) + tax = SOCIAL_SECURITY_RATE * np.minimum( + net_earnings, social_security_room + ) + MEDICARE_RATE * np.maximum(net_earnings, 0) + return tax / 2 + + +def close(actual, expected, message): + np.testing.assert_allclose( + actual, expected, rtol=RTOL, atol=TOLERANCE, err_msg=message + ) + + +def at_most(actual, bound): + return np.all(actual <= bound + TOLERANCE + RTOL * np.abs(bound)) + + +def catch_up_amount(year, age): + amounts = PUBLISHED[year] + return np.select( + [age < 50, (age >= 60) & (age <= 63)], + [0, amounts["catch_up_60_to_63"]], + amounts["catch_up"], + ) + + +def worksheet_step_19(step_3, deferrals, catch_up, amounts): + """Pub. 560 Deduction Worksheet for Self-Employed, steps 4 to 19.""" + step_4 = PLAN_RATE / (1 + PLAN_RATE) # Rate Table: 25% -> 0.2 + step_5 = step_3 * step_4 + step_6 = amounts["compensation_limit"] * PLAN_RATE + step_7 = np.minimum(step_5, step_6) + step_8 = amounts["annual_additions"] + step_9 = deferrals + step_10 = step_8 - step_9 + step_11 = step_3 - step_9 + step_12 = step_11 / 2 + step_13 = np.minimum(np.minimum(step_7, step_10), step_12) + step_14 = step_3 - step_13 + step_15 = np.minimum(step_9, step_14) + step_16 = step_14 - step_15 + step_18 = np.minimum(step_16, catch_up) + with_deferrals = step_13 + step_15 + step_18 + # Without elective deferrals the worksheet skips to step 21. + without_deferrals = np.minimum(step_7, step_8) + return np.where((deferrals > 0) | (catch_up > 0), with_deferrals, without_deferrals) + + +def worksheet_maximum(step_3, regular_room, catch_up_room, amounts, points=41): + """Largest step 19 over deferrals and catch-up contributions in their room.""" + best = np.zeros_like(step_3) + for d, c in itertools.product(np.linspace(0, 1, points), repeat=2): + # A deferral can't exceed compensation, so the grid stops at step 3. + deferrals = d * np.minimum(regular_room, step_3) + catch_up = c * catch_up_room + best = np.maximum(best, worksheet_step_19(step_3, deferrals, catch_up, amounts)) + return best + + +def every_year(value): + return {year: value for year in YEARS} + + +def build_situation(people_inputs): + people, tax_units, households = {}, {}, {} + for i, inputs in enumerate(people_inputs): + name = f"p{i}" + people[name] = {variable: every_year(v) for variable, v in inputs.items()} + tax_units[f"t{i}"] = {"members": [name]} + households[f"h{i}"] = {"members": [name], "state_code": every_year("TX")} + return {"people": people, "tax_units": tax_units, "households": households} + + +def column(people_inputs, variable): + return np.array([p.get(variable, 0) for p in people_inputs], dtype=float) + + +def reference(sim, people_inputs, year): + amounts = PUBLISHED[year] + age = column(people_inputs, "age") + schedule_c = np.maximum( + column(people_inputs, "self_employment_income") + + column(people_inputs, "sstb_self_employment_income"), + 0, + ) + others = np.maximum( + column(people_inputs, "farm_operations_income"), 0 + ) + np.maximum(column(people_inputs, "partnership_self_employment_net_earnings"), 0) + share = np.divide( + schedule_c, + schedule_c + others, + out=np.zeros_like(schedule_c), + where=schedule_c + others > 0, + ) + # Schedule 1 line 15 from Schedule SE; step 2 is Schedule C's share. + net_profit = ( + column(people_inputs, "self_employment_income") + + column(people_inputs, "sstb_self_employment_income") + + column(people_inputs, "farm_operations_income") + + column(people_inputs, "partnership_self_employment_net_earnings") + ) + line_15 = schedule_se_deduction( + year, net_profit, column(people_inputs, "employment_income") + ) + step_2 = line_15 * share + step_3 = np.maximum(schedule_c - step_2, 0) + # Section 402(g) room left by deferrals to an employer's plan, which use + # the regular limit first. + catch_up = catch_up_amount(year, age) + deferral_limit = amounts["deferral"] + catch_up + desired = column(people_inputs, "traditional_401k_contributions_desired") + column( + people_inputs, "roth_401k_contributions_desired" + ) + room = deferral_limit - np.minimum(desired, deferral_limit) + catch_up_room = np.minimum(room, catch_up) + regular_room = room - catch_up_room + limit = worksheet_maximum(step_3, regular_room, catch_up_room, amounts) + return schedule_c, line_15, step_3, limit, catch_up + + +def check(sim, people_inputs, years=YEARS): + desired = column(people_inputs, "self_employed_pension_contributions_desired") + schedule_c_only = np.array([p["schedule_c_only"] for p in people_inputs]) + for year in years: + schedule_c, line_15, step_3, expected, catch_up = reference( + sim, people_inputs, year + ) + earned_income = sim.calculate("self_employed_pension_earned_income", year) + limit = sim.calculate("self_employed_pension_contribution_limit", year) + deduction = sim.calculate("self_employed_pension_contribution_ald_person", year) + message = f"year {year}" + # 1. Schedule SE and the worksheet. + close(sim.calculate("self_employment_tax_ald_person", year), line_15, message) + close(earned_income, step_3, message) + close(limit, expected, message) + # 2. Bounds. + close(deduction, np.minimum(desired, limit), message) + assert np.all(deduction >= 0), message + assert at_most(deduction, earned_income), message + assert at_most(earned_income, schedule_c), message + cap = PUBLISHED[year]["annual_additions"] + catch_up + assert at_most(limit, cap), message + # 3. AGI from Schedule C alone. + agi = sim.calculate("adjusted_gross_income", year) + net_profit = column(people_inputs, "self_employment_income") + column( + people_inputs, "sstb_self_employment_income" + ) + rows = schedule_c_only & (net_profit >= 0) + assert np.all(agi[rows] >= -TOLERANCE), (message, agi[rows]) + + +profits = st.one_of( + st.sampled_from([0, 400, 433, 434, 2_000, 20_000, 30_000, 200_000, 500_000]), + st.integers(-20_000, 600_000), +) +smaller = st.one_of(st.just(0), st.integers(-10_000, 150_000)) +contributions = st.one_of(st.sampled_from([0, 1_000_000]), st.integers(0, 100_000)) +person_strategy = st.fixed_dictionaries( + { + "schedule_c_only": st.booleans(), + "age": st.sampled_from([30, 49, 50, 55, 59, 60, 63, 64, 70]), + "self_employment_income": profits, + "sstb_self_employment_income": st.one_of(st.just(0), profits), + "farm_operations_income": smaller, + "partnership_self_employment_net_earnings": st.one_of( + st.just(0), st.integers(0, 150_000) + ), + "employment_income": st.one_of(st.just(0), st.integers(0, 250_000)), + "traditional_401k_contributions_desired": st.one_of( + st.just(0), st.integers(0, 40_000) + ), + "roth_401k_contributions_desired": st.one_of( + st.just(0), st.integers(0, 20_000) + ), + "self_employed_pension_contributions_desired": contributions, + } +) + + +def normalize(person): + person = dict(person) + if person["schedule_c_only"]: + for variable in ( + "farm_operations_income", + "partnership_self_employment_net_earnings", + "employment_income", + "traditional_401k_contributions_desired", + "roth_401k_contributions_desired", + ): + person[variable] = 0 + return person + + +def simulation_inputs(people_inputs): + return [ + {k: float(v) for k, v in p.items() if k != "schedule_c_only"} + for p in people_inputs + ] + + +SETTINGS = dict( + max_examples=20, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(person_strategy.map(normalize), min_size=4, max_size=15)) +def test_limit_matches_the_pub_560_worksheet(people_inputs): + sim = Simulation(situation=build_situation(simulation_inputs(people_inputs))) + check(sim, people_inputs) + + +GRID_PROFITS = [ + -1_000, + 0, + 434, + 5_000, + 20_000, + 30_000, + 60_000, + 100_000, + 200_000, + 600_000, +] +GRID_DEFERRALS = [0, 10_000, 23_500, 40_000] + + +@pytest.fixture(scope="module") +def grid(): + people_inputs = [ + { + "schedule_c_only": False, + "age": 55, + "self_employment_income": profit, + "employment_income": 50_000, + "traditional_401k_contributions_desired": deferrals, + "self_employed_pension_contributions_desired": 1_000_000, + } + for deferrals, profit in itertools.product(GRID_DEFERRALS, GRID_PROFITS) + ] + sim = Simulation(situation=build_situation(simulation_inputs(people_inputs))) + return sim, people_inputs + + +@pytest.mark.parametrize("year", YEARS) +def test_grid_matches_the_worksheet_and_is_monotone(grid, year): + sim, people_inputs = grid + check(sim, people_inputs, [year]) + limit = sim.calculate("self_employed_pension_contribution_limit", year).reshape( + len(GRID_DEFERRALS), len(GRID_PROFITS) + ) + # The limit rises with Schedule C profit... + assert np.all(np.diff(limit, axis=1) >= -TOLERANCE) + # ...and falls as deferrals to an employer's plan use up the room. + assert np.all(np.diff(limit, axis=0) <= TOLERANCE) + # No room left: steps 7 and 8, the SEP and profit-sharing limit. + earned_income = sim.calculate("self_employed_pension_earned_income", year) + no_room = earned_income.reshape(limit.shape)[-1] + np.testing.assert_allclose( + limit[-1], + np.minimum(PUBLISHED[year]["annual_additions"], 0.2 * no_room), + atol=TOLERANCE, + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py index 33b247938f7..acfc115e9a0 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py @@ -6,14 +6,22 @@ class self_employed_pension_contribution_ald_person(Variable): entity = Person label = "Self-employed pension contribution ALD for each person" unit = USD - documentation = "Personal above-the-line deduction for self-employed pension plan contributions." + documentation = ( + "Personal above-the-line deduction for self-employed SEP, SIMPLE and " + "qualified plan contributions (Schedule 1 line 16), capped at the " + "Pub. 560 maximum deductible contribution." + ) definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/62#a_6", - "https://www.law.cornell.edu/uscode/text/26/404", + "https://www.law.cornell.edu/uscode/text/26/404#a_8_C", + "https://www.irs.gov/pub/irs-prior/p560--2025.pdf#page=39", ) def formula(person, period, parameters): - earnings = max_(0, person("total_self_employment_income", period)) contributions = person("self_employed_pension_contributions", period) - return min_(earnings, contributions) + # The limit applies again here so the deduction stays within it when + # contributions are supplied directly rather than from desired + # contributions. + limit = person("self_employed_pension_contribution_limit", period) + return min_(contributions, limit) diff --git a/policyengine_us/variables/household/expense/retirement/self_employed_pension_contribution_limit.py b/policyengine_us/variables/household/expense/retirement/self_employed_pension_contribution_limit.py index 66a81e8e63d..ccaf664a5d7 100644 --- a/policyengine_us/variables/household/expense/retirement/self_employed_pension_contribution_limit.py +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_contribution_limit.py @@ -4,18 +4,89 @@ class self_employed_pension_contribution_limit(Variable): value_type = float entity = Person - label = "self-employed pension contribution annual additions limit" + label = "Self-employed retirement plan contribution limit" unit = USD documentation = ( - "Section 415(c) annual additions limit for modeled self-employed " - "pension contributions. Regular IRA contributions are subject to " - "separate IRA limits, and employee 401(k)/403(b) deferrals are " - "capped separately by the elective deferral limit." + "Largest deduction for the person's own SEP, SIMPLE and qualified " + "defined contribution plan contributions (Schedule 1 line 16), from " + "the Pub. 560 Deduction Worksheet for Self-Employed. Plan " + "compensation for a self-employed person is earned income net of " + "the plan deduction itself, so the 25% employer rate becomes 20% of " + "earned income before that deduction, and the 100%-of-compensation " + "limit caps employer contributions at half of what is left of " + "earned income after elective deferrals (step 12). The modeled input " + "does not say which kind of plan the person has, so the limit is the " + "most a SEP or a qualified defined contribution plan allows: a " + "profit-sharing plan with a 401(k) arrangement into which the person " + "defers, before tax, all of the elective deferral room that " + "deferrals to other employers' 401(k) and 403(b) plans leave. With no " + "room left, it is the SEP and profit-sharing limit. Catch-up " + "contributions sit outside the dollar limit, and the total never " + "exceeds earned income. A SEP allows less, so the limit overstates " + "its deductions. A SIMPLE IRA usually allows less too, but the " + "contributions don't reduce its compensation, and section 415 " + "doesn't apply to it. So when deferrals to other employers' plans " + "leave elective deferral room close to earned income, a SIMPLE IRA " + "can allow slightly more. A self-employed minister's section " + "403(b)(9) account can also allow more, since section 404(a)(10) " + "limits it only by the section 402(g) or 415 limit. Defined " + "benefit plans are not modeled, so contributions to them above this " + "limit are cut." ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/415#c" + reference = ( + # Subsections (a)(3)(A)(i)(I), (a)(8)(C) and (D), (a)(10), (h)(1)(C) + # and (n). + "https://www.law.cornell.edu/uscode/text/26/404", + # Subsections (a)(2), (c)(1) and (c)(3)(B) and (D). + "https://www.law.cornell.edu/uscode/text/26/415", + # Subsections (v)(2)(A) and (v)(3)(A). + "https://www.law.cornell.edu/uscode/text/26/414#v", + # PDF pages 15, 39: SIMPLE IRA compensation and the worksheet. + "https://www.irs.gov/pub/irs-prior/p560--2025.pdf#page=15", + "https://www.irs.gov/pub/irs-prior/p560--2022.pdf#page=34", + ) def formula(person, period, parameters): - p = parameters(period).gov.irs.gross_income.retirement_contributions - compensation = max_(0, person("total_self_employment_income", period)) - return min_(p.limit.annual_additions, compensation) + p = parameters(period).gov.irs.gross_income.retirement_contributions.limit + # Step 3: earned income before the plan deduction. + earned_income = person("self_employed_pension_earned_income", period) + # Steps 4 and 5: the employer contribution rate, reduced because + # compensation is net of the contribution. Steps 6 and 7 cap the + # employer contribution at the plan rate times the section + # 401(a)(17) compensation limit. At current law that never binds the + # result, because 25% of the compensation limit is 1.25 times the + # step 8 dollar limit, so the model leaves it out; a reform that set + # the rate times that limit below the dollar limit would need it. + rate = p.profit_sharing_deduction_rate + employer_limit = earned_income * rate / (1 + rate) + # Step 9: elective deferrals, up to the section 402(g) room left + # after deferrals to other employers' plans, which use the regular + # limit before the catch-up amount. + other_deferrals = add( + person, + period, + [ + "traditional_401k_contributions", + "roth_401k_contributions", + "traditional_403b_contributions", + "roth_403b_contributions", + ], + ) + deferral_room = max_( + 0, person("elective_deferral_limit", period) - other_deferrals + ) + catch_up_room = min_(deferral_room, person("k401_catch_up_limit", period)) + regular_deferral_room = deferral_room - catch_up_room + # Steps 10 to 15: employer contributions stay within step 7 and + # within half of what is left of earned income after the regular + # deferrals (step 12), because compensation also excludes the + # employer contribution. With the regular deferrals, they stay + # within the dollar limit (step 8). + annual_additions = min_( + min_(p.annual_additions, employer_limit + regular_deferral_room), + (earned_income + regular_deferral_room) / 2, + ) + # Steps 16 to 19: catch-up contributions, outside the dollar limit. + # The total never exceeds earned income. + return min_(earned_income, annual_additions + catch_up_room) diff --git a/policyengine_us/variables/household/expense/retirement/self_employed_pension_contributions.py b/policyengine_us/variables/household/expense/retirement/self_employed_pension_contributions.py index 25f9a4b78e1..2efe7b18724 100644 --- a/policyengine_us/variables/household/expense/retirement/self_employed_pension_contributions.py +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_contributions.py @@ -7,11 +7,15 @@ class self_employed_pension_contributions(Variable): label = "Self-employed pension contributions" unit = USD documentation = ( - "Self-employed pension contributions after applying the section " - "415(c) annual additions limit." + "Self-employed SEP, SIMPLE and qualified plan contributions after " + "applying the Pub. 560 maximum deductible contribution, which " + "includes the section 415(c) annual additions limit." ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/415#c" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/415#c", + "https://www.law.cornell.edu/uscode/text/26/404#a_8_C", + ) def formula(person, period, parameters): contributions = person("self_employed_pension_contributions_desired", period) diff --git a/policyengine_us/variables/household/expense/retirement/self_employed_pension_earned_income.py b/policyengine_us/variables/household/expense/retirement/self_employed_pension_earned_income.py new file mode 100644 index 00000000000..ea530c07aa5 --- /dev/null +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_earned_income.py @@ -0,0 +1,66 @@ +from policyengine_us.model_api import * + + +class self_employed_pension_earned_income(Variable): + value_type = float + entity = Person + label = "Earned income for self-employed retirement plan limits" + unit = USD + documentation = ( + "Earned income, within the meaning of 26 U.S.C. 401(c)(2), from the " + "business that maintains the self-employed SEP, SIMPLE or qualified " + "plan, determined without regard to the plan deduction itself: net " + "profit less the deductible part of self-employment tax (Pub. 560 " + "Deduction Worksheet for Self-Employed, step 3). The model treats the " + "person's Schedule C business as the plan's business. When the person " + "also has farm or partnership self-employment earnings, the deductible " + "part of self-employment tax is split by each business's share of the " + "net profits, as on Form 7206 lines 5 to 8; the worksheet's step 2 " + "enters all of Schedule 1 line 15. All Schedule C profit counts as " + "earned income: the model assumes the personal-services test of " + "section 401(c)(2)(A)(i) is met." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/401#c_2_A", + "https://www.law.cornell.edu/uscode/text/26/404#a_8_C", + # PDF pages 7, 39 + "https://www.irs.gov/pub/irs-prior/p560--2025.pdf#page=7", + "https://www.irs.gov/pub/irs-prior/p560--2022.pdf#page=34", + "https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1", + ) + + def formula(person, period, parameters): + return pension_plan_earned_income(person, period) + + +def pension_plan_earned_income(person, period, other_line_5_profits=0): + """Pub. 560 step 3 for the Schedule C plan business. + + other_line_5_profits adds the profits of other businesses whose + self-employment tax is in Schedule 1 line 15, such as the owners' draw + reform's draw profits, so the plan business takes only its share. + """ + # Step 1: net profit from the business with the plan. + plan_business_profit = max_(0, person("total_self_employment_income", period)) + # Net profits of every business whose earnings are subject to + # self-employment tax, leaving out net losses. + other_business_profits = max_(0, person("farm_operations_income", period)) + max_( + 0, person("partnership_self_employment_net_earnings", period) + ) + all_business_profits = ( + plan_business_profit + other_business_profits + other_line_5_profits + ) + plan_business_share = np.divide( + plan_business_profit, + all_business_profits, + out=np.zeros_like(all_business_profits), + where=all_business_profits > 0, + ) + # Step 2: the deductible part of self-employment tax (Schedule 1 + # line 15) attributable to the plan's business. + self_employment_tax_deduction = ( + person("self_employment_tax_ald_person", period) * plan_business_share + ) + # Step 3. + return max_(0, plan_business_profit - self_employment_tax_deduction) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 6b1dbcbedc4..c8a862834bf 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -10762,6 +10762,48 @@ candidates: Credit Limit Worksheet B (ctc_credit_limit_worksheet_b_applies.py). The legacy aggregates still count as an exclusion. The EITC keeps the broader section 32(c)(1)(C) test in elects_section_911_exclusion. Register the entry and add the marker once #10297 is on main. +- id: us:catch-up-deferrals-after-402g1c-repeal + program: income-tax + years: + from: 2024 + type: tax + question: Are pre-tax catch-up contributions above the section 402(g)(1)(B) amount excluded from gross income, and + deductible in a self-employed plan, now that SECURE 2.0 section 603(b)(1) has struck section 402(g)(1)(C)? + sources: + - kind: statute + citation: 26 U.S.C. 402(g)(1) as amended by Pub. L. 117-328 section 603(b)(1) and (c); 414(v)(3)(A)(i) + url: https://www.law.cornell.edu/uscode/text/26/402#g_1 + - kind: regulation + citation: 26 CFR 1.402(g)-2(a) + url: https://www.law.cornell.edu/cfr/text/26/1.402(g)-2 + - kind: guidance + citation: Notice 2023-62, section III.A, PDF pages 4-5 + url: https://www.irs.gov/pub/irs-drop/n-23-62.pdf#page=4 + - kind: worksheet + citation: Publication 560 (2025), Deduction Worksheet for Self-Employed, steps 16-18 + url: https://www.irs.gov/pub/irs-pdf/p560.pdf#page=39 + readings: + statute: For taxable years beginning after 2023, section 402(g)(1)(A) includes elective deferrals above the + applicable dollar amount in gross income, except designated Roth contributions; the catch-up exclusion in former + subparagraph (C) is struck, and section 414(v)(3)(A)(i) does not list section 402(g). + other: Notice 2023-62 says non-Roth catch-up contributions are not includible under section 402(g)(1)(A), reading + section 414(v)(3)(A)(i); section 1.402(g)-2(a) still raises the 402(g)(1)(B) limit by the catch-up amount; and + Pub. 560 adds catch-up contributions to the self-employed maximum deductible contribution. + example: In 2025 a 55-year-old with $200,000 of Schedule C profit and $100,000 of desired contributions gets a + $68,280.73 plan limit, including the $7,500 catch-up. Including that $7,500 in gross income under the amended + section 402(g)(1)(A) would leave a net deduction of $60,780.73. + pe_us: + follows: conflicting-source + code: + - policyengine_us/variables/household/expense/retirement/elective_deferral_limit.py + - policyengine_us/variables/household/expense/retirement/self_employed_pension_contribution_limit.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10121 + state: unreviewed + note: elective_deferral_limit adds k401_catch_up_limit to the section 402(g)(1)(B) amount in every year, and the + self-employed plan limit adds the same catch-up room to its maximum. The regulation binds unless it flatly + contradicts the amended statute, and the notice reads section 414(v)(3)(A)(i) to keep catch-ups excludable, so + the project lead should rule before any model change. - id: us:charitable-non-itemizer-2020-separate-cap program: income-tax years: @@ -11268,6 +11310,41 @@ candidates: state: ruled note: Ruled October 2026; verify the worksheet version and whether claiming the rebate defeats the refund-only joint-return exception. +- id: us:se-plan-step-2-multiple-businesses + program: income-tax + years: + from: 2022 + type: tax + question: When a self-employed person has more than one business, does Pub. 560's step 2 subtract all of Schedule 1 + line 15 from the plan business's net profit, or only the part attributable to that business? + sources: + - kind: statute + citation: 26 U.S.C. 401(c)(2)(A)(vi); 404(a)(8)(C) + url: https://www.law.cornell.edu/uscode/text/26/401#c_2_A + - kind: worksheet + citation: Publication 560 (2025), Deduction Worksheet for Self-Employed, step 2 + url: https://www.irs.gov/pub/irs-pdf/p560.pdf#page=39 + - kind: form + citation: Form 7206 (2023), lines 5-8 + url: https://www.irs.gov/pub/irs-prior/f7206--2023.pdf#page=1 + readings: + statute: Earned income is figured with regard to the section 164(f) deduction and only from the business that + has the plan; the statute does not say how to split the deduction among businesses. + other: Pub. 560's step 2 enters the deduction from Schedule 1 line 15; Form 7206 lines 5 to 8 split line 15 by + each business's share of the profitable businesses' net profits for the same section 401(c) earned income. + example: In 2025, Schedule C profit of $60,000 and farm profit of $40,000 give a Schedule 1 line 15 of $7,064.78. + Splitting it gives step 3 of $55,761.14 and a $34,652.23 limit; subtracting all of it gives $52,935.23 and + $34,087.05. + pe_us: + follows: gap-fill + code: + - policyengine_us/variables/household/expense/retirement/self_employed_pension_earned_income.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10121 + state: unreviewed + note: 'The code follows Form 7206: the Schedule C business takes Schedule 1 line 15 in proportion to its share of + the positive net profits of Schedule C, Schedule F and K-1 box 14 code A, the split the self-employed health + insurance limit already uses for the same earned income. Code not yet on main: self_employed_pension_earned_income.' - id: us:separate-filer-zero-income-threshold program: income-tax years: