diff --git a/changelog.d/codex-issue-7897-taxsim-umbrella-vars.fixed.md b/changelog.d/codex-issue-7897-taxsim-umbrella-vars.fixed.md new file mode 100644 index 00000000000..f2471ae2d89 --- /dev/null +++ b/changelog.d/codex-issue-7897-taxsim-umbrella-vars.fixed.md @@ -0,0 +1 @@ +Improved legacy state credit umbrella compatibility for TAXSIM by splitting Oklahoma and Minnesota combined credits into explicit component variables and adding regression coverage. diff --git a/policyengine_us/parameters/gov/states/household/state_cdccs.yaml b/policyengine_us/parameters/gov/states/household/state_cdccs.yaml index a7940395509..7b8451ba60c 100644 --- a/policyengine_us/parameters/gov/states/household/state_cdccs.yaml +++ b/policyengine_us/parameters/gov/states/household/state_cdccs.yaml @@ -17,6 +17,7 @@ values: - la_refundable_cdcc # Louisiana - ma_dependent_care_credit # Massachusetts - md_cdcc # Maryland + - md_refundable_cdcc # Maryland (refundable component) - me_child_care_credit # Maine - mn_cdcc # Minnesota - ms_cdcc # Mississippi @@ -26,7 +27,7 @@ values: - nm_cdcc # New Mexico - ny_cdcc # New York - oh_cdcc # Ohio - - ok_child_care_child_tax_credit # Oklahoma Child Care/Child Tax Credit - note, also in state_ctcs. + - ok_child_care_credit_component # Oklahoma Child Care Credit component - or_working_family_household_and_dependent_care_credit # Oregon - pa_cdcc # Pennsylvania - ri_cdcc # Rhode Island @@ -34,6 +35,7 @@ values: - vt_cdcc # Vermont - vt_low_income_cdcc # Vermont low-income CDCC - wi_childcare_expense_credit # Wisconsin + - wv_cdcc # West Virginia metadata: unit: list diff --git a/policyengine_us/parameters/gov/states/household/state_ctcs.yaml b/policyengine_us/parameters/gov/states/household/state_ctcs.yaml index 1a6b6b2c1c6..56fc0f0375f 100644 --- a/policyengine_us/parameters/gov/states/household/state_ctcs.yaml +++ b/policyengine_us/parameters/gov/states/household/state_ctcs.yaml @@ -14,12 +14,13 @@ values: - ma_child_and_family_credit_or_dependent_care_credit # Massachusetts Child and Family Tax Credit - md_ctc # Maryland Child Tax Credit - me_dependent_exemption_credit # Maine Dependent Exemption Credit - - mn_child_and_working_families_credits # Minnesota Child and Working Family Credits + - mn_child_tax_credit_component # Minnesota Child Tax Credit component + - nc_ctc # North Carolina Child Tax Credit - ne_refundable_ctc # Nebraska Refundable Child Tax Credit - nj_ctc # New Jersey Child Tax Credit - nm_ctc # New Mexico Child Tax Credit - ny_ctc # New York Child Tax Credit (Empire State Child Credit) - - ok_child_care_child_tax_credit # Oklahoma Child Care/Child Tax Credit - also in state_cdccs. + - ok_child_tax_credit_component # Oklahoma Child Tax Credit component - or_ctc # Oregon Child Tax Credit (Oregon Kids Credit) - ri_child_tax_rebate # Rhode Island Child Tax Rebate - ut_ctc # Utah Child Tax Credit diff --git a/policyengine_us/parameters/gov/states/household/state_eitcs.yaml b/policyengine_us/parameters/gov/states/household/state_eitcs.yaml index 6be6966b483..88ba716b4a2 100644 --- a/policyengine_us/parameters/gov/states/household/state_eitcs.yaml +++ b/policyengine_us/parameters/gov/states/household/state_eitcs.yaml @@ -1,4 +1,4 @@ -description: All state Earned Income Tax Credits. +description: All state Earned Income Tax Credits and related credits. values: 0000-01-01: - ca_eitc # California @@ -17,7 +17,7 @@ values: - md_refundable_eitc # Maryland refundable EITC - me_eitc # Maine - mi_eitc # Michigan - - mn_wfc # Minnesota (called "Working Family Credit") repealed in 2023 + - mn_wfc # Minnesota (called "Working Family Credit") - mo_wftc # Missouri (called "Working Families Tax Credit") - mt_eitc # Montana - ne_eitc # Nebraska diff --git a/policyengine_us/parameters/gov/states/household/state_property_tax_credits.yaml b/policyengine_us/parameters/gov/states/household/state_property_tax_credits.yaml index 6a9b8dfc6a4..36e52bc66ae 100644 --- a/policyengine_us/parameters/gov/states/household/state_property_tax_credits.yaml +++ b/policyengine_us/parameters/gov/states/household/state_property_tax_credits.yaml @@ -6,14 +6,17 @@ values: # Exclude ca_renter_credit as it is for renters, not homeowners. - dc_ptc # DC Property Tax Credit # Exclude hi_tax_credit_for_low_income_household_renters as it is for renters, not homeowners. + - il_property_tax_credit # Illinois Property Tax Credit - ma_senior_circuit_breaker # Massachusetts Senior Circuit Breaker Credit - me_property_tax_fairness_credit # Maine Property Tax Fairness Credit - mi_homestead_property_tax_credit # Michigan homestead property tax credit - mo_property_tax_credit # Missouri property tax credit - mt_elderly_homeowner_or_renter_credit # Montana Elderly Homeowner/Renter Credit + - mt_property_tax_rebate # Montana Property Tax Rebate - nj_property_tax_credit # New Jersey property tax credit - nm_property_tax_rebate # New Mexico property tax rebate - ny_real_property_tax_credit # New York real property tax credit + - ok_ptc # Oklahoma Property Tax Credit - ri_property_tax_credit # Rhode Island property tax credit # Omit vt_renter_credit - wi_homestead_credit # Wisconsin homestead credit diff --git a/policyengine_us/parameters/gov/states/household/state_taxable_incomes.yaml b/policyengine_us/parameters/gov/states/household/state_taxable_incomes.yaml index 8f4944387e4..3e0c4708e20 100644 --- a/policyengine_us/parameters/gov/states/household/state_taxable_incomes.yaml +++ b/policyengine_us/parameters/gov/states/household/state_taxable_incomes.yaml @@ -29,12 +29,14 @@ values: - nc_taxable_income # North Carolina - nd_taxable_income # North Dakota - ne_taxable_income # Nebraska + - nh_taxable_income # New Hampshire - nj_taxable_income # New Jersey - nm_taxable_income # New Mexico - ny_taxable_income # New York - oh_taxable_income # Ohio - ok_taxable_income # Oklahoma - or_taxable_income # Oregon + - pa_total_taxable_income # Pennsylvania - ri_taxable_income # Rhode Island - sc_taxable_income # South Carolina - ut_taxable_income # Utah diff --git a/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py b/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py new file mode 100644 index 00000000000..cbe7018b16a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py @@ -0,0 +1,211 @@ +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system + + +def make_tax_unit_situation( + *, + year: int, + state: str, + primary_age: int = 40, + wages: float = 0.0, + childcare: float = 0.0, + rent: float = 0.0, + dependent_ages: tuple[int, ...] = (), +): + year_str = str(year) + members = ["you", *[f"dependent_{i}" for i in range(1, len(dependent_ages) + 1)]] + + people = { + "you": { + "age": {year_str: primary_age}, + "employment_income": {year_str: wages}, + "is_tax_unit_head": {year_str: True}, + "is_tax_unit_spouse": {year_str: False}, + "is_tax_unit_dependent": {year_str: False}, + "ssi": {year_str: 0}, + "head_start": {year_str: 0}, + "early_head_start": {year_str: 0}, + "commodity_supplemental_food_program": {year_str: 0}, + } + } + + if rent: + people["you"]["rent"] = {year_str: rent} + + for index, age in enumerate(dependent_ages, start=1): + name = f"dependent_{index}" + people[name] = { + "age": {year_str: age}, + "employment_income": {year_str: 0}, + "is_tax_unit_head": {year_str: False}, + "is_tax_unit_spouse": {year_str: False}, + "is_tax_unit_dependent": {year_str: True}, + "ssi": {year_str: 0}, + "head_start": {year_str: 0}, + "early_head_start": {year_str: 0}, + "commodity_supplemental_food_program": {year_str: 0}, + } + + tax_unit = {"members": members} + if childcare: + tax_unit["tax_unit_childcare_expenses"] = {year_str: childcare} + + marital_units = {"your marital unit": {"members": ["you"]}} + for index in range(1, len(dependent_ages) + 1): + marital_units[f"dependent_{index}_marital_unit"] = { + "members": [f"dependent_{index}"], + "marital_unit_id": {year_str: index}, + } + + return { + "people": people, + "families": {"your family": {"members": members}}, + "households": { + "your household": { + "members": members, + "state_name": {year_str: state}, + } + }, + "tax_units": {"your tax unit": tax_unit}, + "spm_units": { + "your household": { + "members": members, + "snap": {year_str: 0}, + "tanf": {year_str: 0}, + "free_school_meals": {year_str: 0}, + "reduced_price_school_meals": {year_str: 0}, + } + }, + "marital_units": marital_units, + } + + +def calculate_sum(situation: dict, year: int, variables: list[str]) -> float: + simulation = Simulation(situation=situation) + total = 0.0 + + for variable in variables: + result = simulation.calculate(variable, period=str(year)) + if result.size != 1: + result = simulation.calculate(variable, period=str(year), map_to="tax_unit") + total += float(result.item()) + + return total + + +def configured_component_vars(parameter_name: str, year: int) -> list[str]: + parameter = getattr(system.parameters.gov.states.household, parameter_name) + return list(parameter(f"{year}-01-01")) + + +LEGACY_UMBRELLA_CASES = [ + ( + "state_agi", + ["ok_agi"], + make_tax_unit_situation(year=2023, state="OK", wages=40_000.0), + ), + ( + "state_taxable_income", + ["ok_taxable_income"], + make_tax_unit_situation(year=2023, state="OK", wages=40_000.0), + ), + ( + "state_cdcc", + ["ok_child_care_credit_component"], + make_tax_unit_situation( + year=2023, + state="OK", + wages=40_000.0, + childcare=2_000.0, + dependent_ages=(5,), + ), + ), + ( + "state_eitc", + ["mn_wfc"], + make_tax_unit_situation( + year=2023, + state="MN", + wages=20_050.0, + dependent_ages=(9, 7), + ), + ), +] + + +@pytest.mark.parametrize( + ("legacy_var", "state_specific_vars", "situation"), LEGACY_UMBRELLA_CASES +) +def test_legacy_state_umbrellas_match_state_specific_components( + legacy_var, state_specific_vars, situation +): + expected = calculate_sum(situation, 2023, state_specific_vars) + actual = calculate_sum(situation, 2023, [legacy_var]) + assert actual == pytest.approx(expected, abs=0.01) + + +def test_state_ctc_matches_configured_component_sum(): + situation = make_tax_unit_situation( + year=2023, + state="MN", + wages=20_050.0, + dependent_ages=(9, 7), + ) + expected = calculate_sum( + situation, 2023, configured_component_vars("state_ctcs", 2023) + ) + actual = calculate_sum(situation, 2023, ["state_ctc"]) + assert actual == pytest.approx(expected, abs=0.01) + + +def test_state_property_tax_credit_matches_configured_component_sum(): + situation = make_tax_unit_situation( + year=2023, + state="ME", + primary_age=70, + wages=10_000.0, + rent=12_000.0, + ) + expected = calculate_sum( + situation, + 2023, + configured_component_vars("state_property_tax_credits", 2023), + ) + actual = calculate_sum(situation, 2023, ["state_property_tax_credit"]) + assert actual == pytest.approx(expected, abs=0.01) + + +@pytest.mark.parametrize( + ("component_vars", "combined_var", "situation"), + [ + ( + ["ok_child_care_credit_component", "ok_child_tax_credit_component"], + "ok_child_care_child_tax_credit", + make_tax_unit_situation( + year=2023, + state="OK", + wages=40_000.0, + childcare=2_000.0, + dependent_ages=(5,), + ), + ), + ( + ["mn_wfc", "mn_child_tax_credit_component"], + "mn_child_and_working_families_credits", + make_tax_unit_situation( + year=2023, + state="MN", + wages=20_050.0, + dependent_ages=(9, 7), + ), + ), + ], +) +def test_decomposed_credit_components_preserve_combined_credit_total( + component_vars, combined_var, situation +): + combined_total = calculate_sum(situation, 2023, [combined_var]) + decomposed_total = calculate_sum(situation, 2023, component_vars) + assert decomposed_total == pytest.approx(combined_total, abs=0.01) diff --git a/policyengine_us/variables/gov/states/mn/tax/income/credits/mn_child_tax_credit_component.py b/policyengine_us/variables/gov/states/mn/tax/income/credits/mn_child_tax_credit_component.py new file mode 100644 index 00000000000..2097a780558 --- /dev/null +++ b/policyengine_us/variables/gov/states/mn/tax/income/credits/mn_child_tax_credit_component.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class mn_child_tax_credit_component(Variable): + value_type = float + entity = TaxUnit + label = "Minnesota child tax credit component" + unit = USD + definition_period = YEAR + defined_for = StateCode.MN + + def formula(tax_unit, period, parameters): + combined_credit = tax_unit("mn_child_and_working_families_credits", period) + working_family_credit = tax_unit("mn_wfc", period) + return max_(0, combined_credit - working_family_credit) diff --git a/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_child_tax_credit.py b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_child_tax_credit.py index 10890514567..552a60b3451 100644 --- a/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_child_tax_credit.py +++ b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_child_tax_credit.py @@ -57,7 +57,6 @@ def formula(tax_unit, period, parameters): us_agi = tax_unit("adjusted_gross_income", period) agi_eligible = us_agi <= p.child.agi_limit # Step 2: Calculate OK CDCC amount (20% of federal potential) - # Oklahoma matches the potential federal credit, not the actual credit us_cdcc = tax_unit("cdcc_potential", period) ok_cdcc = us_cdcc * p.child.cdcc_fraction # Step 3: Calculate OK CTC amount (5% of federal CTC) @@ -65,10 +64,9 @@ def formula(tax_unit, period, parameters): ok_ctc = us_ctc * p.child.ctc_fraction # Step 4: Compute proration ratio (OK AGI / Federal AGI) ok_agi = tax_unit("ok_agi", period) - # Use a mask rather than where to avoid a divide-by-zero warning agi_ratio = np.zeros_like(us_agi) mask = us_agi != 0 agi_ratio[mask] = ok_agi[mask] / us_agi[mask] prorate = min_(1, max_(0, agi_ratio)) - # Step 5: Return greater of OK CDCC or OK CTC, prorated, if eligible + # Step 5: Return greater of OK CDCC or OK CTC, prorated return agi_eligible * prorate * max_(ok_cdcc, ok_ctc) diff --git a/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_credit_component.py b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_credit_component.py new file mode 100644 index 00000000000..e90385b68d6 --- /dev/null +++ b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_care_credit_component.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class ok_child_care_credit_component(Variable): + value_type = float + entity = TaxUnit + label = "Oklahoma child care credit component" + unit = USD + definition_period = YEAR + defined_for = StateCode.OK + + def formula(tax_unit, period, parameters): + # OK's combined credit is max(CDCC_portion, CTC_portion). + # This extracts the CDCC portion: combined minus CTC component. + combined = tax_unit("ok_child_care_child_tax_credit", period) + ctc = tax_unit("ok_child_tax_credit_component", period) + return max_(0, combined - ctc) diff --git a/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_tax_credit_component.py b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_tax_credit_component.py new file mode 100644 index 00000000000..5da04c155ba --- /dev/null +++ b/policyengine_us/variables/gov/states/ok/tax/income/credits/ok_child_tax_credit_component.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class ok_child_tax_credit_component(Variable): + value_type = float + entity = TaxUnit + label = "Oklahoma child tax credit component" + unit = USD + definition_period = YEAR + defined_for = StateCode.OK + + def formula(tax_unit, period, parameters): + # OK's combined credit is max(CDCC_portion, CTC_portion). + # This extracts the CTC portion by checking if CTC > CDCC. + p = parameters(period).gov.states.ok.tax.income.credits.child + us_cdcc = tax_unit("cdcc_potential", period) + ok_cdcc = us_cdcc * p.cdcc_fraction + us_ctc = tax_unit("ctc_value", period) + ok_ctc = us_ctc * p.ctc_fraction + ctc_larger_than_cdcc = ok_ctc > ok_cdcc + combined = tax_unit("ok_child_care_child_tax_credit", period) + return where(ctc_larger_than_cdcc, combined, 0) diff --git a/policyengine_us/variables/gov/states/tax/income/state_cdcc.py b/policyengine_us/variables/gov/states/tax/income/state_cdcc.py index 891e34e78ad..e5225fc3e56 100644 --- a/policyengine_us/variables/gov/states/tax/income/state_cdcc.py +++ b/policyengine_us/variables/gov/states/tax/income/state_cdcc.py @@ -4,7 +4,7 @@ class state_cdcc(Variable): value_type = float entity = TaxUnit - label = "State Child and Dependent Care Tax Credit" + label = "State child and dependent care tax credit" unit = USD definition_period = YEAR adds = "gov.states.household.state_cdccs" diff --git a/policyengine_us/variables/gov/states/tax/income/state_ctc.py b/policyengine_us/variables/gov/states/tax/income/state_ctc.py index 2f1742c1ebd..7f1f0a4e551 100644 --- a/policyengine_us/variables/gov/states/tax/income/state_ctc.py +++ b/policyengine_us/variables/gov/states/tax/income/state_ctc.py @@ -4,7 +4,7 @@ class state_ctc(Variable): value_type = float entity = TaxUnit - label = "State Child Tax Credit" + label = "State child tax credit" unit = USD definition_period = YEAR adds = "gov.states.household.state_ctcs" diff --git a/policyengine_us/variables/gov/states/tax/income/state_eitc.py b/policyengine_us/variables/gov/states/tax/income/state_eitc.py index 2d3eba35668..f0a005f842c 100644 --- a/policyengine_us/variables/gov/states/tax/income/state_eitc.py +++ b/policyengine_us/variables/gov/states/tax/income/state_eitc.py @@ -4,7 +4,7 @@ class state_eitc(Variable): value_type = float entity = TaxUnit - label = "State Earned Income Tax Credit" + label = "State earned income tax credit" unit = USD definition_period = YEAR adds = "gov.states.household.state_eitcs" diff --git a/policyengine_us/variables/gov/states/tax/income/state_property_tax_credit.py b/policyengine_us/variables/gov/states/tax/income/state_property_tax_credit.py index e35017e95e3..7969e760235 100644 --- a/policyengine_us/variables/gov/states/tax/income/state_property_tax_credit.py +++ b/policyengine_us/variables/gov/states/tax/income/state_property_tax_credit.py @@ -4,7 +4,7 @@ class state_property_tax_credit(Variable): value_type = float entity = TaxUnit - label = "State Property Tax Credit" + label = "State property tax credit" unit = USD definition_period = YEAR adds = "gov.states.household.state_property_tax_credits"