From 3a0a40bfcc5bf445236c2f7ef747ec3fda4157f0 Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Thu, 18 Jun 2026 14:34:07 -0400 Subject: [PATCH 1/2] Add South Carolina fully refundable EITC contrib reform Adds a Child Poverty Impact Dashboard contrib reform that converts South Carolina's nonrefundable EITC (125% of the federal EITC, capped per filer at $200 from 2026) to fully refundable for all filers, activated by gov.contrib.states.sc.child_poverty_impact_dashboard.eitc.in_effect. Mirrors the corrected Utah/Missouri/Ohio refundability reforms (#8645/#8642/ #8657): pays sc_eitc_potential (uncapped at liability) so zero-liability filers receive the credit, rebuilds sc_non_refundable_credits via the ordered cap walk with sc_eitc filtered out (no add() path-string bug), and clears the inherited adds/subtracts on sc_refundable_credits before giving it a formula. The statutory per-filer cap (gov.states.sc.tax.income.credits.eitc.max) is preserved and remains separately adjustable. Tested: 4 YAML cases (zero-liability refund, no double-count with liability, $200 cap respected, SC-only) plus an end-to-end check that the in_effect param activates the reform and raises a zero-liability filer's net income. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sc-fully-refundable-eitc-reform.added.md | 1 + .../eitc/in_effect.yaml | 9 ++ policyengine_us/reforms/reforms.py | 7 + .../states/sc/child_poverty_eitc/__init__.py | 5 + .../sc_fully_refundable_eitc_reform.py | 125 ++++++++++++++++++ .../reform/sc_fully_refundable_eitc.yaml | 62 +++++++++ 6 files changed, 209 insertions(+) create mode 100644 changelog.d/sc-fully-refundable-eitc-reform.added.md create mode 100644 policyengine_us/parameters/gov/contrib/states/sc/child_poverty_impact_dashboard/eitc/in_effect.yaml create mode 100644 policyengine_us/reforms/states/sc/child_poverty_eitc/__init__.py create mode 100644 policyengine_us/reforms/states/sc/child_poverty_eitc/sc_fully_refundable_eitc_reform.py create mode 100644 policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml diff --git a/changelog.d/sc-fully-refundable-eitc-reform.added.md b/changelog.d/sc-fully-refundable-eitc-reform.added.md new file mode 100644 index 00000000000..561c081e696 --- /dev/null +++ b/changelog.d/sc-fully-refundable-eitc-reform.added.md @@ -0,0 +1 @@ +Added a South Carolina fully refundable EITC contrib reform for the Child Poverty Impact Dashboard. diff --git a/policyengine_us/parameters/gov/contrib/states/sc/child_poverty_impact_dashboard/eitc/in_effect.yaml b/policyengine_us/parameters/gov/contrib/states/sc/child_poverty_impact_dashboard/eitc/in_effect.yaml new file mode 100644 index 00000000000..79565becd17 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/sc/child_poverty_impact_dashboard/eitc/in_effect.yaml @@ -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 diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 1cb63204dce..1c68affe08b 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -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 @@ -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( @@ -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, diff --git a/policyengine_us/reforms/states/sc/child_poverty_eitc/__init__.py b/policyengine_us/reforms/states/sc/child_poverty_eitc/__init__.py new file mode 100644 index 00000000000..a390f12c33f --- /dev/null +++ b/policyengine_us/reforms/states/sc/child_poverty_eitc/__init__.py @@ -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, +) diff --git a/policyengine_us/reforms/states/sc/child_poverty_eitc/sc_fully_refundable_eitc_reform.py b/policyengine_us/reforms/states/sc/child_poverty_eitc/sc_fully_refundable_eitc_reform.py new file mode 100644 index 00000000000..e7f9a73f6d3 --- /dev/null +++ b/policyengine_us/reforms/states/sc/child_poverty_eitc/sc_fully_refundable_eitc_reform.py @@ -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 +) diff --git a/policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml b/policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml new file mode 100644 index 00000000000..b8d37dc0c4e --- /dev/null +++ b/policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml @@ -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 From 0a1891d7b6e48ff340735b38e661b89323ed3578 Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Thu, 18 Jun 2026 17:29:06 -0400 Subject: [PATCH 2/2] Register SC reform's sc_eitc reference in the applied-credit review allowlist The fully refundable SC EITC reform filters sc_eitc out of the ordered non-refundable list by name, which the applied-credit downstream-consumer code-health guardrail flags as a new consumer. Add the reviewed entry, as done for the UT/OH refundability reforms. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_non_refundable_credit_downstream_consumers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py index b91400e3842..21b38057eca 100644 --- a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py +++ b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py @@ -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", },