Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the Ohio refundable EITC contrib reform, which paid no refundable credit to filers with no Ohio tax liability and zeroed out Ohio's other six non-refundable credits.
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
from policyengine_us.model_api import *
from policyengine_core.periods import period as period_
from policyengine_us.variables.gov.states.tax.income.non_refundable_credit_cap import (
ordered_capped_state_non_refundable_credits,
)


def create_oh_refundable_eitc() -> Reform:
"""
Ohio Refundable EITC Reform

Converts the Ohio Earned Income Credit from a nonrefundable credit
to a refundable credit. By default, OH EITC is nonrefundable.
Hypothetical reform that pays the Ohio EITC as a fully refundable
credit. ORC § 5747.71 currently makes the credit nonrefundable; this
contrib module is used for what-if analysis only and does not reflect
enacted Ohio law.

Reading ``oh_eitc_potential`` (uncapped 30% of the federal EITC) yields
the full refundable amount for the modeled era (2020+): Ohio's pre-2019
"50% of tax when OH taxable income exceeds $20,000" limitation was
repealed by HB 62 (eff. 2019-07-03), so the only remaining limit on the
nonrefundable ``oh_eitc`` is the ordinary tax-liability cap — exactly
what refundability lifts.
https://codes.ohio.gov/ohio-revised-code/section-5747.71
"""

class oh_refundable_eitc(Variable):
Expand All @@ -19,7 +32,11 @@ class oh_refundable_eitc(Variable):
defined_for = StateCode.OH

def formula(tax_unit, period, parameters):
return tax_unit("oh_eitc", period)
# Use the potential (uncapped) OH EITC so the full credit is paid
# as a refund; `oh_eitc` is capped at remaining tax liability via
# the ordered nonrefundable cap and would zero out the credit for
# the low-liability filers refundability is meant to help.
return tax_unit("oh_eitc_potential", period)

class oh_non_refundable_eitc(Variable):
value_type = float
Expand All @@ -34,8 +51,6 @@ def formula(tax_unit, period, parameters):
return 0

class oh_non_refundable_credits(Variable):
# NOTE: When reform is active, OH EITC moves from nonrefundable to refundable.
# This formula returns the nonrefundable EITC amount (0 under reform).
value_type = float
entity = TaxUnit
label = "Ohio non-refundable credits"
Expand All @@ -44,12 +59,32 @@ class oh_non_refundable_credits(Variable):
reference = (
"https://tax.ohio.gov/static/forms/ohio_individual/individual/2021/sch-cre.pdf",
"https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/itschedule-credits.pdf",
"https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2023/1040-bundle-original.pdf#page=7",
"https://tax.ohio.gov/static/webview/view1/UIExtension/1/pdf-view.html?filename=forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf",
"https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=28",
)
defined_for = StateCode.OH

def formula(tax_unit, period, parameters):
# When reform is active, EITC is refundable, so nonrefundable EITC is 0
return tax_unit("oh_non_refundable_eitc", period)
# Mirror the baseline's ordered-cap logic but drop oh_eitc from
# the non-refundable bucket — it's paid as refundable under this
# reform. The previous formula returned only oh_non_refundable_eitc
# (= 0 under the reform), which silently zeroed out every other
# entry in Ohio's ordered non-refundable list (CDCC, senior,
# retirement, non-public school, exemption, joint filing — plus the
# adoption credit for pre-2023 years).
ordered_credits = parameters(
period
).gov.states.oh.tax.income.credits.non_refundable
filtered_credits = [
credit for credit in list(ordered_credits) if credit != "oh_eitc"
]
return ordered_capped_state_non_refundable_credits(
tax_unit,
period,
filtered_credits,
"oh_income_tax_before_non_refundable_credits",
)

class oh_refundable_credits(Variable):
value_type = float
Expand All @@ -60,6 +95,9 @@ class oh_refundable_credits(Variable):
reference = (
"https://tax.ohio.gov/static/forms/ohio_individual/individual/2021/sch-cre.pdf",
"https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/itschedule-credits.pdf",
"https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2023/1040-bundle-original.pdf#page=7",
"https://tax.ohio.gov/static/webview/view1/UIExtension/1/pdf-view.html?filename=forms/ohio_individual/individual/2024/1040-bundle-original-fi.pdf",
"https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=28",
)
defined_for = StateCode.OH

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
- name: Case 1 - OH EITC is refundable when reform active
# The reform makes the Ohio EITC fully refundable. It pays the uncapped
# potential credit (oh_eitc_potential = rate * federal_eitc), so these tests
# drive the credit from the federal EITC rather than injecting the capped
# oh_eitc. The OH EITC rate is 30% of the federal EITC (constant since 2020).

- name: Case 1 - OH EITC is fully refundable (uncapped) when reform active
period: 2024
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc
input:
state_code: OH
oh_eitc: 500
eitc: 2_000
oh_income_tax_before_non_refundable_credits: 0
output:
oh_refundable_eitc: 500
oh_eitc_potential: 600 # 2,000 * 0.3
oh_eitc: 0 # capped at zero tax liability
oh_refundable_eitc: 600 # full potential paid as a refund
oh_non_refundable_eitc: 0

