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/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..ebb18465230 --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/tax/income/business_income/sources.yaml @@ -0,0 +1,25 @@ +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 + # (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 + 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/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/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/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/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/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 98420dc467f..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 @@ -87,8 +87,17 @@ 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 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 - name: Tax unit with taxsimid 97470 in j21.its.csv and j21.ots.csv absolute_error_margin: 0.01 @@ -121,8 +130,15 @@ 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 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 - name: 0-OH.yaml absolute_error_margin: 2 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 new file mode 100644 index 00000000000..d5ff7d5d87b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/oh_modified_agi.yaml @@ -0,0 +1,147 @@ +- 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 + +- 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/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/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..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 @@ -15,8 +15,17 @@ class oh_joint_filing_credit_qualifying_income(Variable): def formula(person, period, parameters): agi = person("oh_agi_person", period) - # Prevent negative subtractions from acting as additions - subtractions = max_( - 0, person("oh_joint_filing_credit_agi_subtractions", 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. + # 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) return agi - subtractions 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..0fbdf5d11ec --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/business_income/oh_business_income_deduction_person.py @@ -0,0 +1,30 @@ +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 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) + 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..c97dd054149 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/qualifying_capital_gain/oh_qualifying_capital_gain_deduction.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * + + +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 = ( + # 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). + # 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), + ) + payroll = max_( + person("oh_qualifying_capital_gain_deductible_payroll", period), 0 + ) + # 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) 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)