From 112bd2e16fd2a30089973ed517649be54b3be2bf Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Mon, 15 Jun 2026 18:31:44 -0400 Subject: [PATCH 1/4] Fix MO refundable EITC reform crash and zero-refund bug The bundled Missouri refundable-EITC contrib reform crashed at calculation time and, once running, paid no refundable credit to zero-liability filers. Three issues fixed in mo_refundable_eitc_reform.py: 1. mo_refundable_credits passed a parameter path string to add(), which iterates it character by character and raises AttributeError. Resolve the parameter to its list of variable names first. 2. mo_refundable_credits redeclared a formula but inherited adds from the baseline, tripping the core engine's mixed computation-mode check. Set adds = None and subtracts = None explicitly. 3. mo_refundable_wftc returned mo_wftc, the credit capped at tax liability, so the reform paid $0 refundable for low-liability filers. Use mo_wftc_potential so the full credit is refundable. Fixes #8640 Co-Authored-By: Claude Opus 4.8 (1M context) --- ...x-mo-refundable-eitc-reform-crash.fixed.md | 1 + .../mo/eitc/mo_refundable_eitc_reform.py | 22 +++++++++---- .../policy/reform/mo_refundable_eitc.yaml | 32 +++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fix-mo-refundable-eitc-reform-crash.fixed.md create mode 100644 policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml diff --git a/changelog.d/fix-mo-refundable-eitc-reform-crash.fixed.md b/changelog.d/fix-mo-refundable-eitc-reform-crash.fixed.md new file mode 100644 index 00000000000..5a8f821d577 --- /dev/null +++ b/changelog.d/fix-mo-refundable-eitc-reform-crash.fixed.md @@ -0,0 +1 @@ +Fixed the Missouri refundable EITC contrib reform, which crashed at calculation time and paid no refundable credit to filers with no Missouri tax liability. diff --git a/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py b/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py index eb7d30309eb..d0f2b562ca4 100644 --- a/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py +++ b/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py @@ -20,7 +20,10 @@ class mo_refundable_wftc(Variable): defined_for = StateCode.MO def formula(tax_unit, period, parameters): - return tax_unit("mo_wftc", period) + # Use the potential (uncapped) WFTC so the full credit is paid as + # a refund; `mo_wftc` is capped at tax liability and would zero out + # the credit for the low-liability filers refundability benefits. + return tax_unit("mo_wftc_potential", period) class mo_non_refundable_wftc(Variable): value_type = float @@ -53,14 +56,19 @@ class mo_refundable_credits(Variable): unit = USD definition_period = YEAR defined_for = StateCode.MO + # The baseline variable 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): - # Standard refundable credits - other_refundable = add( - tax_unit, - period, - "gov.states.mo.tax.income.credits.refundable", - ) + # Standard refundable credits, resolved to the list of variable + # names before passing to `add` (which iterates variable names). + refundable_credits = parameters( + period + ).gov.states.mo.tax.income.credits.refundable + other_refundable = add(tax_unit, period, refundable_credits) # Add refundable WFTC (positive when reform is in effect) refundable_wftc = tax_unit("mo_refundable_wftc", period) return other_refundable + refundable_wftc diff --git a/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml b/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml new file mode 100644 index 00000000000..e01474efa92 --- /dev/null +++ b/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml @@ -0,0 +1,32 @@ +# Tests for the Missouri refundable EITC (WFTC) contrib reform. +# Regression coverage for https://github.com/PolicyEngine/policyengine-us/issues/8640 +# (the reform previously crashed at calculation time and, once running, paid no +# refundable credit to zero-liability filers). + +- name: Reform pays the full potential WFTC as a refundable credit at zero liability + period: 2026 + reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc + input: + state_code: MO + eitc: 5_000 + output: + # 2026 WFTC match is 20% of the federal EITC: 5,000 * 0.2 = 1,000. + mo_wftc_potential: 1_000 + # WFTC is moved out of the nonrefundable bucket... + mo_non_refundable_wftc: 0 + # ...and paid in full as refundable, even with no Missouri tax liability. + mo_refundable_wftc: 1_000 + mo_refundable_credits: 1_000 + +- name: Reform does not double count when liability already absorbs the WFTC + period: 2026 + reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc + input: + state_code: MO + eitc: 5_000 + mo_income_tax_before_credits: 1_000 + output: + # Full potential still paid as refundable; nonrefundable portion stays zero. + mo_refundable_wftc: 1_000 + mo_non_refundable_wftc: 0 + mo_refundable_credits: 1_000 From c180b2647f23fdd697e4ad3fddeec190cc1fa402 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Mon, 15 Jun 2026 19:12:13 -0400 Subject: [PATCH 2/4] Address review: strengthen MO reform test assertions - Case 1 now asserts mo_wftc: 0 (capped) alongside mo_refundable_wftc: 1000 (uncapped), so the capped-vs-uncapped contrast that is the actual fix is guarded; reverting the fix would now fail the test. - Case 2 now asserts mo_income_tax: 0 and mo_income_tax_before_refundable_ credits: 1000, genuinely verifying the credit offsets liability exactly once (no double counting) rather than re-asserting case 1's outputs. - Fix grammar in the mo_refundable_wftc comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../states/mo/eitc/mo_refundable_eitc_reform.py | 3 ++- .../tests/policy/reform/mo_refundable_eitc.yaml | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py b/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py index d0f2b562ca4..477068a24f9 100644 --- a/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py +++ b/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py @@ -22,7 +22,8 @@ class mo_refundable_wftc(Variable): def formula(tax_unit, period, parameters): # Use the potential (uncapped) WFTC so the full credit is paid as # a refund; `mo_wftc` is capped at tax liability and would zero out - # the credit for the low-liability filers refundability benefits. + # the credit for the low-liability filers refundability is meant + # to help. return tax_unit("mo_wftc_potential", period) class mo_non_refundable_wftc(Variable): diff --git a/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml b/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml index e01474efa92..56af35692c4 100644 --- a/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml +++ b/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml @@ -12,8 +12,12 @@ output: # 2026 WFTC match is 20% of the federal EITC: 5,000 * 0.2 = 1,000. mo_wftc_potential: 1_000 + # 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. + mo_wftc: 0 # WFTC is moved out of the nonrefundable bucket... mo_non_refundable_wftc: 0 + mo_non_refundable_credits: 0 # ...and paid in full as refundable, even with no Missouri tax liability. mo_refundable_wftc: 1_000 mo_refundable_credits: 1_000 @@ -26,7 +30,12 @@ eitc: 5_000 mo_income_tax_before_credits: 1_000 output: - # Full potential still paid as refundable; nonrefundable portion stays zero. + # Full potential paid as refundable; nonrefundable portion stays zero. mo_refundable_wftc: 1_000 mo_non_refundable_wftc: 0 mo_refundable_credits: 1_000 + # The $1,000 liability is offset exactly once by the refundable credit: + # 1,000 before refundable credits - 1,000 refundable = 0. If the credit + # were double counted this would go negative. + mo_income_tax_before_refundable_credits: 1_000 + mo_income_tax: 0 From 75ce33dbb14725262360e9b069d7ca0ce492d4b5 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Mon, 15 Jun 2026 19:53:02 -0400 Subject: [PATCH 3/4] Update pre-existing MO reform tests for uncapped refundable WFTC The refundability fix (mo_refundable_wftc now reads mo_wftc_potential) broke two pre-existing tests that encoded the old capped behavior: - The contrib reform test injected mo_wftc directly and asserted the refundable amount equaled it. Rewrite it to drive the credit from the federal EITC and assert the full uncapped potential is paid (with mo_wftc capped to 0 at zero liability), which is what the fix corrects. - The applied-credit downstream-consumer guard listed the reform as a consumer of the applied mo_wftc. The reform now consumes mo_wftc_potential instead - exactly the pattern the guard recommends - so drop the stale entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- ..._refundable_credit_downstream_consumers.py | 3 --- .../eitc/mo_refundable_eitc.yaml | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) 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 9ea675c3d41..b91400e3842 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 @@ -21,9 +21,6 @@ "md_non_refundable_eitc": { "variables/gov/states/md/tax/income/credits/eitc/md_eitc.py", }, - "mo_wftc": { - "reforms/states/mo/eitc/mo_refundable_eitc_reform.py", - }, "ny_household_credit": { "reforms/states/ny/wftc/ny_working_families_tax_credit.py", "variables/gov/states/ny/tax/income/credits/ny_eitc.py", diff --git a/policyengine_us/tests/policy/contrib/states/mo/child_poverty_impact_dashboard/eitc/mo_refundable_eitc.yaml b/policyengine_us/tests/policy/contrib/states/mo/child_poverty_impact_dashboard/eitc/mo_refundable_eitc.yaml index 0b970f6670d..9145dbcb701 100644 --- a/policyengine_us/tests/policy/contrib/states/mo/child_poverty_impact_dashboard/eitc/mo_refundable_eitc.yaml +++ b/policyengine_us/tests/policy/contrib/states/mo/child_poverty_impact_dashboard/eitc/mo_refundable_eitc.yaml @@ -1,19 +1,26 @@ -- name: Case 1 - MO WFTC is refundable when reform active +# The reform makes the Missouri WFTC fully refundable. It pays the uncapped +# potential credit (mo_wftc_potential), so these tests drive the credit from +# the federal EITC rather than injecting the capped mo_wftc. The 2024 WFTC +# match is 20% of the federal EITC. + +- name: Case 1 - MO WFTC is fully refundable (uncapped) when reform active period: 2024 reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc input: state_code: MO - mo_wftc: 500 + eitc: 2_500 output: - mo_refundable_wftc: 500 + mo_wftc_potential: 500 # 2,500 * 0.2 + mo_wftc: 0 # capped at zero tax liability + mo_refundable_wftc: 500 # full potential paid as a refund mo_non_refundable_wftc: 0 -- name: Case 2 - MO WFTC is 0 when WFTC is 0 +- name: Case 2 - MO refundable WFTC is 0 when the federal EITC is 0 period: 2024 reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc input: state_code: MO - mo_wftc: 0 + eitc: 0 output: mo_refundable_wftc: 0 mo_non_refundable_wftc: 0 @@ -23,7 +30,7 @@ reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc input: state_code: CA - mo_wftc: 500 + eitc: 2_500 output: mo_refundable_wftc: 0 mo_non_refundable_wftc: 0 @@ -33,6 +40,6 @@ reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc input: state_code: MO - mo_wftc: 500 + eitc: 2_500 output: mo_non_refundable_credits: 0 From 2fdc9234a520984394f4c83cb04d2253bd80cb9b Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Tue, 16 Jun 2026 23:33:12 -0400 Subject: [PATCH 4/4] Address PR review: multi-credit test, boundary cases, forward-compat note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DTrim99 review on #8642: - Add a multi-refundable-credit regression test (mo_property_tax_credit pinned to a positive value) that proves the reform's mo_refundable_credits formula sums all entries in `gov.states.mo.tax.income.credits.refundable` alongside mo_refundable_wftc rather than replacing them. Closes the silent-regression gap the original tests didn't cover. - Add zero-EITC boundary case (potential WFTC = 0) and partial-liability case (liability=400, potential=1,000) to pin down the capped-vs-uncapped refund boundary; the partial case asserts mo_income_tax=-600 (the refund flowing through), guarding against a future double-count regression that would land at -1,000. - Add `absolute_error_margin: 0.01` to every currency assertion in the new reform-test yaml, matching project convention. - Add forward-compat comment on the reform's mo_non_refundable_credits formula: today the bucket is just `[mo_wftc]` so the simplified formula is equivalent to the baseline, but if MO adds a second nonrefundable credit later this would silently drop it — at that point switch to the UT/OH pattern (ordered-cap walk with mo_wftc filtered out). Co-Authored-By: Claude Opus 4.7 --- .../mo/eitc/mo_refundable_eitc_reform.py | 7 ++- .../policy/reform/mo_refundable_eitc.yaml | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py b/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py index 477068a24f9..b87586aba11 100644 --- a/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py +++ b/policyengine_us/reforms/states/mo/eitc/mo_refundable_eitc_reform.py @@ -47,7 +47,12 @@ class mo_non_refundable_credits(Variable): defined_for = StateCode.MO def formula(tax_unit, period, parameters): - # Include the nonrefundable WFTC (0 when reform is in effect) + # Today `gov.states.mo.tax.income.credits.non_refundable` is + # just `[mo_wftc]`, so returning the (zeroed) reform replacement + # is equivalent to the baseline. If Missouri later adds a second + # nonrefundable credit, this formula would silently drop it — + # at that point switch to summing the full nonrefundable list + # with `mo_wftc` filtered out, mirroring the UT/OH fix pattern. return tax_unit("mo_non_refundable_wftc", period) class mo_refundable_credits(Variable): diff --git a/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml b/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml index 56af35692c4..7aeee17986b 100644 --- a/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml +++ b/policyengine_us/tests/policy/reform/mo_refundable_eitc.yaml @@ -4,6 +4,7 @@ # refundable credit to zero-liability filers). - name: Reform pays the full potential WFTC as a refundable credit at zero liability + absolute_error_margin: 0.01 period: 2026 reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc input: @@ -23,6 +24,7 @@ mo_refundable_credits: 1_000 - name: Reform does not double count when liability already absorbs the WFTC + absolute_error_margin: 0.01 period: 2026 reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc input: @@ -39,3 +41,53 @@ # were double counted this would go negative. mo_income_tax_before_refundable_credits: 1_000 mo_income_tax: 0 + +- name: Reform preserves other Missouri refundable credits (does not drop them) + absolute_error_margin: 0.01 + period: 2026 + reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc + input: + state_code: MO + eitc: 5_000 + # The other Missouri refundable credit in + # `gov.states.mo.tax.income.credits.refundable` is mo_property_tax_credit. + # Pin it to a positive value: the reform's mo_refundable_credits formula + # must sum it alongside mo_refundable_wftc, not replace it. + mo_property_tax_credit: 300 + output: + mo_refundable_wftc: 1_000 + # 1,000 (refundable WFTC) + 300 (property tax credit) = 1,300. + mo_refundable_credits: 1_300 + +- name: Reform pays zero when the federal EITC is zero (zero-EITC boundary) + absolute_error_margin: 0.01 + period: 2026 + reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc + input: + state_code: MO + eitc: 0 + output: + mo_wftc_potential: 0 + mo_refundable_wftc: 0 + mo_refundable_credits: 0 + +- name: Reform pays in full when liability only partially absorbs the WFTC + absolute_error_margin: 0.01 + period: 2026 + reforms: policyengine_us.reforms.states.mo.eitc.mo_refundable_eitc_reform.mo_refundable_eitc + input: + state_code: MO + eitc: 5_000 + # Potential WFTC = 1,000. Liability of 400 would absorb only part of the + # baseline (nonrefundable-capped) WFTC; the reform should still pay the + # full uncapped potential as refundable. + mo_income_tax_before_credits: 400 + output: + mo_wftc_potential: 1_000 + mo_refundable_wftc: 1_000 + mo_refundable_credits: 1_000 + # The full $1,000 refundable credit flows through against $400 of + # liability, producing a $600 refund. mo_income_tax goes negative + # (refund) by exactly the surplus — proves no double-counting (would + # be -$1,000 if the credit were applied twice). + mo_income_tax: -600