- name: Case 2 - OH refundable credits include EITC when reform active
period: 2024
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc
input:
state_code: OH
oh_eitc: 1_000
eitc: 3_000
oh_income_tax_before_non_refundable_credits: 0
output:
oh_refundable_credits: 1_000
oh_refundable_eitc: 900 # 3,000 * 0.3
oh_refundable_credits: 900

- name: Case 3 - OH EITC is 0 when EITC is 0
- name: Case 3 - OH refundable EITC is 0 when the federal EITC is 0
period: 2024
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc
input:
state_code: OH
oh_eitc: 0
eitc: 0
output:
oh_refundable_eitc: 0
oh_non_refundable_eitc: 0
Expand All @@ -31,17 +41,8 @@
period: 2024
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc
input:
state_code: CA
oh_eitc: 500
state_code: TX
eitc: 2_000
output:
oh_refundable_eitc: 0
oh_non_refundable_eitc: 0

- name: Case 5 - OH non-refundable credits exclude EITC when reform active
period: 2024
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc
input:
state_code: OH
oh_eitc: 500
output:
oh_non_refundable_credits: 0
87 changes: 87 additions & 0 deletions policyengine_us/tests/policy/reform/oh_refundable_eitc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Tests for the Ohio refundable EITC contrib reform.
# Regression coverage for https://github.com/PolicyEngine/policyengine-us/issues/8656
# (the reform previously paid no refundable credit to zero-liability filers and
# discarded all of Ohio's other non-refundable credits — six in 2023+, seven in
# 2021-2022 with the adoption credit).

- name: Reform pays the full potential OH EITC as a refundable credit at zero liability
period: 2026
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc_reform.oh_refundable_eitc
input:
state_code: OH
eitc: 5_000
# Pin OH tax liability to zero so the nonrefundable cap binds at $0 —
# the test is whether the reform pays the credit despite the cap.
oh_income_tax_before_non_refundable_credits: 0
output:
# 2026 OH EITC rate is 30% of the federal EITC: 5,000 * 0.3 = 1,500.
oh_eitc_potential: 1_500
# The capped credit is 0 at zero liability; the fix pays the uncapped
# potential instead, so this contrast is the whole point of the reform.
oh_eitc: 0
# EITC is moved out of the nonrefundable bucket...
oh_non_refundable_eitc: 0
# ...and paid in full as refundable, even with no Ohio tax liability.
oh_refundable_eitc: 1_500
oh_refundable_credits: 1_500
# End-to-end: the refund flows through to a negative oh_income_tax
# (liability 0 − refundable 1,500 = −1,500), i.e. an actual payout.
oh_income_tax: -1_500

- name: Reform preserves other Ohio non-refundable credits (does not discard them)
period: 2026
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc_reform.oh_refundable_eitc
input:
state_code: OH
eitc: 5_000
oh_income_tax_before_non_refundable_credits: 800
# Force a non-EITC OH non-refundable credit to a positive value to prove
# the reform's oh_non_refundable_credits formula does not zero it out.
# The bug returned `oh_non_refundable_eitc` (= 0), zeroing out every
# other credit in the ordered list.
oh_joint_filing_credit: 500
output:
# Joint-filing credit still applies (capped at liability via the ordered
# walk, which has $800 remaining when joint-filing's turn comes).
oh_non_refundable_credits: 500
# EITC is paid as refundable in full.
oh_refundable_eitc: 1_500
oh_refundable_credits: 1_500

- name: Reform does not double count when liability partially absorbs other credits
period: 2026
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc_reform.oh_refundable_eitc
input:
state_code: OH
eitc: 2_000
# Liability lower than the potential EITC. After EITC is moved to the
# refundable bucket, the remaining $300 of liability is consumed by
# oh_cdcc (first surviving entry in the filtered ordered list).
oh_income_tax_before_non_refundable_credits: 300
oh_cdcc: 200
output:
# 0.3 * 2,000 federal EITC = 600 — paid as refundable regardless of
# liability, not capped at the 300 of OH tax remaining.
oh_refundable_eitc: 600
oh_refundable_credits: 600
# CDCC consumes 200 of the 300 remaining liability; no double-counting
# against the refundable EITC.
oh_non_refundable_credits: 200

- name: Filtered ordered walk caps multiple non-EITC credits at remaining liability
period: 2026
reforms: policyengine_us.reforms.states.oh.eitc.oh_refundable_eitc_reform.oh_refundable_eitc
input:
state_code: OH
eitc: 0
# Multiple OH non-refundable credits compete for the cap; total = 1,200
# but only $600 of liability is available, so the ordered walk must
# cap the bucket at exactly $600.
oh_income_tax_before_non_refundable_credits: 600
oh_cdcc: 400
oh_exemption_credit: 400
oh_joint_filing_credit: 400
output:
# CDCC fills $400, exemption fills the remaining $200, joint-filing
# gets $0 (later in the order). Total caps at the liability of $600.
oh_non_refundable_credits: 600
Loading