From 01218d551f19f1c3915bd3f106e8c93c067209c2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:08:29 -0400 Subject: [PATCH 1/5] Apply Ohio's business income deduction and add it back in modified AGI Ohio deducts the first $250,000 ($125,000 married filing separately) of business income (R.C. 5747.01(A)(28)) and taxes the rest at a flat 3% (R.C. 5747.02(A)(4)). The model taxed all business income at the graduated rates. Modified AGI is Ohio AGI plus the (A)(28) deduction and, from 2026, the (A)(34) qualifying capital gain deduction (R.C. 5747.01(II), 5747.79(B)). Personal exemptions and the exemption credit now use modified AGI (R.C. 5747.025, 5747.022). Fixes #4056 Co-Authored-By: Claude Opus 5.5 --- .../fix-oh-modified-agi-addbacks.fixed.md | 1 + .../oh/tax/income/business_income/rate.yaml | 14 ++ .../tax/income/business_income/sources.yaml | 23 +++ .../deductions/business_income/cap.yaml | 25 +++ .../oh/tax/income/deductions/deductions.yaml | 2 + .../qualifying_capital_gain/in_effect.yaml | 13 ++ .../oh_business_income_deduction.yaml | 188 ++++++++++++++++++ .../business_income/oh_modified_agi.yaml | 75 +++++++ .../oh_joint_filing_credit_eligible.yaml | 31 +++ .../business_income/oh_business_income.py | 17 ++ .../oh_business_income_person.py | 23 +++ .../business_income/oh_business_income_tax.py | 22 ++ .../oh_taxable_business_income.py | 32 +++ .../oh_joint_filing_credit_potential.py | 11 +- .../credits/oh_exemption_credit_potential.py | 7 +- .../oh_business_income_deduction.py | 27 +++ .../oh_business_income_deduction_person.py | 27 +++ .../oh_qualifying_capital_gain.py | 18 ++ ...lifying_capital_gain_deductible_payroll.py | 16 ++ .../oh_qualifying_capital_gain_deduction.py | 30 +++ .../exemptions/oh_personal_exemptions.py | 6 +- ...ncome_tax_before_non_refundable_credits.py | 8 +- .../oh/tax/income/oh_income_tax_exempt.py | 13 +- .../states/oh/tax/income/oh_modified_agi.py | 14 +- .../income/oh_taxable_nonbusiness_income.py | 22 ++ 25 files changed, 643 insertions(+), 22 deletions(-) create mode 100644 changelog.d/fix-oh-modified-agi-addbacks.fixed.md create mode 100644 policyengine_us/parameters/gov/states/oh/tax/income/business_income/rate.yaml create mode 100644 policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml create mode 100644 policyengine_us/parameters/gov/states/oh/tax/income/deductions/business_income/cap.yaml create mode 100644 policyengine_us/parameters/gov/states/oh/tax/income/deductions/qualifying_capital_gain/in_effect.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_business_income_deduction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_modified_agi.yaml create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_person.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_tax.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_taxable_business_income.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deductible_payroll.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/oh_taxable_nonbusiness_income.py diff --git a/changelog.d/fix-oh-modified-agi-addbacks.fixed.md b/changelog.d/fix-oh-modified-agi-addbacks.fixed.md new file mode 100644 index 00000000000..0d38e9ee550 --- /dev/null +++ b/changelog.d/fix-oh-modified-agi-addbacks.fixed.md @@ -0,0 +1 @@ +Ohio now applies the business income deduction and the 3% tax on taxable business income, and modified adjusted gross income adds back the business income deduction and, from 2026, the qualifying capital gain deduction; personal exemptions and the exemption credit use modified adjusted gross income. diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/business_income/rate.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/rate.yaml new file mode 100644 index 00000000000..b13ae13ea20 --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/rate.yaml @@ -0,0 +1,14 @@ +description: Ohio taxes taxable business income at this flat rate. +values: + 2021-01-01: 0.03 +metadata: + unit: /1 + period: year + label: Ohio business income tax rate + reference: + - title: R.C. 5747.02(A)(4)(a) + href: https://codes.ohio.gov/ohio-revised-code/section-5747.02 + - title: 2024 Ohio Schedule of Business Income, line 16 + href: https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5 + - title: 2024 Ohio IT 1040 Instructions, Ohio Schedule of Business Income, Part 3 + href: https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/it1040-booklet.pdf#page=27 diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml new file mode 100644 index 00000000000..2fc36277f59 --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml @@ -0,0 +1,23 @@ +description: Ohio counts these income sources as business income on the Schedule of Business Income. +values: + 2021-01-01: + - self_employment_income # Schedule C, line 2 + - sstb_self_employment_income # Schedule C, line 2 + - partnership_s_corp_income # Schedule E pass-through income, line 4 + - farm_operations_income # Schedule F, line 6 +metadata: + unit: list + period: year + label: Ohio business income sources + reference: + # "Generally, income recognized by a sole proprietorship or pass-through + # entity is business income." Interest, dividends, capital gains, rents, + # royalties and trust income are business income only when they arise in + # a trade or business, which the model cannot observe, so they are treated + # as nonbusiness income. + - title: R.C. 5747.01(B) and (C) + href: https://codes.ohio.gov/ohio-revised-code/section-5747.01 + - title: 2024 Ohio IT 1040 Instructions, Definitions and Examples of Business and Nonbusiness Income + href: https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/it1040-booklet.pdf#page=11 + - title: 2024 Ohio IT 1040 Instructions, Ohio Schedule of Business Income + href: https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/it1040-booklet.pdf#page=26 diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/business_income/cap.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/business_income/cap.yaml new file mode 100644 index 00000000000..592da67014c --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/business_income/cap.yaml @@ -0,0 +1,25 @@ +description: Ohio limits the business income deduction to this amount, by filing status. +metadata: + propagate_metadata_to_children: true + label: Ohio business income deduction cap + period: year + unit: currency-USD + breakdown: + - filing_status + reference: + - title: R.C. 5747.01(A)(28) + href: https://codes.ohio.gov/ohio-revised-code/section-5747.01 + - title: 2024 Ohio Schedule of Business Income, line 12 + href: https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5 + - title: 2021 Ohio IT 1040 Instructions, Business Income + href: https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2021/pit-it1040-booklet.pdf#page=10 +SINGLE: + 2021-01-01: 250_000 +JOINT: + 2021-01-01: 250_000 +SEPARATE: + 2021-01-01: 125_000 +HEAD_OF_HOUSEHOLD: + 2021-01-01: 250_000 +SURVIVING_SPOUSE: + 2021-01-01: 250_000 diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/deductions.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/deductions.yaml index 80af64bd48d..55b555e02bc 100644 --- a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/deductions.yaml +++ b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/deductions.yaml @@ -11,6 +11,8 @@ values: - oh_unreimbursed_medical_care_expense_deduction_person - us_govt_interest_person - salt_refund_income + - oh_business_income_deduction_person # Schedule of Adjustments, line 13 + - oh_qualifying_capital_gain_deduction # R.C. 5747.01(A)(34), from 2026 metadata: unit: list diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/deductions/qualifying_capital_gain/in_effect.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/qualifying_capital_gain/in_effect.yaml new file mode 100644 index 00000000000..ff0d285f41d --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/tax/income/deductions/qualifying_capital_gain/in_effect.yaml @@ -0,0 +1,13 @@ +description: Ohio allows the qualifying capital gain deduction if this is true. +values: + 2021-01-01: false + 2026-01-01: true +metadata: + unit: bool + period: year + label: Ohio qualifying capital gain deduction in effect + reference: + - title: R.C. 5747.79(B) + href: https://codes.ohio.gov/ohio-revised-code/section-5747.79 + - title: R.C. 5747.01(A)(34) + href: https://codes.ohio.gov/ohio-revised-code/section-5747.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_business_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_business_income_deduction.yaml new file mode 100644 index 00000000000..974a1adef26 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_business_income_deduction.yaml @@ -0,0 +1,188 @@ +# Ohio Schedule of Business Income (2024 IT 1040 bundle, PDF p. 5) and IT 1040 +# lines 1-8c (PDF p. 1); 2024 Ohio IT 1040 instructions, pp. 16-18 and 26-27. +# 2024 nonbusiness brackets: 0 up to 26,050; 360.69 + 2.75% of the excess over +# 26,050 up to 100,000; 2,394.32 + 3.50% of the excess over 100,000. +# Exemption by MAGI: 2,400 (40,000 or less), 2,150 (to 80,000), 1,900 (above). + +- name: Pass-through income under the cap is fully deducted + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 200_000 + s_corp_income: 100_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + # Schedule lines 4 and 10: 100_000; line 11: min(100_000, 300_000); + # line 13: min(100_000, 250_000). + oh_business_income: 100_000 + oh_business_income_deduction: 100_000 + # IT 1040 line 3 = 300_000 - 100_000. + oh_agi: 200_000 + # MAGI = Ohio AGI + business income deduction (R.C. 5747.01(II)). + oh_modified_agi: 300_000 + oh_personal_exemptions: 1_900 + # Line 5 = 198_100; line 6 = 0; line 7 = 198_100. + oh_taxable_income: 198_100 + oh_taxable_business_income: 0 + oh_taxable_nonbusiness_income: 198_100 + # Line 8a = 2_394.32 + 3.5% * 98_100 = 5_827.82; line 8b = 0. + oh_income_tax_before_non_refundable_credits: 5_827.82 + +- name: Business income above the cap is taxed at the flat 3% rate + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 50_000 + s_corp_income: 400_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + # Line 11 = 400_000; line 13 = 250_000; line 14 = 150_000. + oh_business_income_deduction: 250_000 + oh_agi: 200_000 + oh_modified_agi: 450_000 + # Line 5 = 200_000 - 1_900 = 198_100; line 15 = min(150_000, 198_100). + oh_taxable_business_income: 150_000 + # Line 7 = 198_100 - 150_000 = 48_100. + oh_taxable_nonbusiness_income: 48_100 + # Line 16 / 8b = 3% * 150_000. + oh_business_income_tax: 4_500 + # Line 8a = 360.69 + 2.75% * 22_050 = 967.065; line 8c = 5_467.065. + oh_income_tax_before_non_refundable_credits: 5_467.065 + +- name: Married filing separately is capped at 125,000 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + s_corp_income: 200_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SEPARATE + households: + household: + members: [person1] + state_code: OH + output: + # Line 12: 125_000 for married filing separately. + oh_business_income_deduction: 125_000 + +- name: Business losses net against business gains on the schedule + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 50_000 + self_employment_income: -15_000 + person2: + age: 45 + self_employment_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # 2024 instructions, line 2 example: "Sam has a business that incurs a + # loss of -$15,000. Sam's spouse Suzy has a separate business that + # generates a profit of $10,000. ... they must report -$5,000 on this + # line." Line 11 is then zero: no deduction. + oh_business_income: -5_000 + oh_business_income_deduction: 0 + oh_business_income_deduction_person: [0, 0] + +- name: The deduction is limited to federal adjusted gross income + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + s_corp_income: 100_000 + rental_income: -80_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + # Line 10 = 100_000 (the rental loss is nonbusiness); line 11 = the + # lesser of line 10 or federal AGI (20_000). + oh_business_income: 100_000 + oh_business_income_deduction: 20_000 + oh_agi: 0 + +- name: Exemptions above nonbusiness income reduce taxable business income + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + s_corp_income: 300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + # Line 3 = 50_000; MAGI 300_000 so line 4 = 1_900; line 5 = 48_100. + # Line 14 = 50_000; line 15 = min(50_000, 48_100) = 48_100 + # (R.C. 5747.02(A)(4)(b)); line 7 = 0. + oh_taxable_business_income: 48_100 + oh_taxable_nonbusiness_income: 0 + # Line 8a = 0; line 8b = 3% * 48_100 = 1_443. + oh_income_tax_before_non_refundable_credits: 1_443 + +- name: Each spouse's deduction comes from their own business income + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + s_corp_income: 300_000 + person2: + age: 45 + self_employment_income: 0 + farm_operations_income: 100_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # Line 10 = 400_000; line 13 = 250_000, taken from each spouse's business + # income in proportion: 3/4 and 1/4. + oh_business_income_person: [300_000, 100_000] + oh_business_income_deduction_person: [187_500, 62_500] diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_modified_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_modified_agi.yaml new file mode 100644 index 00000000000..9483a1c4b00 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_modified_agi.yaml @@ -0,0 +1,75 @@ +- name: Modified AGI adds back the business income deduction, and exemptions use it + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 30_000 + person2: + age: 45 + s_corp_income: 30_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # 2024 MAGI worksheet (instructions p. 40): line 1 Ohio AGI 30_000 plus + # line 2 business income deduction 30_000 = 60_000. + oh_agi: 30_000 + oh_modified_agi: 60_000 + # The exemption is set by MAGI (R.C. 5747.025): 2 * 2_150. Ohio AGI of + # 30_000 would give 2 * 2_400. + oh_personal_exemptions: 4_300 + # Exemption credit: MAGI less exemptions (55_700) is not under 30_000. + oh_exemption_credit_potential: 0 + # Line 5 = 25_700; line 7 = 25_700, at or below 26_050: no tax. + oh_taxable_nonbusiness_income: 25_700 + oh_income_tax_before_non_refundable_credits: 0 + +- name: From 2026 the qualifying capital gain deduction is taken and added back + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 60 + long_term_capital_gains: 500_000 + oh_qualifying_capital_gain: 500_000 + oh_qualifying_capital_gain_deductible_payroll: 300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + # R.C. 5747.79(B): the lesser of the qualifying capital gain or the + # deductible payroll. R.C. 5747.01(II) adds it back to MAGI. + oh_qualifying_capital_gain_deduction: 300_000 + oh_agi: 200_000 + oh_modified_agi: 500_000 + +- name: Before 2026 there is no qualifying capital gain deduction + period: 2025 + input: + people: + person1: + age: 60 + long_term_capital_gains: 500_000 + oh_qualifying_capital_gain: 500_000 + oh_qualifying_capital_gain_deductible_payroll: 300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + oh_qualifying_capital_gain_deduction: 0 + oh_modified_agi: 500_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_eligible.yaml index 6c732cc363a..e0c8d5ec126 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_eligible.yaml @@ -60,3 +60,34 @@ state_code: OH output: oh_joint_filing_credit_eligible: true + +- name: Business income deducted from Ohio AGI is not qualifying income (2025 instructions example) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 50_000 + taxable_interest_income: 5_000 + person2: + age: 45 + s_corp_income: 40_000 + taxable_social_security: 20_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # 2025 Ohio IT 1040 instructions, Schedule of Credits line 12 (PDF p. 29): + # "Kevin has $50,000 of wage income and $5,000 of interest income. Krysten + # has $40,000 of business income and $20,000 of taxable Social Security + # benefits. While Kevin has $50,000 of qualifying income (his wages), + # Krysten has no qualifying income because her business income and + # taxable Social Security benefits were both deducted in calculating their + # Ohio AGI. Therefore, they are not entitled to the joint filing credit." + oh_joint_filing_credit_qualifying_income: [50_000, 0] + oh_joint_filing_credit_eligible: false diff --git a/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income.py b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income.py new file mode 100644 index 00000000000..9307614a295 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class oh_business_income(Variable): + value_type = float + entity = TaxUnit + label = "Ohio business income" + documentation = "Ohio Schedule of Business Income, line 10." + unit = USD + definition_period = YEAR + reference = ( + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5", + ) + defined_for = StateCode.OH + + adds = ["oh_business_income_person"] diff --git a/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_person.py b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_person.py new file mode 100644 index 00000000000..faa55e64b0a --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_person.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class oh_business_income_person(Variable): + value_type = float + entity = Person + label = "Ohio business income for each filer" + unit = USD + definition_period = YEAR + reference = ( + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", + # 2024 Ohio IT 1040 instructions, Ohio Schedule of Business Income + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/it1040-booklet.pdf#page=26", + ) + defined_for = StateCode.OH + + def formula(person, period, parameters): + p = parameters(period).gov.states.oh.tax.income.business_income + # The schedule lists the business income "that you (and your spouse, + # if filing jointly) received"; a dependent's income is on the + # dependent's own return. Losses net against gains. + not_dependent = ~person("is_tax_unit_dependent", period) + return not_dependent * add(person, period, p.sources) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_tax.py b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_tax.py new file mode 100644 index 00000000000..e4785cbdb78 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_business_income_tax.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class oh_business_income_tax(Variable): + value_type = float + entity = TaxUnit + label = "Ohio business income tax" + documentation = ( + "Ohio Schedule of Business Income, line 16, and Ohio IT 1040, line 8b." + ) + unit = USD + definition_period = YEAR + reference = ( + # R.C. 5747.02(A)(4)(a) + "https://codes.ohio.gov/ohio-revised-code/section-5747.02", + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5", + ) + defined_for = StateCode.OH + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.oh.tax.income.business_income + return tax_unit("oh_taxable_business_income", period) * p.rate diff --git a/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_taxable_business_income.py b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_taxable_business_income.py new file mode 100644 index 00000000000..73f3b258208 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/business_income/oh_taxable_business_income.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class oh_taxable_business_income(Variable): + value_type = float + entity = TaxUnit + label = "Ohio taxable business income" + documentation = ( + "Ohio Schedule of Business Income, line 15, and Ohio IT 1040, line 6." + ) + unit = USD + definition_period = YEAR + reference = ( + # R.C. 5747.02(A)(4) + "https://codes.ohio.gov/ohio-revised-code/section-5747.02", + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5", + ) + defined_for = StateCode.OH + + def formula(tax_unit, period, parameters): + # Line 11 + business_income = tax_unit("oh_business_income", period) + federal_agi = tax_unit("adjusted_gross_income", period) + deductible = max_(min_(business_income, federal_agi), 0) + # Line 14: business income above the deduction. + deduction = tax_unit("oh_business_income_deduction", period) + excess = deductible - deduction + # Line 15: no more than the Ohio income tax base (IT 1040 line 5), so + # exemptions that exceed nonbusiness income reduce taxable business + # income (R.C. 5747.02(A)(4)(b)). + income_tax_base = tax_unit("oh_taxable_income", period) + return max_(min_(excess, income_tax_base), 0) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_potential.py b/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_potential.py index 80bce6dca34..ed762da80bd 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_potential.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_potential.py @@ -15,11 +15,10 @@ def formula(tax_unit, period, parameters): "oh_tax_before_joint_filing_credit", period ) p = parameters(period).gov.states.oh.tax.income.credits.joint_filing - # Ohio uses MAGI for the credit computation, which is Ohio AGI with - # the addition of the business income deduction, which is currently not included in the model, - # hence, we use the Ohio AGI for the credit computation - oh_agi = tax_unit("oh_modified_agi", period) + # The percentage depends on modified adjusted gross income less + # exemptions. + modified_agi = tax_unit("oh_modified_agi", period) exemptions = tax_unit("oh_personal_exemptions", period) - agi_less_exepmtions = max_(oh_agi - exemptions, 0) - percentage = p.rate.calc(agi_less_exepmtions) + modified_agi_less_exemptions = max_(modified_agi - exemptions, 0) + percentage = p.rate.calc(modified_agi_less_exemptions) return min_(tax_before_joint_filing_credit * percentage, p.cap) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/credits/oh_exemption_credit_potential.py b/policyengine_us/variables/gov/states/oh/tax/income/credits/oh_exemption_credit_potential.py index 9fc6c4fd61e..024f5431b13 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/credits/oh_exemption_credit_potential.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/credits/oh_exemption_credit_potential.py @@ -15,11 +15,12 @@ class oh_exemption_credit_potential(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.oh.tax.income.credits.exemption - agi = tax_unit("oh_agi", period) + # Modified adjusted gross income less exemptions (R.C. 5747.022). + modified_agi = tax_unit("oh_modified_agi", period) personal_exemptions = tax_unit("oh_personal_exemptions", period) # Per tax form, amount can be negative - modified_agi = agi - personal_exemptions - amount_per_exemption = p.amount.calc(modified_agi, right=True) + modified_agi_less_exemptions = modified_agi - personal_exemptions + amount_per_exemption = p.amount.calc(modified_agi_less_exemptions, right=True) exemptions = tax_unit("exemptions_count", period) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction.py new file mode 100644 index 00000000000..6504536a998 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class oh_business_income_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Ohio business income deduction" + documentation = "Ohio Schedule of Business Income, line 13." + unit = USD + definition_period = YEAR + reference = ( + # R.C. 5747.01(A)(28) + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5", + ) + defined_for = StateCode.OH + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.oh.tax.income.deductions.business_income + # Line 11: the lesser of business income or federal adjusted gross + # income, but not less than zero. + business_income = tax_unit("oh_business_income", period) + federal_agi = tax_unit("adjusted_gross_income", period) + deductible = max_(min_(business_income, federal_agi), 0) + # Lines 12-13 + filing_status = tax_unit("filing_status", period) + return min_(deductible, p.cap[filing_status]) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py new file mode 100644 index 00000000000..b1c948143e9 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class oh_business_income_deduction_person(Variable): + value_type = float + entity = Person + label = "Ohio business income deduction for each filer" + unit = USD + definition_period = YEAR + reference = ( + # R.C. 5747.01(A)(28): deducted "from the portion of an individual's + # federal adjusted gross income that is business income". + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=5", + ) + defined_for = StateCode.OH + + def formula(person, period, parameters): + # The deduction comes out of the business income of the filer who + # earned it, in proportion to each filer's positive business income. + deduction = person.tax_unit("oh_business_income_deduction", period) + business_income = max_(person("oh_business_income_person", period), 0) + total_business_income = person.tax_unit.sum(business_income) + share = np.zeros_like(total_business_income) + mask = total_business_income > 0 + share[mask] = business_income[mask] / total_business_income[mask] + return deduction * share diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain.py new file mode 100644 index 00000000000..316aac7f799 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class oh_qualifying_capital_gain(Variable): + value_type = float + entity = Person + label = "Ohio qualifying capital gain" + documentation = ( + "Capital gain from the sale of an interest in an entity that meets " + "R.C. 5747.79(A)(1): the seller materially participated in the entity " + "or made a venture capital investment of at least one million dollars " + "in it, and the entity was organized and headquartered in Ohio for the " + "five years before the sale." + ) + unit = USD + definition_period = YEAR + reference = "https://codes.ohio.gov/ohio-revised-code/section-5747.79" + defined_for = StateCode.OH diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deductible_payroll.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deductible_payroll.py new file mode 100644 index 00000000000..b07a96dd8f5 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deductible_payroll.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class oh_qualifying_capital_gain_deductible_payroll(Variable): + value_type = float + entity = Person + label = "Ohio deductible payroll for the qualifying capital gain deduction" + documentation = ( + "The qualifying payroll of the entity whose sale produced the " + "qualifying capital gain, multiplied by the percentage of the entity " + "the taxpayer sold (R.C. 5747.79(A)(3)-(4))." + ) + unit = USD + definition_period = YEAR + reference = "https://codes.ohio.gov/ohio-revised-code/section-5747.79" + defined_for = StateCode.OH diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py new file mode 100644 index 00000000000..42fe513f8f5 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class oh_qualifying_capital_gain_deduction(Variable): + value_type = float + entity = Person + label = "Ohio qualifying capital gain deduction" + unit = USD + definition_period = YEAR + reference = ( + # R.C. 5747.79(B); R.C. 5747.01(A)(34) + "https://codes.ohio.gov/ohio-revised-code/section-5747.79", + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", + ) + defined_for = StateCode.OH + + def formula(person, period, parameters): + p = parameters( + period + ).gov.states.oh.tax.income.deductions.qualifying_capital_gain + if not p.in_effect: + return 0 + # The lesser of the qualifying capital gain or the deductible payroll. + # The model counts capital gains as nonbusiness income, so none of + # this gain is also deducted as business income under (A)(28). + gain = max_(person("oh_qualifying_capital_gain", period), 0) + payroll = max_( + person("oh_qualifying_capital_gain_deductible_payroll", period), 0 + ) + return min_(gain, payroll) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions.py b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions.py index 26cd41315c5..e2e1fb95dd6 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/exemptions/oh_personal_exemptions.py @@ -9,6 +9,7 @@ class oh_personal_exemptions(Variable): definition_period = YEAR reference = ( "https://tax.ohio.gov/static/forms/ohio_individual/individual/2021/pit-it1040-booklet.pdf#page=14", + "https://codes.ohio.gov/ohio-revised-code/section-5747.025", ) defined_for = StateCode.OH @@ -19,7 +20,8 @@ def formula(tax_unit, period, parameters): tax_unit, period, ["oh_personal_exemptions_eligible_person"] ) - agi = tax_unit("oh_agi", period) - exemption_amount = p.amount.calc(agi) + # R.C. 5747.025(A) sets the exemption by modified adjusted gross income. + modified_agi = tax_unit("oh_modified_agi", period) + exemption_amount = p.amount.calc(modified_agi) return eligible_exemptions * exemption_amount diff --git a/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_before_non_refundable_credits.py b/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_before_non_refundable_credits.py index 36cd6cb8d33..54f06107f70 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_before_non_refundable_credits.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_before_non_refundable_credits.py @@ -11,7 +11,8 @@ class oh_income_tax_before_non_refundable_credits(Variable): reference = "https://tax.ohio.gov/static/forms/ohio_individual/individual/2021/pit-it1040-booklet.pdf" def formula(tax_unit, period, parameters): - taxable_income = tax_unit("oh_taxable_income", period) + # IT 1040 line 8a: tax on taxable nonbusiness income (line 7). + taxable_income = tax_unit("oh_taxable_nonbusiness_income", period) p = parameters(period).gov.states.oh.tax.income exempt = tax_unit("oh_income_tax_exempt", period) # The statute sets a base amount for each bracket. The marginal rates @@ -19,4 +20,7 @@ def formula(tax_unit, period, parameters): # $100,000), which the base adjustment adds. "More than" the # threshold, so right=True keeps the threshold itself in the lower step. base_adjustment = p.base_adjustment.calc(taxable_income, right=True) - return (p.rates.calc(taxable_income) + base_adjustment) * ~exempt + nonbusiness_tax = (p.rates.calc(taxable_income) + base_adjustment) * ~exempt + # Line 8b: tax on taxable business income; line 8c is the sum. + business_tax = tax_unit("oh_business_income_tax", period) + return nonbusiness_tax + business_tax diff --git a/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_exempt.py b/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_exempt.py index 8b2972926b7..675f23e6c4f 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_exempt.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/oh_income_tax_exempt.py @@ -8,11 +8,14 @@ class oh_income_tax_exempt(Variable): defined_for = StateCode.OH unit = USD definition_period = YEAR - reference = "https://tax.ohio.gov/static/forms/ohio_individual/individual/2021/pit-it1040-booklet.pdf" + reference = ( + "https://tax.ohio.gov/static/forms/ohio_individual/individual/2021/pit-it1040-booklet.pdf", + "https://codes.ohio.gov/ohio-revised-code/section-5747.02", + ) def formula(tax_unit, period, parameters): - taxable_income = tax_unit("oh_taxable_income", period) + # ORC 5747.02(A)(3): no tax on income other than taxable business + # income if that balance is "equal to or less than" the threshold. + taxable_nonbusiness_income = tax_unit("oh_taxable_nonbusiness_income", period) p = parameters(period).gov.states.oh.tax.income - # ORC 5747.02(A)(3): no tax if the balance is "equal to or less than" - # the threshold. - return taxable_income <= p.agi_threshold + return taxable_nonbusiness_income <= p.agi_threshold diff --git a/policyengine_us/variables/gov/states/oh/tax/income/oh_modified_agi.py b/policyengine_us/variables/gov/states/oh/tax/income/oh_modified_agi.py index 1132ac719f0..3fe292c90b6 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/oh_modified_agi.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/oh_modified_agi.py @@ -8,11 +8,17 @@ class oh_modified_agi(Variable): unit = USD definition_period = YEAR reference = ( + # R.C. 5747.01(II): "Ohio adjusted gross income plus any amount + # deducted under divisions (A)(28) and (34) of this section". + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=31", + # 2024 Ohio IT 1040 instructions, MAGI worksheet + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/it1040-booklet.pdf#page=40", ) defined_for = StateCode.OH - adds = ["oh_agi"] - - -# Additionally adds the Ohio Business income deduction + adds = [ + "oh_agi", + "oh_business_income_deduction", + "oh_qualifying_capital_gain_deduction", + ] diff --git a/policyengine_us/variables/gov/states/oh/tax/income/oh_taxable_nonbusiness_income.py b/policyengine_us/variables/gov/states/oh/tax/income/oh_taxable_nonbusiness_income.py new file mode 100644 index 00000000000..660fcf1b667 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/oh_taxable_nonbusiness_income.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class oh_taxable_nonbusiness_income(Variable): + value_type = float + entity = TaxUnit + label = "Ohio taxable nonbusiness income" + documentation = "Ohio IT 1040, line 7." + unit = USD + definition_period = YEAR + reference = ( + # R.C. 5747.02(A)(3) + "https://codes.ohio.gov/ohio-revised-code/section-5747.02", + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf#page=1", + ) + defined_for = StateCode.OH + + def formula(tax_unit, period, parameters): + # Line 5 minus line 6, not less than zero. + income_tax_base = tax_unit("oh_taxable_income", period) + taxable_business_income = tax_unit("oh_taxable_business_income", period) + return max_(income_tax_base - taxable_business_income, 0) From a929b0916fb2832472dafbffff08f389a5116f5b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:34:54 -0400 Subject: [PATCH 2/5] Update Ohio TAXSIM cases for the business income deduction; mirror MAGI in the dependent exemption reform TAXSIM35 does not apply R.C. 5747.01(A)(28); the two 2021 integration cases now carry the form-line derivation with the deduction. The contrib dependent exemption reform's no-op schedule mirrors the baseline, which is stepped by modified AGI (R.C. 5747.025). Co-Authored-By: Claude Opus 5.5 --- .../oh_dependent_exemption_reform.py | 9 ++++---- .../gov/states/oh/tax/income/integration.yaml | 21 +++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/policyengine_us/reforms/states/oh/dependent_exemption/oh_dependent_exemption_reform.py b/policyengine_us/reforms/states/oh/dependent_exemption/oh_dependent_exemption_reform.py index 677c648e879..28154def134 100644 --- a/policyengine_us/reforms/states/oh/dependent_exemption/oh_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/oh/dependent_exemption/oh_dependent_exemption_reform.py @@ -34,8 +34,9 @@ def formula(tax_unit, period, parameters): # Negative amount sentinel = use the discrete MAGI-stepped schedule # (no-op default, since the contrib schedule mirrors the baseline); # a value >= 0 applies a flat per-dependent amount instead. - agi = tax_unit("oh_agi", period) - stepped_per = p.schedule.amount.calc(agi) + # The baseline schedule is stepped by modified AGI (R.C. 5747.025). + modified_agi = tax_unit("oh_modified_agi", period) + stepped_per = p.schedule.amount.calc(modified_agi) per_dependent = where(p.amount < 0, stepped_per, p.amount) return count * per_dependent @@ -77,8 +78,8 @@ class oh_personal_exemptions(Variable): def formula(tax_unit, period, parameters): p_base = parameters(period).gov.states.oh.tax.income.exemptions.personal - agi = tax_unit("oh_agi", period) - per_person = p_base.amount.calc(agi) + modified_agi = tax_unit("oh_modified_agi", period) + per_person = p_base.amount.calc(modified_agi) # Personal portion = all eligible persons minus the dependents that # are separated out (over-age dependents stay in the personal count). diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml index 98420dc467f..d1564a00de1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml @@ -87,8 +87,15 @@ household: members: [person1, person2, person3, person4, person5] state_fips: 39 # OH - output: # expected results from patched TAXSIM35 2024-02-21 version - oh_income_tax: 13_756.36 + output: + # Patched TAXSIM35 (2024-02-21) gives 13_756.36, but it does not apply + # Ohio's business income deduction (R.C. 5747.01(A)(28)). With it: the + # Schedule C income of 160_010 is deducted in full (Schedule of Business + # Income lines 10-13); Ohio AGI 242_927.34; MAGI 402_937.34, so five + # exemptions of 1_900; lines 5 and 7 = 233_427.34; line 8a = 8_021.96; + # joint filing credit 5% = 401.10; tax 7_620.86. + oh_business_income_deduction: 160_010 + oh_income_tax: 7_620.86 - name: Tax unit with taxsimid 97470 in j21.its.csv and j21.ots.csv absolute_error_margin: 0.01 @@ -121,8 +128,14 @@ household: members: [person1, person2, person3] state_fips: 39 # OH - output: # expected results from patched TAXSIM35 2024-03-09 version - oh_income_tax: 2_818.55 + output: + # Patched TAXSIM35 (2024-03-09) gives 2_818.55, but it does not apply + # Ohio's business income deduction (R.C. 5747.01(A)(28)). With it: the + # Schedule C income of 28_010 is deducted in full; Ohio AGI 80_081.16; + # MAGI 108_091.16, so three exemptions of 1_900; lines 5 and 7 = + # 74_381.16; line 8a = 1_850.54. + oh_business_income_deduction: 28_010 + oh_income_tax: 1_850.54 - name: 0-OH.yaml absolute_error_margin: 2 From 35ee5d96b58edd8c71f4cccc65c6bdb8fdbc1e84 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:35:52 -0400 Subject: [PATCH 3/5] Address review: no double exclusion of a deducted qualifying gain; disclose conventions - Joint filing credit qualifying income excludes capital gains only to the extent they are still in Ohio AGI; a gain deducted under R.C. 5747.01(A)(34) is not excluded again (R.C. 5747.05(E)(1)). 2026 test. - The qualifying capital gain deduction cannot exceed the capital gains reported, and its documentation states the per-entity rule of R.C. 5747.79(C)(2). - Label the per-spouse split of a joint business income deduction as a modeling convention; disclose farm rent, Form 4797 gains and 20%-owner compensation as nonbusiness; describe the TAXSIM cases' line 8a as model values; move the MAGI test out of business_income/. Co-Authored-By: Claude Opus 5.5 --- .../tax/income/business_income/sources.yaml | 10 ++++--- ...joint_filing_credit_qualifying_income.yaml | 28 +++++++++++++++++++ .../gov/states/oh/tax/income/integration.yaml | 7 +++-- .../oh_modified_agi.yaml | 0 ...h_joint_filing_credit_qualifying_income.py | 10 +++++-- .../oh_business_income_deduction_person.py | 7 +++-- .../oh_qualifying_capital_gain_deduction.py | 15 +++++++++- 7 files changed, 65 insertions(+), 12 deletions(-) rename policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/{business_income => }/oh_modified_agi.yaml (100%) diff --git a/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml index 2fc36277f59..ebb18465230 100644 --- a/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml +++ b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml @@ -11,10 +11,12 @@ metadata: label: Ohio business income sources reference: # "Generally, income recognized by a sole proprietorship or pass-through - # entity is business income." Interest, dividends, capital gains, rents, - # royalties and trust income are business income only when they arise in - # a trade or business, which the model cannot observe, so they are treated - # as nonbusiness income. + # entity is business income." Interest, dividends, capital gains, rents + # (including farm rent), royalties, trust income and Form 4797 gains are + # business income only when they arise in a trade or business, which the + # model cannot observe, so they are treated as nonbusiness income. Wages + # and guaranteed payments a pass-through pays a 20%-or-more owner + # (schedule line 5) are not modeled. - title: R.C. 5747.01(B) and (C) href: https://codes.ohio.gov/ohio-revised-code/section-5747.01 - title: 2024 Ohio IT 1040 Instructions, Definitions and Examples of Business and Nonbusiness Income diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.yaml index 158417b1341..b3c1e4b597f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.yaml @@ -27,3 +27,31 @@ oh_joint_filing_credit_agi_subtractions: 10_000 output: oh_joint_filing_credit_qualifying_income: -6_000 + +- name: A qualifying capital gain deducted from Ohio AGI is not excluded again + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + employment_income: 600 + long_term_capital_gains: 100_000 + oh_qualifying_capital_gain: 100_000 + oh_qualifying_capital_gain_deductible_payroll: 60_000 + person2: + age: 50 + employment_income: 50_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # Person 1's Ohio AGI is 100_600 - 60_000 = 40_600, of which 40_000 is + # capital gain still in Ohio AGI. R.C. 5747.05(E)(1) excludes capital gains + # from "adjusted gross income", so qualifying income is 600, not + # 40_600 - 100_000. + oh_joint_filing_credit_qualifying_income: [600, 50_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml index d1564a00de1..4e61fb816ae 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml @@ -92,7 +92,9 @@ # Ohio's business income deduction (R.C. 5747.01(A)(28)). With it: the # Schedule C income of 160_010 is deducted in full (Schedule of Business # Income lines 10-13); Ohio AGI 242_927.34; MAGI 402_937.34, so five - # exemptions of 1_900; lines 5 and 7 = 233_427.34; line 8a = 8_021.96; + # exemptions of 1_900; lines 5 and 7 = 233_427.34; line 8a = 8_021.96 in + # the model (the 2021 table's $346.16 base gives 8_021.87; the model's + # first-bracket rate gives a $0.09 higher base, as before this change); # joint filing credit 5% = 401.10; tax 7_620.86. oh_business_income_deduction: 160_010 oh_income_tax: 7_620.86 @@ -133,7 +135,8 @@ # Ohio's business income deduction (R.C. 5747.01(A)(28)). With it: the # Schedule C income of 28_010 is deducted in full; Ohio AGI 80_081.16; # MAGI 108_091.16, so three exemptions of 1_900; lines 5 and 7 = - # 74_381.16; line 8a = 1_850.54. + # 74_381.16; line 8a = 1_850.54 in the model (1_850.45 on the 2021 table, + # the same pre-existing $0.09 base difference). oh_business_income_deduction: 28_010 oh_income_tax: 1_850.54 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_modified_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml similarity index 100% rename from policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/business_income/oh_modified_agi.yaml rename to policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml diff --git a/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py b/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py index 15c7befde23..c597c5879fa 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py @@ -15,8 +15,12 @@ class oh_joint_filing_credit_qualifying_income(Variable): def formula(person, period, parameters): agi = person("oh_agi_person", period) + # Interest, dividends, capital gains and rents are excluded only to + # the extent included in Ohio AGI (R.C. 5747.05(E)(1)). A qualifying + # capital gain deducted under R.C. 5747.01(A)(34) is already out of + # Ohio AGI, so it is not excluded again. + deducted_gain = person("oh_qualifying_capital_gain_deduction", period) + excluded = person("oh_joint_filing_credit_agi_subtractions", period) # Prevent negative subtractions from acting as additions - subtractions = max_( - 0, person("oh_joint_filing_credit_agi_subtractions", period) - ) + subtractions = max_(0, excluded - deducted_gain) return agi - subtractions diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py index b1c948143e9..0fbdf5d11ec 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py @@ -16,8 +16,11 @@ class oh_business_income_deduction_person(Variable): defined_for = StateCode.OH def formula(person, period, parameters): - # The deduction comes out of the business income of the filer who - # earned it, in proportion to each filer's positive business income. + # The deduction comes out of each filer's business income. The statute + # caps the deduction per return and does not say how a joint cap is + # divided; as a modeling convention, it is shared in proportion to + # each filer's positive business income. The split matters only for + # each spouse's Ohio AGI (joint filing credit qualifying income). deduction = person.tax_unit("oh_business_income_deduction", period) business_income = max_(person("oh_business_income_person", period), 0) total_business_income = person.tax_unit.sum(business_income) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py index 42fe513f8f5..a7309d22463 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py @@ -5,6 +5,13 @@ class oh_qualifying_capital_gain_deduction(Variable): value_type = float entity = Person label = "Ohio qualifying capital gain deduction" + documentation = ( + "The lesser of the qualifying capital gain or the deductible payroll " + "(R.C. 5747.79(B)), computed for the sale of interests in one entity. " + "For sales in several entities, the deduction is the sum of the lesser " + "amount for each entity (R.C. 5747.79(C)(2)); set this variable " + "directly in that case." + ) unit = USD definition_period = YEAR reference = ( @@ -23,7 +30,13 @@ def formula(person, period, parameters): # The lesser of the qualifying capital gain or the deductible payroll. # The model counts capital gains as nonbusiness income, so none of # this gain is also deducted as business income under (A)(28). - gain = max_(person("oh_qualifying_capital_gain", period), 0) + # The qualifying gain must be included in federal adjusted gross + # income (R.C. 5747.79(A)(1)), so it cannot exceed the capital gains + # reported. + gain = min_( + max_(person("oh_qualifying_capital_gain", period), 0), + max_(person("capital_gains", period), 0), + ) payroll = max_( person("oh_qualifying_capital_gain_deductible_payroll", period), 0 ) From f96ba752ae6c58a9915c96af0c7d7747e8287f06 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:59:54 -0400 Subject: [PATCH 4/5] Cap the deducted qualifying gain in joint filing credit qualifying income; quote 5747.79(A)(1) A directly set multi-entity deduction above the capital gains reported can no longer cancel the interest, dividend and rent exclusions. Co-Authored-By: Claude Opus 5.5 --- .../oh_joint_filing_credit_qualifying_income.py | 7 ++++++- .../oh_qualifying_capital_gain_deduction.py | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py b/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py index c597c5879fa..cc0f1dfda6c 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/credits/joint_filing_credit/oh_joint_filing_credit_qualifying_income.py @@ -19,7 +19,12 @@ def formula(person, period, parameters): # the extent included in Ohio AGI (R.C. 5747.05(E)(1)). A qualifying # capital gain deducted under R.C. 5747.01(A)(34) is already out of # Ohio AGI, so it is not excluded again. - deducted_gain = person("oh_qualifying_capital_gain_deduction", period) + # Capped at the capital gains reported, so a directly set multi-entity + # deduction cannot cancel the other exclusions. + deducted_gain = min_( + person("oh_qualifying_capital_gain_deduction", period), + max_(person("capital_gains", period), 0), + ) excluded = person("oh_joint_filing_credit_agi_subtractions", period) # Prevent negative subtractions from acting as additions subtractions = max_(0, excluded - deducted_gain) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py index a7309d22463..a9a102d54fb 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py @@ -30,9 +30,10 @@ def formula(person, period, parameters): # The lesser of the qualifying capital gain or the deductible payroll. # The model counts capital gains as nonbusiness income, so none of # this gain is also deducted as business income under (A)(28). - # The qualifying gain must be included in federal adjusted gross - # income (R.C. 5747.79(A)(1)), so it cannot exceed the capital gains - # reported. + # A qualifying capital gain counts only "to the extent that such + # capital gain is not otherwise deducted or excluded in computing + # federal or Ohio adjusted gross income" (R.C. 5747.79(A)(1)); as a + # stand-in, it cannot exceed the capital gains reported. gain = min_( max_(person("oh_qualifying_capital_gain", period), 0), max_(person("capital_gains", period), 0), From e38e81a591aae7d5eddb85f586cf17d68afc3979 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 12:23:10 -0400 Subject: [PATCH 5/5] Exclude dependent qualifying gains from Ohio filer deductions Zero the qualifying capital gain deduction for dependent members whose income is excluded from the filers' federal AGI, retaining the existing filer gain/payroll limits and filer-only business deduction. Pin federal, Ohio and modified AGI using actual dependent gain inputs. Add a generated twin comparison for single/joint returns with dependents. Addresses the PR #9876 interaction identified with #9939. --- .../fix-oh-dependent-capital-gain.fixed.md | 1 + .../test_oh_filer_deduction_invariants.py | 229 ++++++++++++++++++ .../states/oh/tax/income/oh_modified_agi.yaml | 72 ++++++ .../oh_qualifying_capital_gain_deduction.py | 5 +- 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fix-oh-dependent-capital-gain.fixed.md create mode 100644 policyengine_us/tests/core/test_oh_filer_deduction_invariants.py diff --git a/changelog.d/fix-oh-dependent-capital-gain.fixed.md b/changelog.d/fix-oh-dependent-capital-gain.fixed.md new file mode 100644 index 00000000000..6bd6ce553e3 --- /dev/null +++ b/changelog.d/fix-oh-dependent-capital-gain.fixed.md @@ -0,0 +1 @@ +Exclude tax-unit dependents' qualifying capital gain deductions from the filers' Ohio AGI and modified AGI accounting. diff --git a/policyengine_us/tests/core/test_oh_filer_deduction_invariants.py b/policyengine_us/tests/core/test_oh_filer_deduction_invariants.py new file mode 100644 index 00000000000..2d9a3f66bfc --- /dev/null +++ b/policyengine_us/tests/core/test_oh_filer_deduction_invariants.py @@ -0,0 +1,229 @@ +"""Ohio deductions and addbacks use income on the filers' return. + +R.C. 5747.79(A)(1) permits qualifying capital gain only to the extent it +has not been excluded from federal or Ohio AGI; subsection (B) limits the +deduction to deductible payroll. A dependent's income is excluded from the +filers' federal AGI. R.C. 5747.01(A)(28) limits the business deduction to +business income in federal AGI, capped at $250,000 for single and joint +returns. Subsection (II) adds both deductions back to Ohio modified AGI. + +Sources: https://codes.ohio.gov/ohio-revised-code/section-5747.79 and +https://codes.ohio.gov/ohio-revised-code/section-5747.01. + +Each Hypothesis example evaluates six tax units and their twins together: +one or two filers and one through three dependent children. Actual capital gains and +S-corporation income avoid unrelated loss allocation. An independent sum +over filers checks federal AGI, the deductions and Ohio AGI, then a twin +tax unit with dependent gain and business inputs zeroed checks that those +inputs cannot change the filers' deductions or either Ohio AGI measure. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +TOLERANCE = 0.01 +DEPENDENT_INPUTS = ( + "long_term_capital_gains", + "s_corp_income", + "oh_qualifying_capital_gain", + "oh_qualifying_capital_gain_deductible_payroll", +) +PERSON_OUTPUTS = ( + "adjusted_gross_income_person", + "oh_business_income_deduction_person", + "oh_qualifying_capital_gain_deduction", +) +UNIT_OUTPUTS = ( + "adjusted_gross_income", + "oh_business_income_deduction", + "oh_agi", + "oh_modified_agi", +) + + +@st.composite +def person_amounts(draw, *, dependent): + return { + "employment_income": draw(st.integers(1, 100_000)), + "s_corp_income": draw(st.integers(1 if dependent else 0, 350_000)), + "long_term_capital_gains": draw(st.integers(1 if dependent else 0, 400_000)), + "oh_qualifying_capital_gain": draw( + st.integers(1 if dependent else -50_000, 400_000) + ), + "oh_qualifying_capital_gain_deductible_payroll": draw( + st.integers(1 if dependent else -50_000, 400_000) + ), + } + + +@st.composite +def tax_unit_batch(draw): + return [ + { + "filers": [draw(person_amounts(dependent=False)) for _ in range(n_filers)], + "dependents": [ + draw(person_amounts(dependent=True)) for _ in range(n_dependents) + ], + } + for n_filers in (1, 2) + for n_dependents in (1, 2, 3) + ] + + +def _run(units): + situation = { + "people": {}, + "tax_units": {}, + "households": {}, + "marital_units": {}, + } + # Evaluate each unit and its twin in one vectorized simulation. + for i, unit in enumerate(units + units): + members, filers = [], [] + for j, amounts in enumerate(unit["filers"] + unit["dependents"]): + dependent = j >= len(unit["filers"]) + name = f"person_{i}_{j}" + values = { + **amounts, + "age": 10 if dependent else 45, + "is_tax_unit_head": j == 0, + "is_tax_unit_spouse": j == 1 and not dependent, + "is_tax_unit_dependent": dependent, + } + if dependent and i >= len(units): + values.update({variable: 0 for variable in DEPENDENT_INPUTS}) + situation["people"][name] = { + variable: {YEAR: value} for variable, value in values.items() + } + members.append(name) + if dependent: + situation["marital_units"][name] = {"members": [name]} + else: + filers.append(name) + situation["marital_units"][f"filers_{i}"] = {"members": filers} + situation["tax_units"][f"tax_unit_{i}"] = { + "members": members, + "filing_status": {YEAR: "JOINT" if len(filers) == 2 else "SINGLE"}, + } + situation["households"][f"household_{i}"] = { + "members": members, + "state_code": {YEAR: "OH"}, + } + simulation = Simulation(situation=situation) + outputs = { + variable: np.asarray(simulation.calculate(variable, YEAR), dtype=float) + for variable in PERSON_OUTPUTS + UNIT_OUTPUTS + } + outputs["dependent"] = np.asarray( + simulation.calculate("is_tax_unit_dependent", YEAR), dtype=bool + ) + index = simulation.populations["tax_unit"].members_entity_id + n_people = len(index) // 2 + runs = [] + for half in (0, 1): + results = {} + for variable in PERSON_OUTPUTS + ("dependent",): + results[variable] = outputs[variable][ + half * n_people : (half + 1) * n_people + ] + for variable in UNIT_OUTPUTS: + results[variable] = outputs[variable][ + half * len(units) : (half + 1) * len(units) + ] + results["unit"] = index[half * n_people : (half + 1) * n_people] - half * len( + units + ) + runs.append(results) + return runs + + +def _unit_sum(run, amounts): + return np.bincount( + run["unit"], weights=amounts, minlength=len(run["adjusted_gross_income"]) + ) + + +@settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(tax_unit_batch()) +def test_dependent_income_cannot_change_ohio_filer_deductions(units): + run, zeroed = _run(units) + filer = ~run["dependent"] + people = [ + person for unit in units for person in unit["filers"] + unit["dependents"] + ] + + def inputs(variable): + return np.array([person[variable] for person in people], dtype=float) + + expected_person_agi = filer * ( + inputs("employment_income") + + inputs("s_corp_income") + + inputs("long_term_capital_gains") + ) + expected_agi = _unit_sum(run, expected_person_agi) + expected_business = np.minimum( + 250_000, _unit_sum(run, filer * inputs("s_corp_income")) + ) + expected_capital_person = filer * np.minimum.reduce( + [ + np.maximum(0, inputs("oh_qualifying_capital_gain")), + inputs("long_term_capital_gains"), + np.maximum(0, inputs("oh_qualifying_capital_gain_deductible_payroll")), + ] + ) + expected_capital = _unit_sum(run, expected_capital_person) + + for results in (run, zeroed): + for variable in PERSON_OUTPUTS: + np.testing.assert_array_equal(results[variable][run["dependent"]], 0) + np.testing.assert_allclose( + results["adjusted_gross_income_person"], + expected_person_agi, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + results["adjusted_gross_income"], expected_agi, atol=TOLERANCE + ) + np.testing.assert_allclose( + results["oh_qualifying_capital_gain_deduction"], + expected_capital_person, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + results["oh_business_income_deduction"], + expected_business, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + _unit_sum(results, results["oh_business_income_deduction_person"]), + expected_business, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + results["oh_agi"], + expected_agi - expected_business - expected_capital, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + results["oh_modified_agi"], expected_agi, atol=TOLERANCE + ) + for variable in UNIT_OUTPUTS: + np.testing.assert_allclose( + run[variable], zeroed[variable], atol=TOLERANCE, err_msg=variable + ) + for variable in PERSON_OUTPUTS: + np.testing.assert_allclose( + run[variable][filer], + zeroed[variable][filer], + atol=TOLERANCE, + err_msg=variable, + ) diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml index 9483a1c4b00..d5ff7d5d87b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml @@ -73,3 +73,75 @@ output: oh_qualifying_capital_gain_deduction: 0 oh_modified_agi: 500_000 + +- name: A dependent's qualifying gain stays off the filer's Ohio return + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_tax_unit_head: true + employment_income: 60_000 + child: + age: 15 + is_tax_unit_dependent: true + long_term_capital_gains: 100_000 + oh_qualifying_capital_gain: 100_000 + oh_qualifying_capital_gain_deductible_payroll: 80_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: OH + output: + # R.C. 5747.79(A)(1), (B): the child's gain is outside the filer's + # federal AGI and cannot generate a deduction on this return. + adjusted_gross_income_person: [60_000, 0] + adjusted_gross_income: 60_000 + oh_qualifying_capital_gain_deduction: [0, 0] + oh_agi: 60_000 + # R.C. 5747.01(II) adds back only deductions actually taken. + oh_modified_agi: 60_000 + +- name: Filer business and qualifying gain deductions exclude dependent income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_tax_unit_head: true + employment_income: 60_000 + s_corp_income: 40_000 + long_term_capital_gains: 50_000 + oh_qualifying_capital_gain: 50_000 + oh_qualifying_capital_gain_deductible_payroll: 20_000 + child: + age: 15 + is_tax_unit_dependent: true + s_corp_income: 150_000 + long_term_capital_gains: 100_000 + oh_qualifying_capital_gain: 100_000 + oh_qualifying_capital_gain_deductible_payroll: 80_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: OH + output: + adjusted_gross_income_person: [150_000, 0] + adjusted_gross_income: 150_000 + # Schedule of Business Income includes only the filer's income. + oh_business_income_person: [40_000, 0] + oh_business_income_deduction: 40_000 + oh_business_income_deduction_person: [40_000, 0] + oh_qualifying_capital_gain_deduction: [20_000, 0] + oh_deductions: [60_000, 0] + # Ohio AGI = federal AGI - business deduction - qualifying gain deduction. + oh_agi: 90_000 + oh_modified_agi: 150_000 diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py index a9a102d54fb..c97dd054149 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py @@ -41,4 +41,7 @@ def formula(person, period, parameters): payroll = max_( person("oh_qualifying_capital_gain_deductible_payroll", period), 0 ) - return min_(gain, payroll) + # A dependent's gain is outside the filers' federal AGI; the + # deduction belongs on the dependent's own return (R.C. 5747.79(A)(1)). + not_dependent = ~person("is_tax_unit_dependent", period) + return where(not_dependent, min_(gain, payroll), 0)