diff --git a/CHANGELOG.md b/CHANGELOG.md index cdfae4ac500..d98ce09f6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.39.0] - 2026-10-11 + +### Added + +- Add taxsim_v17, TAXSIM-35's itemized deductions in taxable income (0 for filers who take the standard deduction). + + ## [2.38.24] - 2026-10-11 ### Fixed diff --git a/changelog.d/fix-known-violations-il-aabd.fixed.md b/changelog.d/fix-known-violations-il-aabd.fixed.md new file mode 100644 index 00000000000..480f61ef2c6 --- /dev/null +++ b/changelog.d/fix-known-violations-il-aabd.fixed.md @@ -0,0 +1 @@ +Remove the Illinois AABD expense exemption from the known person-adds-group violations, which #9902 fixed, so the ratchet test passes again. 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..a3429675655 --- /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, based on Schedule C net profit less the deductible part of self-employment tax, so it can no longer reach 100% of profit or make AGI negative. diff --git a/changelog.d/se-plan-business-farm-partnership.fixed.md b/changelog.d/se-plan-business-farm-partnership.fixed.md new file mode 100644 index 00000000000..e6f67e34374 --- /dev/null +++ b/changelog.d/se-plan-business-farm-partnership.fixed.md @@ -0,0 +1 @@ +Let self-employed people whose SEP, SIMPLE or qualified plan is established under a farm (Schedule F) or partnership (K-1 box 14 code A) business deduct their contributions, by default from the combined earned income of every profitable self-employment business, with the self-employed health insurance limit subtracting only the plan deduction attributable to the health plan's business. 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..d74d72522cf --- /dev/null +++ b/policyengine_us/parameters/gov/irs/gross_income/retirement_contributions/limit/profit_sharing_deduction_rate.yaml @@ -0,0 +1,18 @@ +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) - Stock bonus and profit-sharing trusts + href: https://www.law.cornell.edu/uscode/text/26/404#a_3_A_i_I + - title: 26 U.S. Code § 404(h)(1)(C) - Simplified employee pensions + href: https://www.law.cornell.edu/uscode/text/26/404#h_1_C + - 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-pdf/p560.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/tests/code_health/test_no_group_variables_in_person_adds.py b/policyengine_us/tests/code_health/test_no_group_variables_in_person_adds.py index cc36269eca5..888a6260870 100644 --- a/policyengine_us/tests/code_health/test_no_group_variables_in_person_adds.py +++ b/policyengine_us/tests/code_health/test_no_group_variables_in_person_adds.py @@ -113,7 +113,6 @@ KNOWN_VIOLATIONS = { # PolicyEngine/policyengine-us#9888. ("mt_misc_deductions", "adds", "casualty_loss_deduction"), - ("il_aabd_expense_exemption_person", "adds", "state_withheld_income_tax"), ( "ca_riv_general_relief_countable_property_value", "adds", diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml index e1683e2ce58..b253ea0fdb5 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.yaml @@ -152,3 +152,18 @@ output: # Form 7206 line 13: 10,000 - 0.25 x 2,825.91. self_employed_health_insurance_ald_person: 9_293.5225 + +- name: Case 11, a farmer with health and retirement plans under the farm ends with AGI of zero. + period: 2023 + absolute_error_margin: 0.01 + input: + is_self_employed: true + farm_operations_income: 20_000 + self_employed_pension_contributions_desired: 3_000 + health_insurance_premiums: 20_000 + output: + # Form 7206 line 13: 20,000 - 1,412.955 - 3,000 = 15,587.045. + self_employed_pension_contribution_ald_person: 3_000 + self_employed_health_insurance_ald_person: 15_587.045 + # 20,000 - 1,412.955 - 3,000 - 15,587.045. + adjusted_gross_income: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.yaml index 8c2390748c0..250438cb6fd 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 @@ -4,7 +4,10 @@ # One Form 7206 per plan: the limit is the earned income of the business under # which the plan is established (self_employed_health_insurance_plan_business), # or of the person's business with the largest earned income when that input -# is not supplied. +# is not supplied. Line 9 is the part of the Schedule 1 line 16 retirement plan +# deduction attributable to that business: the retirement plan's businesses +# (self_employed_pension_plan_business, by default every profitable one) take +# it in proportion to their line 8 amounts. - name: Case 1, one business, the limit is net profit less half of self-employment tax. period: 2022 @@ -167,12 +170,13 @@ # Lines 5 to 8: 10,000 / 40,000 = 0.25; 10,000 - 0.25 x 2,825.91. self_employed_health_insurance_earned_income_limit: 9_293.5225 -- name: Case 15, the plan deduction is attributable to Schedule C, so the partnership earns more. +- name: Case 15, a retirement plan under the Schedule C business leaves the partnership earning more. period: 2023 absolute_error_margin: 0.01 input: self_employment_income: 20_000 self_employed_pension_contributions_desired: 15_000 + self_employed_pension_plan_business: SCHEDULE_C partnership_self_employment_net_earnings: 10_000 output: # Line 15: 30,000 x 0.9235 x 0.153 / 2 = 2,119.4325. @@ -205,3 +209,63 @@ output: self_employment_tax_ald_person: 706.4775 self_employed_health_insurance_earned_income_limit: 9_293.5225 + +- name: Case 18, retirement plans under both businesses split the plan deduction by earned income. + period: 2023 + absolute_error_margin: 0.01 + input: + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 15_000 + partnership_self_employment_net_earnings: 10_000 + output: + # Line 15: 30,000 x 0.9235 x 0.153 / 2 = 2,119.4325. + self_employment_tax_ald_person: 2_119.4325 + # Line 8: Schedule C 18,587.045 and partnership 9,293.5225, together the + # retirement plan's earned income (Pub. 560 step 3), 27,880.5675. + self_employed_pension_earned_income: 27_880.5675 + # Within the 2023 limit of min(66,000, 5,576.11 + 22,500, + # (27,880.57 + 22,500) / 2) = 25,190.28. + self_employed_pension_contribution_ald_person: 15_000 + # Line 9: Schedule C 2 / 3 x 15,000 = 10,000; partnership 5,000. + # Line 10: Schedule C 8,587.045; partnership 4,293.5225. + self_employed_health_insurance_earned_income_limit: 8_587.045 + +- name: Case 19, a retirement plan under the partnership leaves the Schedule C business's earned income whole. + period: 2023 + absolute_error_margin: 0.01 + input: + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 15_000 + self_employed_pension_plan_business: PARTNERSHIP + partnership_self_employment_net_earnings: 10_000 + output: + # The partnership's earned income, 9,293.5225, caps the plan deduction. + self_employed_pension_contribution_ald_person: 9_293.5225 + # Line 10: Schedule C 18,587.045 (line 9 is 0); partnership 0. + self_employed_health_insurance_earned_income_limit: 18_587.045 + +- name: Case 20, a farm's own retirement plan reduces the farm's limit. + period: 2023 + absolute_error_margin: 0.01 + input: + farm_operations_income: 20_000 + self_employed_pension_contributions_desired: 3_000 + output: + # Line 8: 20,000 - 1,412.955. Line 9: the farm's plan deduction, 3,000. + self_employed_pension_contribution_ald_person: 3_000 + self_employed_health_insurance_earned_income_limit: 15_587.045 + +- name: Case 21, a health plan under the farm subtracts only the farm's share of the retirement plan deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + farm_operations_income: 10_000 + self_employed_pension_contributions_desired: 4_000 + self_employed_health_insurance_plan_business: SCHEDULE_F + output: + # Line 15: 2,825.91. Line 8: Schedule C 27,880.5675 and farm 9,293.5225. + self_employed_pension_contribution_ald_person: 4_000 + # Line 9 for the farm: 9,293.5225 / 37,174.09 x 4,000 = 1,000. + # Line 10: 9,293.5225 - 1,000. + self_employed_health_insurance_earned_income_limit: 8_293.5225 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..2fdd4ed36a9 --- /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,87 @@ +- 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.96. + # Pub. 560 Deduction Worksheet step 3: 20,000 - 1,412.96 = 18,587.05, + # 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 + 23,500 = 60,780.73 (the Pub. 560 2025 + # example's step 3). Person 2: (27,880.57 + 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 + +- name: Case 4, a farm's own plan is deductible and cannot make AGI negative. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + farm_operations_income: 20_000 + self_employed_pension_contributions_desired: 20_000 + output: + # Pub. 560 step 1 is Schedule F line 34: 20,000. Step 3: + # 20,000 - 1,412.955 = 18,587.045, all of which deferrals can cover. + self_employed_pension_contribution_ald_person: [18_587.045] + adjusted_gross_income: 0 + +- name: Case 5, a partner's plan is deductible from K-1 box 14 code A earnings. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + # The partnership's earnings enter gross income through + # partnership_income and self-employment tax through + # partnership_self_employment_net_earnings. + partnership_income: 20_000 + partnership_self_employment_net_earnings: 20_000 + self_employed_pension_contributions_desired: 20_000 + output: + self_employed_pension_contribution_ald_person: [18_587.045] + adjusted_gross_income: 0 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 11b233376f0..867dcdcdeaa 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/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..89ed78dae7e 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,49 @@ +# Expected values follow the Pub. 560 Deduction Worksheet for Self-Employed +# (2025: https://www.irs.gov/pub/irs-pdf/p560.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 the net profit of the +# plan's businesses less their share of half of self-employment tax: 92.35% of +# profit at 15.3% (12.4% up to the Social Security wage base), halved. Unless +# the case names one (self_employed_pension_plan_business), every business +# with a net profit has a plan. + - 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 7: 20% x 481,865.63 = 96,373.13; 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 +55,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 +75,364 @@ 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: 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 sit outside the dollar limit. + 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: 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: Farm earnings take part of the self-employment tax deduction, which is split by net profit, from a plan under the Schedule C business. + 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 + self_employed_pension_plan_business: SCHEDULE_C + 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: Partnership self-employment earnings do not support a contribution to a plan under the Schedule C business. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + partnership_self_employment_net_earnings: 100_000 + self_employed_pension_contributions_desired: 10_000 + self_employed_pension_plan_business: SCHEDULE_C + output: + self_employed_pension_earned_income: 0 + self_employed_pension_contribution_limit: 0 + self_employed_pension_contributions: 0 + +- 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 + +- name: A farm (Schedule F) is the only business. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + farm_operations_income: 100_000 + self_employed_pension_contributions_desired: 100_000 + output: + # Steps 1 to 3: 100,000 - 92,350 x 15.3% / 2 = 100,000 - 7,064.7775. + self_employment_tax_ald_person: 7_064.7775 + self_employed_pension_earned_income: 92_935.2225 + # Step 7: 18,587.0445. Steps 13 + 15: 18,587.0445 + 23,500. + self_employed_pension_contribution_limit: 42_087.0445 + self_employed_pension_contributions: 42_087.0445 + self_employed_pension_contribution_ald_person: 42_087.0445 + +- name: A partnership with K-1 box 14 code A earnings is the only business. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + partnership_self_employment_net_earnings: 100_000 + self_employed_pension_contributions_desired: 10_000 + output: + # Step 3 and the limit as for the farm alone. + self_employed_pension_earned_income: 92_935.2225 + self_employed_pension_contribution_limit: 42_087.0445 + self_employed_pension_contributions: 10_000 + +- name: By default a Schedule C business and a farm both have plans, and their earned income adds up. + 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: + # Steps 1 to 3: 100,000 - 7,064.7775 (all of Schedule 1 line 15). + self_employed_pension_earned_income: 92_935.2225 + # Steps 13 + 15: 18,587.0445 + 23,500, against 34,652.23 with the plan + # under the Schedule C business alone. + self_employed_pension_contribution_limit: 42_087.0445 + +- name: A plan under the farm alone uses the farm's earned income. + 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 + self_employed_pension_plan_business: SCHEDULE_F + output: + # Steps 1 to 3: 40,000 - 40% x 7,064.7775 = 37,174.089. + self_employed_pension_earned_income: 37_174.089 + # Step 7: 7,434.82. Step 9: 23,500. Step 12: (37,174.089 - 23,500) / 2 = + # 6,837.0445, the smallest, so step 13 is 6,837.0445. Step 19: 30,337.0445. + self_employed_pension_contribution_limit: 30_337.0445 + +- name: A plan under the partnership alone uses the partnership's earned income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 50_000 + partnership_self_employment_net_earnings: 50_000 + self_employed_pension_contributions_desired: 100_000 + self_employed_pension_plan_business: PARTNERSHIP + output: + # Steps 1 to 3: 50,000 - 50% x 7,064.7775 = 46,467.61125. + self_employed_pension_earned_income: 46_467.6113 + # Steps 13 + 15: 9,293.5223 + 23,500. + self_employed_pension_contribution_limit: 32_793.5223 + +- name: A farm with a net loss has no plan by default and does not reduce the Schedule C business's earned income. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 30_000 + farm_operations_income: -10_000 + self_employed_pension_contributions_desired: 30_000 + output: + # Line 15: (30,000 - 10,000) x 0.9235 x 15.3% / 2 = 1,412.955, all + # attributable to the profitable Schedule C business. + self_employment_tax_ald_person: 1_412.955 + self_employed_pension_earned_income: 28_587.045 + # Step 12: (28,587.045 - 23,000) / 2 = 2,793.5225, the smallest, so + # steps 13 + 15 are 2,793.5225 + 23,000. + self_employed_pension_contribution_limit: 25_793.5225 + self_employed_pension_contributions: 25_793.5225 + +- name: A plan under a farm with a net loss allows no contribution. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 30_000 + farm_operations_income: -10_000 + self_employed_pension_contributions_desired: 30_000 + self_employed_pension_plan_business: SCHEDULE_F + output: + self_employed_pension_earned_income: 0 + self_employed_pension_contribution_limit: 0 + self_employed_pension_contributions: 0 + +- name: Plans under several businesses share one dollar limit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + self_employment_income: 300_000 + partnership_self_employment_net_earnings: 300_000 + self_employed_pension_contributions_desired: 150_000 + output: + # Line 15: (184,500 x 12.4% + 554,100 x 2.9%) / 2 = 19,473.45. Step 3: + # 600,000 - 19,473.45 = 580,526.55. Step 7 (116,105.31) exceeds the + # $72,000 dollar limit, which binds once for both businesses. + self_employment_tax_ald_person: 19_473.45 self_employed_pension_contribution_limit: 72_000 - self_employed_pension_contributions: 72_000 diff --git a/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_v17.yaml b/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_v17.yaml new file mode 100644 index 00000000000..a36c25c0df4 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_v17.yaml @@ -0,0 +1,48 @@ +# TAXSIM-35 v17 is "Itemized Deductions in taxable income": 0 when the filer +# takes the standard deduction. Expected values are the TAXSIM-35 executable's +# v17 for the same Texas households (no state income tax). TAXSIM has no +# sales tax deduction, so the general sales tax deduction is set to 0. + +- name: Itemized deductions below the standard deduction are not in taxable income + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + employment_income: 50_000 + deductible_mortgage_interest: 5_000 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [head] + state_and_local_sales_or_income_tax: 0 + households: + household: + members: [head] + state_code: TX + output: + # 7,000 of itemized deductions is below the 14,600 standard deduction. + itemized_taxable_income_deductions: 7_000 + taxsim_v17: 0 + +- name: An itemizer reports the itemized deductions + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + employment_income: 200_000 + deductible_mortgage_interest: 30_000 + real_estate_taxes: 8_000 + tax_units: + tax_unit: + members: [head] + state_and_local_sales_or_income_tax: 0 + households: + household: + members: [head] + state_code: TX + output: + taxsim_v17: 38_000 diff --git a/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py index 0e4a16eb642..e12803234e2 100644 --- a/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py +++ b/policyengine_us/tests/unit/test_self_employed_health_insurance_properties.py @@ -15,18 +15,25 @@ plan business may be a Schedule C business, a Schedule F farm, a partnership (K-1 box 14 code A) or an S corporation. When self_employed_health_insurance_plan_business is not supplied, the model uses -the business with the largest earned income. The model bases the line 16 plan -deduction on Schedule C income, so only the Schedule C business subtracts it. - -Hypothesis draws batches of self-employed single filers and runs each batch as -one vectorized baseline simulation. Each drawn filer appears six times: with -no named plan business, again with higher premiums, and once naming each of -the four businesses. For every filer and year: +the business with the largest earned income. The Schedule 1 line 16 plan +deduction is attributable to the businesses under which the retirement plan is +established (self_employed_pension_plan_business: by default every profitable +business, or one named business), in proportion to each one's line 8 amount, +whose sum is the retirement plan's earned income (Pub. 560 Deduction Worksheet +step 3). + +Hypothesis draws batches of self-employed single filers, each with a +retirement plan business, and runs each batch as one vectorized baseline +simulation. Each drawn filer appears six times: with no named health plan +business, again with higher premiums, and once naming each of the four +businesses. For every filer and year: 1. Reference: each limit equals a closed-form Form 7206 computation for its plan business that takes lines 15 and 16 from the model, and line 15 equals half of the self-employment tax. Without a named business, the - limit is the largest of the four. + limit is the largest of the four. The retirement plan's earned income + equals the sum of its businesses' line 8 amounts, and line 9 across the + businesses adds up to line 16. 2. Differential: the limit without a named business equals the largest of the limits the model computes when each business is named. 3. Bounds: 0 <= deduction <= premiums; the limit is at least the Schedule C @@ -35,12 +42,9 @@ 4. Monotone in premiums: raising premiums by d raises the deduction by at least 0 and at most d, and leaves the limit unchanged. 5. For a filer with no business losses whose income is wages and - self-employment income, the deduction never takes adjusted gross income - below the Schedule C line 10 amount before its floor at zero. That amount - is negative only when the Schedule 1 line 16 plan deduction exceeds the - Schedule C business's net profit less its share of line 15; otherwise - adjusted gross income stays at or above zero. Intended: a plan under the - farm or partnership does not subtract the Schedule C plan deduction. + self-employment income, adjusted gross income is never negative: line 16 + never exceeds the retirement plan's earned income, and the health plan's + business subtracts its share of it. """ import numpy as np @@ -56,6 +60,8 @@ PREMIUM_RAISE = 2_500 BUSINESSES = ["SCHEDULE_C", "SCHEDULE_F", "PARTNERSHIP", "S_CORPORATION"] # (name prefix, named plan business, premium raise) for each copy of a filer. +# Businesses under which the retirement plan may be established. +PLAN_BUSINESSES = ["ALL", "SCHEDULE_C", "SCHEDULE_F", "PARTNERSHIP"] COPIES = [("u", "UNSPECIFIED", 0), ("r", "UNSPECIFIED", PREMIUM_RAISE)] + [ (business.lower(), business, 0) for business in BUSINESSES ] @@ -87,6 +93,9 @@ def build_situation(filers): "self_employed_pension_contributions_desired": every_year( filer["plan"] ), + "self_employed_pension_plan_business": every_year( + filer["plan_business"] + ), "health_insurance_premiums": every_year(filer["premiums"] + raise_), "self_employed_health_insurance_plan_business": every_year( plan_business @@ -98,10 +107,14 @@ def build_situation(filers): return situation, len(filers) -def form_7206_limits(schedule_c, farm, partnership, s_corp, line_15, line_16): +def form_7206_limits( + schedule_c, farm, partnership, s_corp, plan_business, line_15, line_16 +): """Form 7206 line 13 for each plan business, with lines 15 and 16 given. - Also returns the Schedule C line 10 amount before its floor at zero. + Also returns line 9 for each self-employment business and the retirement + plan's earned income (Pub. 560 step 3), the sum of line 8 over the + businesses under which the retirement plan is established. """ line_4 = { "SCHEDULE_C": np.maximum(schedule_c, 0), @@ -110,15 +123,27 @@ def form_7206_limits(schedule_c, farm, partnership, s_corp, line_15, line_16): } line_5 = sum(line_4.values()) safe_line_5 = np.where(line_5 > 0, line_5, 1) - line_10 = {} - for business, profit in line_4.items(): - line_6 = np.where(line_5 > 0, profit / safe_line_5, 0) - line_8 = profit - line_15 * line_6 - line_9 = line_16 if business == "SCHEDULE_C" else 0 - line_10[business] = line_8 - line_9 - line_13 = {business: np.maximum(amount, 0) for business, amount in line_10.items()} + line_8 = { + business: profit - line_15 * np.where(line_5 > 0, profit / safe_line_5, 0) + for business, profit in line_4.items() + } + in_plan = { + business: (plan_business == "ALL") | (plan_business == business) + for business in line_4 + } + plan_earned_income = sum(line_8[b] * in_plan[b] for b in line_4) + safe_plan_earned_income = np.where(plan_earned_income > 0, plan_earned_income, 1) + line_9 = { + business: np.where( + in_plan[business] & (plan_earned_income > 0), + line_16 * line_8[business] / safe_plan_earned_income, + 0, + ) + for business in line_4 + } + line_13 = {b: np.maximum(line_8[b] - line_9[b], 0) for b in line_4} line_13["S_CORPORATION"] = np.maximum(s_corp, 0) - return line_13, line_10["SCHEDULE_C"] + return line_13, line_9, plan_earned_income def close(actual, expected): @@ -142,6 +167,7 @@ def copy(array, prefix): farm = column(filers, "farm") partnership = column(filers, "partnership") s_corp = column(filers, "s_corp") + plan_business = np.array([f["plan_business"] for f in filers]) for year in YEARS: premiums = sim.calculate("self_employed_health_insurance_premiums", year) limit = sim.calculate( @@ -152,6 +178,7 @@ def copy(array, prefix): line_16 = sim.calculate("self_employed_pension_contribution_ald_person", year) se_tax = sim.calculate("self_employment_tax", year) agi = sim.calculate("adjusted_gross_income", year) + plan_earned_income = sim.calculate("self_employed_pension_earned_income", year) # Lines 15 and 16 and the deduction for every copy. close(line_15, se_tax / 2) @@ -160,11 +187,12 @@ def copy(array, prefix): for prefix, _, _ in COPIES[1:]: close(copy(line_15, prefix), copy(line_15, "u")) close(copy(line_16, prefix), copy(line_16, "u")) - line_13, schedule_c_line_10 = form_7206_limits( + line_13, line_9, reference_plan_earned_income = form_7206_limits( schedule_c, farm, partnership, s_corp, + plan_business, copy(line_15, "u"), copy(line_16, "u"), ) @@ -174,6 +202,8 @@ def copy(array, prefix): close(copy(limit, "u"), largest) for business in BUSINESSES: close(copy(limit, business.lower()), line_13[business]) + close(copy(plan_earned_income, "u"), reference_plan_earned_income) + close(sum(line_9.values()), copy(line_16, "u")) # 2. Differential: the default equals the best named business. named = np.stack([copy(limit, b.lower()) for b in BUSINESSES]) @@ -193,9 +223,8 @@ def copy(array, prefix): # 5. Adjusted gross income floor for filers with no business losses. no_losses = (schedule_c >= 0) & (farm >= 0) & (partnership >= 0) - floor = np.minimum(0, schedule_c_line_10) for prefix, _, _ in COPIES: - assert at_least(copy(agi, prefix)[no_losses], floor[no_losses]) + assert at_least(copy(agi, prefix)[no_losses], 0) money = st.integers(0, 120_000) @@ -210,6 +239,7 @@ def copy(array, prefix): "s_corp": st.one_of(st.just(0), st.integers(0, 150_000)), "wages": st.one_of(st.just(0), st.integers(0, 250_000)), "plan": st.one_of(st.just(0), money), + "plan_business": st.sampled_from(PLAN_BUSINESSES), "premiums": st.one_of(st.sampled_from([0, 1_000, 6_000]), money), } ) @@ -228,7 +258,16 @@ def test_deduction_follows_form_7206(filers): check_properties(Simulation(situation=situation), filers, n) -def filer(schedule_c=0, farm=0, partnership=0, s_corp=0, wages=0, plan=0, premiums=0): +def filer( + schedule_c=0, + farm=0, + partnership=0, + s_corp=0, + wages=0, + plan=0, + plan_business="ALL", + premiums=0, +): return dict( schedule_c=schedule_c, farm=farm, @@ -236,6 +275,7 @@ def filer(schedule_c=0, farm=0, partnership=0, s_corp=0, wages=0, plan=0, premiu s_corp=s_corp, wages=wages, plan=plan, + plan_business=plan_business, premiums=premiums, ) @@ -254,9 +294,20 @@ def test_reviewer_example_and_boundaries(): filer(partnership=2_000, premiums=6_000), # S corporation wages. filer(s_corp=5_000, premiums=6_000), - # A plan deduction as large as the Schedule C profit, with the farm as - # the plan business (property 5). + # A plan deduction as large as the Schedule C profit, with retirement + # plans under both businesses and then under the Schedule C business + # alone; the health plan is under the larger farm. filer(schedule_c=20_000, plan=20_000, farm=50_000, premiums=100_000), + filer( + schedule_c=20_000, + plan=20_000, + plan_business="SCHEDULE_C", + farm=50_000, + premiums=100_000, + ), + # A farm's own retirement plan and a partner's. + filer(farm=20_000, plan=3_000, premiums=100_000), + filer(partnership=20_000, plan=3_000, premiums=100_000), ] situation, n = build_situation(filers) sim = Simulation(situation=situation) @@ -271,5 +322,19 @@ def test_reviewer_example_and_boundaries(): assert abs(deduction[2] - 433) < TOLERANCE # Line 11: the S corporation wages, with no self-employment tax. assert abs(deduction[8] - 5_000) < TOLERANCE - # Farm line 13: 50,000 - 50,000 / 70,000 x (70,000 x 0.9235 x 0.153 / 2). - assert abs(deduction[9] - (50_000 - 50_000 * 0.9235 * 0.153 / 2)) < TOLERANCE + # Line 15: 70,000 x 0.9235 x 0.153 / 2. Line 8: Schedule C 18,587.045 and + # farm 46,467.6125. With retirement plans under both businesses, the + # 2022 limit (13,010.93 + 20,500) allows the 20,000 contribution, and the + # farm's line 9 is 20,000 x 46,467.6125 / 65,054.6575. + farm_line_8 = 50_000 - 50_000 * 0.9235 * 0.153 / 2 + plan_earned_income = 70_000 - 70_000 * 0.9235 * 0.153 / 2 + farm_line_9 = 20_000 * farm_line_8 / plan_earned_income + assert abs(deduction[9] - (farm_line_8 - farm_line_9)) < TOLERANCE + # With the retirement plan under the Schedule C business alone, line 16 + # is capped at that business's earned income and the farm subtracts none + # of it. + assert abs(deduction[10] - farm_line_8) < TOLERANCE + # Line 13 for a farm or a partnership with its own retirement plan: + # 20,000 - 1,412.955 - 3,000. + for i in (11, 12): + assert abs(deduction[i] - 15_587.045) < TOLERANCE 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..1393c6320a1 --- /dev/null +++ b/policyengine_us/tests/unit/test_self_employed_pension_limit_properties.py @@ -0,0 +1,420 @@ +"""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 plan may be established under the Schedule C business, the Schedule F +farm or a partnership (K-1 box 14 code A), and step 1 is the net profit of the +plan's business (`self_employed_pension_plan_business`). By default every +profitable business has a plan, under one dollar limit and one elective +deferral limit, and their earned income adds up. + +The reference below runs the worksheet's steps 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). Only Schedule 1 line 15, the deductible part of self-employment +tax, comes from the model. Hypothesis draws batches of single filers across +three years, and each batch runs as one vectorized simulation in which every +filer appears once for each plan business. For every person and year: + +1. The limit equals the worksheet maximum, and step 3 equals the plan + businesses' net profit less their share of line 15, split by net profit + as on Form 7206 lines 5 to 8. +2. The deduction is min(desired contributions, limit), is at most earned + income (404(a)(8)(C)), which is at most the plan businesses' net profit, + and is at most the dollar limit plus the person's catch-up amount. +3. For a filer whose only income is self-employment profit, AGI is never + negative when no business has a loss. +4. Across plan businesses: the default's earned income is the sum of the + named businesses' earned income; its limit is at least each named + business's limit, so naming no business never lowers the deduction from + a plan under the Schedule C business alone, and at most their sum. + +A deterministic grid checks that the limit rises with Schedule C profit and +with farm profit, and falls with deferrals to an employer's plan. +""" + +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 +# Plan contribution rate: 26 U.S.C. 404(a)(3)(A)(i)(I) and 404(h)(1)(C). +PLAN_RATE = 0.25 +# Model outputs are float32, whose spacing exceeds a cent above $131,072. +RTOL = 1e-6 +# Businesses under which the plan may be established; ALL is the default. +PLAN_BUSINESSES = ["ALL", "SCHEDULE_C", "SCHEDULE_F", "PARTNERSHIP"] +# 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) + + +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()} + # The partnership's earnings also enter gross income. + people[name]["partnership_income"] = every_year( + inputs.get("partnership_self_employment_net_earnings", 0) + ) + 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 plan_business_profit(people_inputs): + """Step 1: the net profit of the businesses under which the plan is established.""" + profits = { + "SCHEDULE_C": np.maximum( + column(people_inputs, "self_employment_income") + + column(people_inputs, "sstb_self_employment_income"), + 0, + ), + "SCHEDULE_F": np.maximum(column(people_inputs, "farm_operations_income"), 0), + "PARTNERSHIP": np.maximum( + column(people_inputs, "partnership_self_employment_net_earnings"), 0 + ), + } + all_profits = sum(profits.values()) + plan_business = np.array([p["plan_business"] for p in people_inputs]) + plan_profit = np.select( + [plan_business == business for business in profits], + list(profits.values()), + all_profits, + ) + return plan_profit, all_profits + + +def reference(sim, people_inputs, year): + amounts = PUBLISHED[year] + age = column(people_inputs, "age") + plan_profit, all_profits = plan_business_profit(people_inputs) + share = np.divide( + plan_profit, + all_profits, + out=np.zeros_like(plan_profit), + where=all_profits > 0, + ) + # Step 2 is the one model output the reference takes as given. + step_2 = sim.calculate("self_employment_tax_ald_person", year) * share + step_3 = np.maximum(plan_profit - 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 plan_profit, step_3, limit, catch_up + + +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 check(sim, people_inputs): + desired = column(people_inputs, "self_employed_pension_contributions_desired") + self_employment_only = np.array([p["self_employment_only"] for p in people_inputs]) + no_losses = ( + ( + column(people_inputs, "self_employment_income") + + column(people_inputs, "sstb_self_employment_income") + >= 0 + ) + & (column(people_inputs, "farm_operations_income") >= 0) + & (column(people_inputs, "partnership_self_employment_net_earnings") >= 0) + ) + for year in YEARS: + plan_profit, 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. The worksheet. + 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, plan_profit), message + cap = PUBLISHED[year]["annual_additions"] + catch_up + assert at_most(limit, cap), message + # 3. AGI from self-employment profit alone. + agi = sim.calculate("adjusted_gross_income", year) + rows = self_employment_only & no_losses + assert np.all(agi[rows] >= -TOLERANCE), (message, agi[rows]) + + +def check_across_plan_businesses(sim, people_inputs): + """Property 4, for filers laid out by every_plan_business.""" + width = len(PLAN_BUSINESSES) + for year in YEARS: + message = f"year {year}" + earned, limits, deductions = ( + sim.calculate(variable, year).reshape(-1, width) + for variable in ( + "self_employed_pension_earned_income", + "self_employed_pension_contribution_limit", + "self_employed_pension_contribution_ald_person", + ) + ) + # Column 0 is the default (every profitable business), column 1 the + # Schedule C business alone. + close(earned[:, 0], earned[:, 1:].sum(axis=1), message) + assert at_most(limits[:, 1:].max(axis=1), limits[:, 0]), message + assert at_most(deductions[:, 1], deductions[:, 0]), message + assert at_most(limits[:, 0], limits[:, 1:].sum(axis=1)), message + + +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( + { + "self_employment_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), smaller), + "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["self_employment_only"]: + for variable in ( + "employment_income", + "traditional_401k_contributions_desired", + "roth_401k_contributions_desired", + ): + person[variable] = 0 + return person + + +def every_plan_business(people_inputs): + """Each filer once for each plan business, in the order of PLAN_BUSINESSES.""" + return [ + dict(person, plan_business=business) + for person in people_inputs + for business in PLAN_BUSINESSES + ] + + +def simulation_inputs(people_inputs): + return [ + { + **{ + k: float(v) + for k, v in p.items() + if k not in ("self_employment_only", "plan_business") + }, + "self_employed_pension_plan_business": p["plan_business"], + } + 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=2, max_size=8)) +def test_limit_matches_the_pub_560_worksheet(people_inputs): + people_inputs = every_plan_business(people_inputs) + sim = Simulation(situation=build_situation(simulation_inputs(people_inputs))) + check(sim, people_inputs) + check_across_plan_businesses(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 = [ + { + "self_employment_only": False, + "plan_business": "ALL", + "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) + 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, + ) + + +GRID_FARM_PROFITS = [0, 1_000, 5_000, 20_000, 60_000, 200_000, 600_000] + + +@pytest.mark.parametrize("year", YEARS) +def test_limit_rises_with_farm_profit(year): + """A profitable farm adds to the default plan's earned income.""" + people_inputs = [ + { + "self_employment_only": True, + "plan_business": "ALL", + "age": 45, + "self_employment_income": 20_000, + "farm_operations_income": farm, + "self_employed_pension_contributions_desired": 1_000_000, + } + for farm in GRID_FARM_PROFITS + ] + sim = Simulation(situation=build_situation(simulation_inputs(people_inputs))) + check(sim, people_inputs) + earned_income = sim.calculate("self_employed_pension_earned_income", year) + limit = sim.calculate("self_employed_pension_contribution_limit", year) + assert np.all(np.diff(earned_income) > 0) + assert np.all(np.diff(limit) >= -TOLERANCE) diff --git a/policyengine_us/variables/contrib/taxsim/taxsim_v17.py b/policyengine_us/variables/contrib/taxsim/taxsim_v17.py new file mode 100644 index 00000000000..6c517fcb414 --- /dev/null +++ b/policyengine_us/variables/contrib/taxsim/taxsim_v17.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class taxsim_v17(Variable): + value_type = float + entity = TaxUnit + label = "Itemized deductions in taxable income in TAXSIM" + documentation = ( + "TAXSIM-35 v17, 'Itemized Deductions in taxable income': the " + "itemized deductions when the filer itemizes, otherwise 0." + ) + unit = USD + definition_period = YEAR + reference = "https://taxsim.nber.org/taxsim35/" + + def formula(tax_unit, period, parameters): + itemizes = tax_unit("tax_unit_itemizes", period) + itemized = tax_unit("itemized_taxable_income_deductions", period) + return where(itemizes, itemized, 0) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py index 9fda60d1bcb..3aa6c17292c 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_earned_income_limit.py @@ -18,9 +18,11 @@ class self_employed_health_insurance_earned_income_limit(Variable): "is net profit less the part of the deductible part of " "self-employment tax attributable to that business (lines 4 to 8) and " "less the self-employed SEP, SIMPLE and qualified plan deduction " - "attributable to it (lines 9 and 10); the model bases that plan " - "deduction on Schedule C income, so only the Schedule C business " - "subtracts it. For an S corporation it is the shareholder's wages " + "attributable to it (lines 9 and 10). That plan deduction is " + "attributable to the businesses under which the retirement plan is " + "established (self_employed_pension_plan_business), in proportion to " + "each one's earned income, so a business outside the retirement plan " + "subtracts none of it. For an S corporation it is the shareholder's wages " "from the corporation (line 11). self_employed_health_insurance_plan_" "business names the business; when it is not supplied, the limit uses " "the business with the largest earned income, the most that one plan " @@ -62,20 +64,54 @@ def formula(person, period, parameters): out=np.zeros_like(all_business_profits), where=all_business_profits > 0, ) - # Lines 9 and 10: the SEP, SIMPLE and qualified plan deduction - # (Schedule 1 line 16) is attributable to the Schedule C business. + # Line 8 for each self-employment business. + schedule_c_line_8 = schedule_c_profit * (1 - self_employment_tax_deduction_rate) + farm_line_8 = farm_profit * (1 - self_employment_tax_deduction_rate) + partnership_line_8 = partnership_profit * ( + 1 - self_employment_tax_deduction_rate + ) + # Line 9: the SEP, SIMPLE and qualified plan deduction (Schedule 1 + # line 16) attributable to each business. The retirement plan's + # earned income is the sum of its businesses' line 8 amounts (Pub. + # 560 Deduction Worksheet step 3), and each of them takes the + # deduction in proportion to its share of that sum. retirement_plan_deduction = person( "self_employed_pension_contribution_ald_person", period ) + retirement_plan_earned_income = person( + "self_employed_pension_earned_income", period + ) + retirement_plan_deduction_rate = np.divide( + retirement_plan_deduction, + retirement_plan_earned_income, + out=np.zeros_like(retirement_plan_earned_income), + where=retirement_plan_earned_income > 0, + ) + retirement_plan_business = person("self_employed_pension_plan_business", period) + retirement_plan_businesses = retirement_plan_business.possible_values + all_businesses = retirement_plan_business == retirement_plan_businesses.ALL + schedule_c_in_plan = all_businesses | ( + retirement_plan_business == retirement_plan_businesses.SCHEDULE_C + ) + farm_in_plan = all_businesses | ( + retirement_plan_business == retirement_plan_businesses.SCHEDULE_F + ) + partnership_in_plan = all_businesses | ( + retirement_plan_business == retirement_plan_businesses.PARTNERSHIP + ) # Line 10 for each self-employment business. schedule_c = max_( 0, - schedule_c_profit * (1 - self_employment_tax_deduction_rate) - - retirement_plan_deduction, + schedule_c_line_8 + * (1 - schedule_c_in_plan * retirement_plan_deduction_rate), + ) + farm = max_( + 0, farm_line_8 * (1 - farm_in_plan * retirement_plan_deduction_rate) ) - farm = max_(0, farm_profit * (1 - self_employment_tax_deduction_rate)) partnership = max_( - 0, partnership_profit * (1 - self_employment_tax_deduction_rate) + 0, + partnership_line_8 + * (1 - partnership_in_plan * retirement_plan_deduction_rate), ) # Line 11: an S corporation shareholder's wages from the corporation. s_corporation = max_(0, person("s_corp_shareholder_medicare_wages", period)) 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..ece0c96b280 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-pdf/p560.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..7c467b28a3b 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,85 @@ 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 becomes half of it less elective deferrals. The modeled input " + "does not say which kind of plan the person has, so the limit is the " + "most any of them allows: a profit-sharing plan with a 401(k) " + "arrangement into which the person defers all of the elective " + "deferral room that deferrals to other employers' 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. Earned income comes from every business " + "under which a plan is established (self_employed_pension_earned_" + "income), under one dollar limit and one elective deferral limit. A " + "sole proprietor is the employer for all of their own businesses, so " + "their plans share one dollar limit. A partnership is a separate " + "employer, and unless it is under common control with the person's " + "own business, its plan has a dollar limit of its own. The model " + "applies one, which understates the limit only when that dollar limit " + "binds. Defined benefit plans are not modeled." ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/415#c" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/404#a_3_A_i_I", + "https://www.law.cornell.edu/uscode/text/26/404#a_8_C", + "https://www.law.cornell.edu/uscode/text/26/404#n", + "https://www.law.cornell.edu/uscode/text/26/415#c_1", + "https://www.law.cornell.edu/uscode/text/26/415#c_3_B", + "https://www.law.cornell.edu/uscode/text/26/414#v_3_A", + # Employers under common control are one employer, and section 415 + # aggregates the plans of one employer. + "https://www.law.cornell.edu/uscode/text/26/414#c", + "https://www.law.cornell.edu/uscode/text/26/415#f", + "https://www.law.cornell.edu/uscode/text/26/415#h", + "https://www.irs.gov/pub/irs-pdf/p560.pdf#page=39", + "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 rate + # at the section 401(a)(17) compensation limit, which never binds: + # 25% of it has exceeded the step 8 dollar limit in every year. + 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 and regular deferrals + # together stay within the dollar limit (step 8) and within + # compensation, which also excludes the employer contribution + # (step 12). + 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..0d50fb7b664 --- /dev/null +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_earned_income.py @@ -0,0 +1,75 @@ +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 or businesses under which the self-employed SEP, SIMPLE or " + "qualified plan is established, 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, " + "steps 1 to 3). The plan may be established under the Schedule C " + "business, the Schedule F farm or a partnership that reports net " + "earnings on Schedule K-1 box 14 code A " + "(self_employed_pension_plan_business). By default every one of these " + "businesses with a net profit has a plan, and their earned income is " + "added up. 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, so " + "with every profitable business in the plan step 2 is all of Schedule " + "1 line 15." + ) + 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", + "https://www.irs.gov/pub/irs-pdf/p560.pdf#page=7", + "https://www.irs.gov/pub/irs-pdf/p560.pdf#page=39", + "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): + # Step 1: net profit of each business that can establish a plan, + # leaving out net losses. + schedule_c_profit = max_(0, person("total_self_employment_income", period)) + farm_profit = max_(0, person("farm_operations_income", period)) + partnership_profit = max_( + 0, person("partnership_self_employment_net_earnings", period) + ) + all_business_profits = schedule_c_profit + farm_profit + partnership_profit + plan_business = person("self_employed_pension_plan_business", period) + businesses = plan_business.possible_values + plan_business_profit = select( + [ + plan_business == businesses.SCHEDULE_C, + plan_business == businesses.SCHEDULE_F, + plan_business == businesses.PARTNERSHIP, + plan_business == businesses.ALL, + ], + [ + schedule_c_profit, + farm_profit, + partnership_profit, + all_business_profits, + ], + default=all_business_profits, + ) + # Step 2: the deductible part of self-employment tax (Schedule 1 + # line 15) attributable to the plan's businesses, by their share of + # the net profits of every business whose earnings are subject to + # self-employment tax. + 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 + ) + # Step 3. + return max_(0, plan_business_profit - self_employment_tax_deduction) diff --git a/policyengine_us/variables/household/expense/retirement/self_employed_pension_plan_business.py b/policyengine_us/variables/household/expense/retirement/self_employed_pension_plan_business.py new file mode 100644 index 00000000000..d88eb35fdc9 --- /dev/null +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_plan_business.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class SelfEmployedPensionPlanBusiness(Enum): + SCHEDULE_C = "Schedule C business" + SCHEDULE_F = "Schedule F farm" + PARTNERSHIP = "Partnership" + ALL = "All self-employment businesses" + + +class self_employed_pension_plan_business(Variable): + value_type = Enum + entity = Person + possible_values = SelfEmployedPensionPlanBusiness + default_value = SelfEmployedPensionPlanBusiness.ALL + definition_period = YEAR + label = "Business under which the self-employed retirement plan is established" + documentation = ( + "The trade or business under which the person's SEP, SIMPLE or " + "qualified plan (Schedule 1 line 16) is established: the Schedule C " + "business, the Schedule F farm, or a partnership in which the person " + "has Schedule K-1 box 14 code A net earnings. The plan deduction is " + "limited to earned income from the plan's business. ALL (the default) " + "means every one of these businesses with a net profit has a plan, " + "which is the largest limit the person's businesses allow; a " + "business with a net loss is left out. A named business limits the " + "deduction to that business's earned income, as when the person has " + "more than one business but only one has a plan." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/404#a_8_C", + "https://www.law.cornell.edu/uscode/text/26/401#c_4", + # Compensation: with more than one business but only one plan, only + # that business's earned income counts. Employer: a sole proprietor + # is its own employer, and a partnership is each partner's employer. + "https://www.irs.gov/pub/irs-pdf/p560.pdf#page=7", + # Deduction Worksheet step 1: Schedule C, Schedule F or K-1 box 14 A. + "https://www.irs.gov/pub/irs-pdf/p560.pdf#page=39", + ) diff --git a/pyproject.toml b/pyproject.toml index 10ddac47f17..92519a155c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "policyengine-us" -version = "2.38.24" +version = "2.39.0" description = "US federal and state tax-benefit microsimulation model." readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index f00e27986c2..9a13da31d30 100644 --- a/uv.lock +++ b/uv.lock @@ -1818,7 +1818,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "2.38.24" +version = "2.39.0" source = { editable = "." } dependencies = [ { name = "microdf-python" },