diff --git a/changelog.d/credit-recompute-branches-keep-inputs.fixed.md b/changelog.d/credit-recompute-branches-keep-inputs.fixed.md new file mode 100644 index 00000000000..ee83378701b --- /dev/null +++ b/changelog.d/credit-recompute-branches-keep-inputs.fixed.md @@ -0,0 +1 @@ +Keep plain inputs such as EITC take-up when New York and Alabama recompute federal credits under pinned rules, so a 2021 New York filer who does not take up the federal EITC no longer gets the New York EITC. diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml index 3587902612d..1d7847bfa54 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml @@ -286,3 +286,56 @@ dependent_care_assistance_exclusion: 9_000 cdcc: 3_500 al_federal_income_tax_deduction: 4_390 + +- name: 2021 head of household, one child - Part II recomputes the EITC only for a taker + period: 2021 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 41_000 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + takes_up_eitc: true + households: + household: + members: [parent, child] + state_code: AL + output: + # Federal tax: $41,000 - $18,800 standard deduction = $22,200 taxable; + # 10% x 14,200 + 12% x 8,000 = $2,380. + income_tax_before_refundable_credits: 2_380 + # Part I: 2,380 - 3,600 refundable CTC - 185.50 EITC < 0, so 0. + # Part II (2020 IRC): 2,380 - 2,000 nonrefundable 2020 CTC = 380 (line 6); + # 2020 EITC 3,584 - 15.98% x (41,000 - 19,330) = 121.13; the 2020 CTC is + # all nonrefundable here, so 380 - 121.13 = 258.87. + al_federal_income_tax_deduction: 258.87 + +- name: 2021 head of household, one child, no EITC take-up - Part II recomputes no EITC + period: 2021 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 41_000 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + takes_up_eitc: false + households: + household: + members: [parent, child] + state_code: AL + output: + eitc: 0 + # Part I: 2,380 - 3,600 refundable CTC < 0, so 0. Part II: line 6 is + # 380 and the 2020-rule EITC is zero for a filer who does not take it + # up, so 380. + al_federal_income_tax_deduction: 380 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml index 7ac587c9883..2ea0c7a1f73 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_ctc_pre_2024.yaml @@ -405,3 +405,58 @@ output: ny_ctc_pre_2024_eligible: false ny_ctc_pre_2024: 0 + +# The pre-TCJA branch copies the parent's cache, so this case lists the +# current-law federal credit first: the branch must recompute it, not read it. +- name: 2023 pre-TCJA credit recomputes the federal CTC after the current-law CTC + period: 2023 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 30_000 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NY + output: + # Current law: $2,000 per child. + ctc: 2_000 + # Pre-TCJA: $1,000 per child, under the $75,000 head-of-household + # phase-out threshold. Federal tax is (30,000 - 20,800) x 10% = 920, so + # 920 is nonrefundable and the other 80 refundable (15% x (30,000 - + # 3,000) = 4,050 exceeds it): 1,000. 33% x 1,000 = 330, above the $100 + # minimum. The 2023 minimum age is zero, so the child aged 3 counts. + ny_ctc_pre_2024: 330 + +# A supplied ctc_qualifying_child is the current-law determination; the +# pre-TCJA branch recomputes it under the pinned rules. +- name: 2023 pre-TCJA eligibility recomputes a supplied federal qualifying-child flag + period: 2023 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 30_000 + child: + age: 6 + ctc_qualifying_child: false + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NY + output: + # The child qualifies under the pre-TCJA rules, so the credit is the + # same 33% x 1,000 = 330 as above. + ny_ctc_pre_2024_eligible: true + ny_ctc_pre_2024: 330 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml index 56bdd57f9fc..3b2b65ff605 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/credits/ny_eitc.yaml @@ -226,3 +226,107 @@ adjusted_gross_income: 41_500 ny_recomputed_federal_agi: 41_800 ny_eitc: 0 + +- name: 2021 NY EITC for a parent who takes up the federal EITC + period: 2021 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 10_000 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + takes_up_eitc: true + households: + household: + members: [parent, child] + state_code: NY + output: + # Tax Law 606(d) and the 2021 IT-215 instructions: 30% of the federal + # credit computed without ARPA. One child: 34% phase-in x $10,000 = + # $3,400, under the $3,618 maximum. 30% x 3,400 = $1,020, less the + # household credit capped at NY tax before credits, which is zero. + eitc: 3_400 + ny_income_tax_before_credits: 0 + ny_eitc: 1_020 + +- name: 2021 NY EITC is zero for a parent who does not take up the federal EITC + period: 2021 + input: + people: + parent: + age: 35 + employment_income: 10_000 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + takes_up_eitc: false + households: + household: + members: [parent, child] + state_code: NY + output: + # The pre-ARPA recomputation reads the same take-up input, so the + # recomputed federal credit, and 30% of it, is zero. + eitc: 0 + ny_eitc: 0 + +# The pre-ARPA branch copies the parent's cache, so this case lists the +# current-law federal credit first: the branch must recompute it, not read it. +- name: 2021 NY EITC recomputes the federal credit without ARPA after the ARPA credit + period: 2021 + absolute_error_margin: 0.01 + input: + people: + filer: + age: 30 + employment_income: 9_000 + households: + household: + members: [filer] + state_code: NY + output: + # ARPA childless credit (gov.irs.credits.eitc.phase_in_rate and .max, + # 2021, no children): 15.3% x 9,000 = 1,377, under the 1,502 maximum, + # and below the 11,610 ARPA phase-out start. + eitc: 1_377 + # The model's NY tax before credits and household credit for this filer. + ny_income_tax_before_credits: 40 + ny_household_credit: 40 + # Pre-ARPA (Rev. Proc. 2020-45): gov.states.ny.tax.income.credits.eitc + # .pre_arpa childless_max 543 and childless_phase_out_start 8,880, and + # the 2020 childless phase-out rate of 7.65% (eitc.phase_out.rate): + # 543 - 7.65% x (9,000 - 8,880) = 533.82; 30% = 160.146, less the + # household credit of 40 (capped at NY tax of 40) = 120.15. + ny_eitc: 120.15 + +# A supplied eitc is the current-law (ARPA) amount; the pre-ARPA branch +# recomputes the federal credit rather than reading it. +- name: 2021 NY EITC recomputes the federal credit when the ARPA credit is supplied + period: 2021 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 10_000 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + eitc: 500 + households: + household: + members: [parent, child] + state_code: NY + output: + # Pre-ARPA one-child credit: 34% x 10,000 = 3,400, under the 3,618 + # maximum; 30% = 1,020, less a household credit capped at NY tax of 0. + ny_eitc: 1_020 diff --git a/policyengine_us/tools/period_branch.py b/policyengine_us/tools/period_branch.py index 93d598229bb..f81448ca771 100644 --- a/policyengine_us/tools/period_branch.py +++ b/policyengine_us/tools/period_branch.py @@ -28,7 +28,8 @@ ``get_branch_for_period`` is the same with no inputs, for branches that change the tax-benefit system rather than inputs: the caller swaps the system -and deletes the variables it recalculates. +and drops the values of the formula variables it recalculates with +``drop_recomputed_values``, which keeps plain inputs. """ from typing import Dict, Iterable, Optional, Set, Tuple, Union @@ -176,6 +177,23 @@ def drop_inherited_values( branch._fast_cache = {} +def drop_recomputed_values(branch: Simulation, name_parts: Iterable[str]) -> None: + """Drop the values of formula variables a pinned-rules branch recalculates. + + A branch that swaps in a pinned tax-benefit system recalculates every + formula variable whose name contains one of ``name_parts``, including one + the caller supplied: a supplied value is the current-law amount, which the + pinned rules replace. Plain inputs (no formula), such as ``takes_up_eitc``, + are facts the pinned rules don't change, so they are kept. + """ + name_parts = tuple(name_parts) + for name, variable in branch.tax_benefit_system.variables.items(): + if any(part in name for part in name_parts) and ( + variable.formulas or variable.adds or variable.subtracts + ): + branch.delete_arrays(name) + + def get_override_branch( simulation: Simulation, name: str, @@ -239,7 +257,8 @@ def get_branch_for_period( """Return ``simulation``'s branch ``name`` for ``period``, created per period. For branches that change the tax-benefit system rather than inputs: the - caller swaps the system and deletes the variables it recalculates. Within + caller swaps the system and drops the formula variables it recalculates + with ``drop_recomputed_values``. Within a period the branch is shared; a branch left from another period is dropped and created again from the parent as it stands now. """ diff --git a/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py b/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py index 02c98792366..d7c710f9c51 100644 --- a/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py +++ b/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.period_branch import drop_recomputed_values from policyengine_us.tools.pinned_tbs import ( CREDIT_DEPENDENT_VARIABLES, IRC_2020_RECOMPUTED_VARIABLES, @@ -60,18 +61,19 @@ def formula(tax_unit, period, parameters): simulation = tax_unit.simulation branch = get_branch_for_period(simulation, "al_2020_irc", period) branch.tax_benefit_system = get_2020_irc_tbs(simulation.tax_benefit_system) - for variable in branch.tax_benefit_system.variables: - if any( - key in variable - for key in ( - "ctc", - "cdcc", - "eitc", - *CREDIT_DEPENDENT_VARIABLES, - *IRC_2020_RECOMPUTED_VARIABLES, - ) - ): - branch.delete_arrays(variable) + # Recalculate the credits' formula variables under the 2020 IRC, a + # supplied current-law value included; keep plain inputs such as + # takes_up_eitc. + drop_recomputed_values( + branch, + [ + "ctc", + "cdcc", + "eitc", + *CREDIT_DEPENDENT_VARIABLES, + *IRC_2020_RECOMPUTED_VARIABLES, + ], + ) recomputed_ctc = branch.tax_unit("ctc", period) recomputed_refundable_ctc = branch.tax_unit("refundable_ctc", period) # Under the 2020 IRC the CDCC is non-refundable (limited by tax), so the diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py index 9a8de7bd691..ed95653cc32 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.period_branch import drop_recomputed_values from policyengine_us.tools.pinned_tbs import ( CREDIT_DEPENDENT_VARIABLES, get_pre_tcja_ctc_tbs, @@ -53,12 +54,9 @@ def formula(tax_unit, period, parameters): simulation.tax_benefit_system ) branch_parameters = pre_tcja_ctc.tax_benefit_system.parameters - # Delete all arrays from pre-TCJA CTC branch. - for variable in pre_tcja_ctc.tax_benefit_system.variables: - if "ctc" in variable or any( - name in variable for name in CREDIT_DEPENDENT_VARIABLES - ): - pre_tcja_ctc.delete_arrays(variable) + # Recalculate every CTC formula variable under the pinned rules, a + # supplied current-law value included. + drop_recomputed_values(pre_tcja_ctc, ["ctc", *CREDIT_DEPENDENT_VARIABLES]) # Calculate pre-TCJA CTC. IT-213 Worksheet A, line 1 counts every # child on form line 4, including children under the NY minimum # age; lines 9-12 below prorate the result. diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py index 3381ec65067..9fbe2773061 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.period_branch import drop_recomputed_values from policyengine_us.tools.pinned_tbs import ( CREDIT_DEPENDENT_VARIABLES, get_pre_tcja_ctc_tbs, @@ -42,12 +43,9 @@ def formula(tax_unit, period, parameters): pre_tcja_ctc.tax_benefit_system = get_pre_tcja_ctc_tbs( simulation.tax_benefit_system ) - # Delete all arrays from pre-TCJA CTC branch. - for variable in pre_tcja_ctc.tax_benefit_system.variables: - if "ctc" in variable or any( - name in variable for name in CREDIT_DEPENDENT_VARIABLES - ): - pre_tcja_ctc.delete_arrays(variable) + # Recalculate every CTC formula variable under the pinned rules, a + # supplied current-law value included. + drop_recomputed_values(pre_tcja_ctc, ["ctc", *CREDIT_DEPENDENT_VARIABLES]) qualifies_for_federal_ctc = pre_tcja_ctc.person( "ctc_qualifying_child", period ) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py index c2abee94cf2..eb430ea9c13 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ny_eitc.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.period_branch import drop_recomputed_values from policyengine_us.tools.pinned_tbs import get_pre_arpa_eitc_tbs from policyengine_us.variables.gov.states.ny.tax.income.adjusted_gross_income.ny_recomputed_federal_agi import ( get_ny_recomputed_branch, @@ -33,9 +34,10 @@ def formula(tax_unit, period, parameters): branch.tax_benefit_system = get_pre_arpa_eitc_tbs( simulation.tax_benefit_system ) - for variable in branch.tax_benefit_system.variables: - if "eitc" in variable: - branch.delete_arrays(variable) + # Recalculate every EITC formula variable under the pinned rules, + # a supplied current-law eitc included; keep plain inputs such as + # takes_up_eitc. + drop_recomputed_values(branch, ["eitc"]) federal_eitc = branch.tax_unit("eitc", period) else: # IT-215: the federal credit recomputed with recomputed federal