Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions changelog.d/sc-fully-refundable-eitc-reform.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a South Carolina fully refundable EITC contrib reform for the Child Poverty Impact Dashboard.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
description: South Carolina applies a fully refundable EITC when this parameter is in effect. By default, the SC EITC is nonrefundable. This reform makes it refundable for ALL filers.

values:
0000-01-01: false

metadata:
unit: bool
period: year
label: South Carolina fully refundable EITC in effect
7 changes: 7 additions & 0 deletions policyengine_us/reforms/reforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@
from .states.ut.child_poverty_eitc import (
create_ut_fully_refundable_eitc_reform,
)
from .states.sc.child_poverty_eitc import (
create_sc_fully_refundable_eitc_reform,
)
from policyengine_core.reforms import Reform
import warnings

Expand Down Expand Up @@ -455,6 +458,9 @@ def create_structural_reforms_from_parameters(parameters, period):
ut_fully_refundable_eitc = create_ut_fully_refundable_eitc_reform(
parameters, period
)
sc_fully_refundable_eitc = create_sc_fully_refundable_eitc_reform(
parameters, period
)
nj_stay_nj = create_nj_stay_nj_reform(parameters, period)
nj_anchor = create_nj_anchor_reform(parameters, period)
working_parents_tax_relief_act = create_working_parents_tax_relief_act_reform(
Expand Down Expand Up @@ -570,6 +576,7 @@ def create_structural_reforms_from_parameters(parameters, period):
mo_refundable_eitc,
oh_refundable_eitc,
ut_fully_refundable_eitc,
sc_fully_refundable_eitc,
nj_stay_nj,
nj_anchor,
working_parents_tax_relief_act,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .sc_fully_refundable_eitc_reform import (
create_sc_fully_refundable_eitc,
create_sc_fully_refundable_eitc_reform,
sc_fully_refundable_eitc,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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_sc_fully_refundable_eitc() -> Reform:
"""
South Carolina Fully Refundable EITC Reform

Converts the South Carolina EITC from nonrefundable to fully refundable
for ALL filers. SC's EITC is 125% of the federal EITC (subject to a
per-filer cap — $200 from 2026) and is nonrefundable by default, i.e.
applied only up to remaining state income tax liability. This reform pays
the potential (uncapped-at-liability) credit as a refundable credit, so
zero-liability filers receive it.

Mirrors the corrected Utah/Missouri/Ohio refundability reforms
(PolicyEngine/policyengine-us#8645, #8642, #8657): pay the ``*_potential``
amount, rebuild the non-refundable bucket with the EITC filtered out of
the ordered cap walk, and clear the inherited ``adds`` on the refundable
aggregate before giving it a formula.

Activated by
``gov.contrib.states.sc.child_poverty_impact_dashboard.eitc.in_effect``.

Reference: SC Code Section 12-6-3632.
"""

class sc_fully_refundable_eitc(Variable):
value_type = float
entity = TaxUnit
label = "South Carolina fully refundable EITC"
unit = USD
definition_period = YEAR
defined_for = StateCode.SC

def formula(tax_unit, period, parameters):
# Pay the potential (uncapped-at-liability) SC EITC so the whole
# credit is refundable; ``sc_eitc`` is capped at tax liability and
# would zero out for the low-liability filers refundability is
# meant to help. ``sc_eitc_potential`` still applies the statutory
# per-filer cap (gov.states.sc.tax.income.credits.eitc.max).
return tax_unit("sc_eitc_potential", period)

class sc_non_refundable_credits(Variable):
value_type = float
entity = TaxUnit
label = "South Carolina non-refundable credits"
unit = USD
definition_period = YEAR
defined_for = StateCode.SC

def formula(tax_unit, period, parameters):
# Mirror the baseline ordered-cap walk but drop sc_eitc from the
# non-refundable bucket — it is paid as a refundable credit under
# this reform. A raw ``sum - sc_eitc`` would overstate the total
# whenever the bucket binds at liability, so re-run the ordered
# walk over the filtered list instead.
ordered_credits = parameters(
period
).gov.states.sc.tax.income.credits.non_refundable
filtered_credits = [
credit for credit in list(ordered_credits) if credit != "sc_eitc"
]
return ordered_capped_state_non_refundable_credits(
tax_unit,
period,
filtered_credits,
"sc_income_tax_before_non_refundable_credits",
)

class sc_refundable_credits(Variable):
value_type = float
entity = TaxUnit
label = "South Carolina refundable credits"
unit = USD
definition_period = YEAR
defined_for = StateCode.SC
# The baseline computes via ``adds``. We replace it with a formula, so
# clear the inherited computation modes to avoid mixing ``formula``
# with ``adds``/``subtracts`` (rejected by the core engine).
adds = None
subtracts = None

def formula(tax_unit, period, parameters):
p = parameters(period).gov.states.sc.tax.income.credits
standard_credits = add(tax_unit, period, p.refundable)
refundable_eitc = tax_unit("sc_fully_refundable_eitc", period)
return standard_credits + refundable_eitc

class reform(Reform):
def apply(self):
self.update_variable(sc_fully_refundable_eitc)
self.update_variable(sc_non_refundable_credits)
self.update_variable(sc_refundable_credits)

return reform


def create_sc_fully_refundable_eitc_reform(parameters, period, bypass: bool = False):
if bypass:
return create_sc_fully_refundable_eitc()

p = parameters.gov.contrib.states.sc.child_poverty_impact_dashboard.eitc

reform_active = False
current_period = period_(period)

for i in range(5):
if p(current_period).in_effect:
reform_active = True
break
current_period = current_period.offset(1, "year")

if reform_active:
return create_sc_fully_refundable_eitc()
else:
return None


sc_fully_refundable_eitc = create_sc_fully_refundable_eitc_reform(
None, None, bypass=True
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"sc_cdcc": {
"reforms/states/sc/h3492/sc_h3492_eitc_refundable.py",
},
"sc_eitc": {
"reforms/states/sc/child_poverty_eitc/sc_fully_refundable_eitc_reform.py",
},
"sc_two_wage_earner_credit": {
"reforms/states/sc/h3492/sc_h3492_eitc_refundable.py",
},
Expand Down
62 changes: 62 additions & 0 deletions policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Tests for the South Carolina fully refundable EITC contrib reform.
#
# The reform converts SC's nonrefundable EITC (125% of the federal EITC, capped
# per filer at $200 from 2026) to fully refundable for all filers. It pays the
# potential (uncapped-at-liability) credit, so zero-liability filers receive it.
# These tests drive the credit from the federal EITC and pin SC tax liability
# directly. Mirrors the corrected UT/MO/OH reforms (issues #8644/#8640/#8656).

- name: Pays the full potential SC EITC as a refundable credit at zero liability
period: 2026
reforms: policyengine_us.reforms.states.sc.child_poverty_eitc.sc_fully_refundable_eitc_reform.sc_fully_refundable_eitc
input:
state_code: SC
eitc: 100
# Pin SC tax liability to zero so the nonrefundable cap binds at $0 — the
# test is whether the reform pays the credit despite no liability.
sc_income_tax_before_non_refundable_credits: 0
output:
# 2026 SC EITC is 125% of the federal EITC: 100 * 1.25 = 125, below the
# $200 per-filer cap.
sc_eitc_potential: 125
# The capped credit is 0 at zero liability; the reform pays the uncapped
# potential instead — the whole point of the conversion.
sc_eitc: 0
# Paid in full as refundable, even with no SC tax liability.
sc_fully_refundable_eitc: 125
sc_refundable_credits: 125

- name: Does not double count when liability could absorb the EITC
period: 2026
reforms: policyengine_us.reforms.states.sc.child_poverty_eitc.sc_fully_refundable_eitc_reform.sc_fully_refundable_eitc
input:
state_code: SC
eitc: 100
sc_income_tax_before_non_refundable_credits: 1_000
output:
sc_fully_refundable_eitc: 125
# EITC is removed from the non-refundable bucket (no other SC credits here).
sc_non_refundable_credits: 0
sc_refundable_credits: 125

- name: Respects the statutory per-filer cap ($200 in 2026)
period: 2026
reforms: policyengine_us.reforms.states.sc.child_poverty_eitc.sc_fully_refundable_eitc_reform.sc_fully_refundable_eitc
input:
state_code: SC
eitc: 1_000
sc_income_tax_before_non_refundable_credits: 0
output:
# 1,000 * 1.25 = 1,250, capped at the $200 maximum.
sc_eitc_potential: 200
sc_fully_refundable_eitc: 200
sc_refundable_credits: 200

- name: No effect outside South Carolina
period: 2026
reforms: policyengine_us.reforms.states.sc.child_poverty_eitc.sc_fully_refundable_eitc_reform.sc_fully_refundable_eitc
input:
state_code: CA
eitc: 100
output:
sc_fully_refundable_eitc: 0
Loading