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
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