Summary
The bundled Ohio refundable-EITC contrib reform (gov.contrib.states.oh.child_poverty_impact_dashboard.eitc.in_effect = true) runs without crashing but is functionally wrong on two fronts: it pays no refundable credit to filers with no OH tax liability (the exact case refundability is meant to help), and it discards Ohio's other six non-refundable credits in the process. Same dual-functional-bug profile as #8640 (MO) and #8644 (UT), just expressed differently because OH has no add() string crash.
Reproduce
from policyengine_us import Simulation
from policyengine_core.reforms import Reform
reform = Reform.from_dict({
"gov.contrib.states.oh.child_poverty_impact_dashboard.eitc.in_effect": {"2026-01-01": True},
}, country_id="us")
sim = Simulation(
situation={
"people": {"head": {"age": {2026: 35}, "employment_income": {2026: 20000}}},
"tax_units": {"tu": {"members": ["head"]}},
"households": {"hh": {"members": ["head"], "state_name": {2026: "OH"}}},
},
reform=reform,
)
print(sim.calculate("oh_refundable_credits", period=2026)) # expected positive; actual 0
Bug 1 — refundable EITC capped at tax liability (silent zero-refund)
policyengine_us/reforms/states/oh/eitc/oh_refundable_eitc_reform.py:
class oh_refundable_eitc(Variable):
def formula(tax_unit, period, parameters):
return tax_unit("oh_eitc", period)
oh_eitc is the applied (nonrefundable-capped) EITC — applied_state_non_refundable_credit(..., "oh_eitc_potential"). A zero-liability filer's oh_eitc is 0, so the reform's oh_refundable_eitc is also 0 — no refund is paid even though the reform claims to make the credit refundable.
Fix: read oh_eitc_potential (which already exists at policyengine_us/variables/gov/states/oh/tax/income/credits/oh_eitc_potential.py and computes federal_eitc * rate). Same shape as the MO fix (mo_wftc → mo_wftc_potential, see #8642) and the UT fix (ut_eitc → ut_eitc_potential, see #8645).
Bug 2 — discards Ohio's other non-refundable credits
class oh_non_refundable_credits(Variable):
def formula(tax_unit, period, parameters):
return tax_unit("oh_non_refundable_eitc", period)
Under the reform oh_non_refundable_eitc evaluates to 0, so the reform's oh_non_refundable_credits is unconditionally 0. The baseline walks the full ordered Ohio non-refundable credit list — currently seven entries:
2023-01-01:
- oh_eitc
- oh_cdcc
- oh_senior_citizen_credit
- oh_retirement_credit
- oh_non_public_school_credits
- oh_exemption_credit
- oh_joint_filing_credit
The reform's intent is to move only oh_eitc out of the bucket; it should not erase the other six credits. As written, an OH filer claiming the CDCC, retirement, exemption, or joint-filing credit loses all of them whenever the reform is in effect.
Fix: mirror the UT pattern from #8645 — walk the same ordered list with oh_eitc filtered out:
from policyengine_us.variables.gov.states.tax.income.non_refundable_credit_cap import (
ordered_capped_state_non_refundable_credits,
)
...
def formula(tax_unit, period, parameters):
ordered_credits = parameters(period).gov.states.oh.tax.income.credits.non_refundable
filtered_credits = [c for c in list(ordered_credits) if c != "oh_eitc"]
return ordered_capped_state_non_refundable_credits(
tax_unit, period, filtered_credits, "oh_income_tax_before_non_refundable_credits",
)
Why this slipped through
Same test-shielding pattern as MO/UT: the existing contrib tests at policyengine_us/tests/policy/contrib/states/oh/child_poverty_impact_dashboard/eitc/oh_refundable_eitc.yaml pin oh_eitc: 500 as input and only assert on oh_refundable_eitc, oh_non_refundable_eitc, and a oh_non_refundable_credits case with no other credits present. No test drives eitc (federal), pins oh_income_tax_before_non_refundable_credits: 0, and asserts the refundable EITC pays out — which is exactly the case the reform is meant to model.
Expected behavior
Under the reform, a Utah-style "fully refundable EITC" semantics: filers receive the full oh_eitc_potential as a refundable credit (subject to the existing rate × federal-EITC formula), regardless of OH tax liability; and Ohio's other six non-refundable credits continue to apply normally.
Environment
policyengine-us==1.715.2 (and current main)
- Python 3.10/3.11
Related
Summary
The bundled Ohio refundable-EITC contrib reform (
gov.contrib.states.oh.child_poverty_impact_dashboard.eitc.in_effect = true) runs without crashing but is functionally wrong on two fronts: it pays no refundable credit to filers with no OH tax liability (the exact case refundability is meant to help), and it discards Ohio's other six non-refundable credits in the process. Same dual-functional-bug profile as #8640 (MO) and #8644 (UT), just expressed differently because OH has noadd()string crash.Reproduce
Bug 1 — refundable EITC capped at tax liability (silent zero-refund)
policyengine_us/reforms/states/oh/eitc/oh_refundable_eitc_reform.py:oh_eitcis the applied (nonrefundable-capped) EITC —applied_state_non_refundable_credit(..., "oh_eitc_potential"). A zero-liability filer'soh_eitcis0, so the reform'soh_refundable_eitcis also0— no refund is paid even though the reform claims to make the credit refundable.Fix: read
oh_eitc_potential(which already exists atpolicyengine_us/variables/gov/states/oh/tax/income/credits/oh_eitc_potential.pyand computesfederal_eitc * rate). Same shape as the MO fix (mo_wftc→mo_wftc_potential, see #8642) and the UT fix (ut_eitc→ut_eitc_potential, see #8645).Bug 2 — discards Ohio's other non-refundable credits
Under the reform
oh_non_refundable_eitcevaluates to0, so the reform'soh_non_refundable_creditsis unconditionally0. The baseline walks the full ordered Ohio non-refundable credit list — currently seven entries:The reform's intent is to move only
oh_eitcout of the bucket; it should not erase the other six credits. As written, an OH filer claiming the CDCC, retirement, exemption, or joint-filing credit loses all of them whenever the reform is in effect.Fix: mirror the UT pattern from #8645 — walk the same ordered list with
oh_eitcfiltered out:Why this slipped through
Same test-shielding pattern as MO/UT: the existing contrib tests at
policyengine_us/tests/policy/contrib/states/oh/child_poverty_impact_dashboard/eitc/oh_refundable_eitc.yamlpinoh_eitc: 500as input and only assert onoh_refundable_eitc,oh_non_refundable_eitc, and aoh_non_refundable_creditscase with no other credits present. No test driveseitc(federal), pinsoh_income_tax_before_non_refundable_credits: 0, and asserts the refundable EITC pays out — which is exactly the case the reform is meant to model.Expected behavior
Under the reform, a Utah-style "fully refundable EITC" semantics: filers receive the full
oh_eitc_potentialas a refundable credit (subject to the existing rate × federal-EITC formula), regardless of OH tax liability; and Ohio's other six non-refundable credits continue to apply normally.Environment
policyengine-us==1.715.2(and currentmain)Related
create_mo_refundable_eitc) crashes when triggered #8640 / Fix MO refundable EITC reform crash and zero-refund bug #8642 — MO has the same shape (plus anadd()string crash).create_ut_fully_refundable_eitc) crashes when triggered #8644 / Fix UT fully refundable EITC reform crash and zero-refund bug #8645 — UT has the same shape (plus anadd()string crash andaddsstrict-check ValueError).