diff --git a/changelog.d/fix-oh-refundable-eitc-reform-zero-refund.fixed.md b/changelog.d/fix-oh-refundable-eitc-reform-zero-refund.fixed.md new file mode 100644 index 00000000000..56e3089fce7 --- /dev/null +++ b/changelog.d/fix-oh-refundable-eitc-reform-zero-refund.fixed.md @@ -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. diff --git a/policyengine_us/reforms/states/oh/eitc/oh_refundable_eitc_reform.py b/policyengine_us/reforms/states/oh/eitc/oh_refundable_eitc_reform.py index 7de8c69ff3c..8cc0b8e3fe9 100644 --- a/policyengine_us/reforms/states/oh/eitc/oh_refundable_eitc_reform.py +++ b/policyengine_us/reforms/states/oh/eitc/oh_refundable_eitc_reform.py @@ -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): @@ -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 @@ -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" @@ -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 @@ -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 diff --git a/policyengine_us/tests/policy/contrib/states/oh/child_poverty_impact_dashboard/eitc/oh_refundable_eitc.yaml b/policyengine_us/tests/policy/contrib/states/oh/child_poverty_impact_dashboard/eitc/oh_refundable_eitc.yaml index 6b6040e397f..7e1377b2e81 100644 --- a/policyengine_us/tests/policy/contrib/states/oh/child_poverty_impact_dashboard/eitc/oh_refundable_eitc.yaml +++ b/policyengine_us/tests/policy/contrib/states/oh/child_poverty_impact_dashboard/eitc/oh_refundable_eitc.yaml @@ -1,11 +1,19 @@ -- 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 @@ -13,16 +21,18 @@ 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 @@ -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 diff --git a/policyengine_us/tests/policy/reform/oh_refundable_eitc.yaml b/policyengine_us/tests/policy/reform/oh_refundable_eitc.yaml new file mode 100644 index 00000000000..f843272538e --- /dev/null +++ b/policyengine_us/tests/policy/reform/oh_refundable_eitc.yaml @@ -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