diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb2d..265419d19fa 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - Expanded CTC reform including a reformed phase-in structure. diff --git a/policyengine_us/parameters/gov/contrib/ctc/eppc/expanded_ctc/in_effect.yaml b/policyengine_us/parameters/gov/contrib/ctc/eppc/expanded_ctc/in_effect.yaml new file mode 100644 index 00000000000..0365aab0d8c --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/ctc/eppc/expanded_ctc/in_effect.yaml @@ -0,0 +1,10 @@ +description: An expanded Child Tax Credit amount with an alternative phase-in structure is provided, if this is true. +values: + 0000-01-01: false +metadata: + unit: bool + period: year + label: Expanded CTC in effect + reference: + - title: EPPC Tax Teams Comment on Working Families + href: https://eppc.org/wp-content/uploads/2024/10/Tax-Teams-Comment-on-Working-Families.pdf diff --git a/policyengine_us/reforms/ctc/eppc/README.md b/policyengine_us/reforms/ctc/eppc/README.md new file mode 100644 index 00000000000..a96ac526ee9 --- /dev/null +++ b/policyengine_us/reforms/ctc/eppc/README.md @@ -0,0 +1,20 @@ +# Reform description: + +# The CTC is phased in based on the sum of the tax liability and the benefits amount which is phased-out. + +# Example: +# A single filer is eligible for a maximum SNAP amount of $3,500. As earnings increase SNAP drops to $2,500 at which point the maximum CTC increased to $1,000 (assuming taxes and other considered benefits are zero). + + +# The following benefits are considered in the new CTC phase-in calculation: + +- SNAP +- Free school meals +- TANF + + +# The main income tax rate of couple is now computed as follows: +# 1. Compute the total tax liability for each person as an individual. +# 2. Compute the total tax liability for each person as a couple. +# 3. Apply the new CTC structure to each scenario. +# 4. The married couple's tax liability is the smaller of the two tax liabilities. diff --git a/policyengine_us/reforms/ctc/eppc/__init__.py b/policyengine_us/reforms/ctc/eppc/__init__.py new file mode 100644 index 00000000000..36b61b4afe4 --- /dev/null +++ b/policyengine_us/reforms/ctc/eppc/__init__.py @@ -0,0 +1,3 @@ +from .expanded_ctc import ( + create_expanded_ctc_reform, +) diff --git a/policyengine_us/reforms/ctc/eppc/expanded_ctc.py b/policyengine_us/reforms/ctc/eppc/expanded_ctc.py new file mode 100644 index 00000000000..7f7a73744bc --- /dev/null +++ b/policyengine_us/reforms/ctc/eppc/expanded_ctc.py @@ -0,0 +1,425 @@ +from policyengine_us.model_api import * + + +def create_expanded_ctc() -> Reform: + class ctc_phase_in(Variable): + value_type = float + entity = TaxUnit + label = "CTC phase-in" + unit = USD + definition_period = YEAR + reference = "https://eppc.org/wp-content/uploads/2024/10/Tax-Teams-Comment-on-Working-Families.pdf" + + def formula(tax_unit, period, parameters): + tax = tax_unit("income_tax_pre_ctc", period) + total_benefits = add( + tax_unit, period, ["snap", "free_school_meals", "tanf"] + ) + max_benefit_amount = tax_unit("maximum_benefits", period) + benefit_reduction = max_benefit_amount - total_benefits + tax_with_benefit_reduction = tax + benefit_reduction + p = parameters(period).gov.irs.credits.ctc.refundable.phase_in + return max_(p.rate * tax_with_benefit_reduction, 0) + + class income_tax_non_refundable_credits_pre_ctc(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "federal non-refundable income tax credits" + unit = USD + + def formula(tax_unit, period, parameters): + non_ref_credits = parameters(period).gov.irs.credits.non_refundable + credits = [ + credit + for credit in non_ref_credits + if credit not in ["non_refundable_ctc"] + ] + return add(tax_unit, period, credits) + + class income_tax_refundable_credits_pre_ctc(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "federal refundable income tax credits" + unit = USD + + def formula(tax_unit, period, parameters): + ref_credits = parameters(period).gov.irs.credits.refundable + credits = [ + credit + for credit in ref_credits + if credit not in ["refundable_ctc"] + ] + return add(tax_unit, period, credits) + + class income_tax_capped_non_refundable_credits(Variable): + value_type = float + entity = TaxUnit + label = "non-refundable tax credits" + unit = USD + documentation = "Capped value of non-refundable tax credits" + definition_period = YEAR + adds = ["income_tax_non_refundable_credits_pre_ctc"] + subtracts = ["income_tax_unavailable_non_refundable_credits"] + + class income_tax_unavailable_non_refundable_credits(Variable): + value_type = float + entity = TaxUnit + label = "unavailable non-refundable tax credits" + unit = USD + documentation = "Total value of non-refundable tax credits that were not available to the filer due to having too low income tax." + definition_period = YEAR + + def formula(tax_unit, period, parameters): + return -min_( + tax_unit("income_tax_before_credits", period), + tax_unit("income_tax_non_refundable_credits_pre_ctc", period), + ) + tax_unit("income_tax_non_refundable_credits_pre_ctc", period) + + class income_tax_pre_ctc(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + unit = USD + label = "Federal income tax" + documentation = "Total federal individual income tax liability." + + def formula(person, period, parameters): + if parameters( + period + ).gov.contrib.ubi_center.flat_tax.abolish_federal_income_tax: + return 0 + else: + added_components = add( + person, period, ["income_tax_before_refundable_credits"] + ) + subtracted_components = add( + person, period, ["income_tax_refundable_credits_pre_ctc"] + ) + return added_components - subtracted_components + + class income_tax(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + unit = USD + label = "Federal income tax" + documentation = "Total federal individual income tax liability." + + def formula(tax_unit, period, parameters): + pre_ctc_tax = tax_unit("income_tax_pre_ctc", period) + indiv_tax = tax_unit("income_tax_pre_ctc_indiv", period) + filing_status = tax_unit("filing_status", period) + is_joint = filing_status == filing_status.possible_values.JOINT + non_ref_ctc = tax_unit("non_refundable_ctc", period) + ref_ctc = tax_unit("refundable_ctc", period) + base_tax = where( + is_joint, min_(pre_ctc_tax, indiv_tax), pre_ctc_tax + ) + return max_(base_tax - non_ref_ctc, 0) - ref_ctc + + class maximum_benefits(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Maximum benefit amount to which the household is entitled" + unit = USD + # Currently only includes SNAP, free school meals, and TANF + + def formula(tax_unit, period, parameters): + snap_max_allotment = tax_unit.spm_unit( + "snap_max_allotment", period + ) + # get maximum free school meal allotment + state_group = tax_unit.spm_unit.household( + "state_group_str", period + ) + tier = "FREE" + p_amount = parameters(period).gov.usda.school_meals.amount + nslp_per_child = p_amount.nslp[state_group][tier] + sbp_per_child = p_amount.sbp[state_group][tier] + school_meals_daily_subsidy = nslp_per_child + sbp_per_child + daily_paid_subsidy = tax_unit.spm_unit( + "school_meal_paid_daily_subsidy", period + ) + net_daily_subsidy_per_child = ( + school_meals_daily_subsidy - daily_paid_subsidy + ) + p_school_meals = parameters(period).gov.usda.school_meals + children = add(tax_unit, period, ["is_in_k12_school"]) + school_meal_max_value = ( + net_daily_subsidy_per_child + * children + * p_school_meals.school_days + ) + # Get TANF grant standards (maximum amounts) + max_federal_tanf = tax_unit.spm_unit("tanf_max_amount", period) + max_ny_tanf = tax_unit.spm_unit("ny_tanf_grant_standard", period) + tanf_dem_eligible = tax_unit.spm_unit( + "is_demographic_tanf_eligible", period + ) + max_dc_tanf = tax_unit.spm_unit("dc_tanf_grant_standard", period) + max_co_tanf = tax_unit.spm_unit("co_tanf_grant_standard", period) + max_tanf = ( + max_co_tanf + max_dc_tanf + max_federal_tanf + max_ny_tanf + ) * tanf_dem_eligible + return snap_max_allotment + school_meal_max_value + max_tanf + + # At the core of the reform, we want to coampre the tax liability of two individuals + # filing as single people vs filing as a couple with the expanded CTC + + class basic_standard_deduction_indiv(Variable): + value_type = float + entity = Person + label = "Basic standard deduction" + definition_period = YEAR + unit = USD + reference = "https://www.law.cornell.edu/uscode/text/26/63#c_2" + + def formula(person, period, parameters): + std = parameters(period).gov.irs.deductions.standard + separate_filer_itemizes = person.tax_unit( + "separate_filer_itemizes", period + ) + dependent_elsewhere = person.tax_unit( + "head_is_dependent_elsewhere", period + ) + + # Calculate secondary earner deduction using single filing status + deduction_amount = std.amount["SINGLE"] + + standard_deduction_if_dependent = min_( + deduction_amount, + max_( + std.dependent.additional_earned_income + + person.tax_unit("tax_unit_earned_income", period), + std.dependent.amount, + ), + ) + + return select( + [ + separate_filer_itemizes, + dependent_elsewhere, + True, + ], + [ + 0, + standard_deduction_if_dependent, + deduction_amount, + ], + ) + + class taxable_income_deductions_person(Variable): + value_type = float + entity = Person + label = "Taxable income deductions for each person" + unit = USD + definition_period = YEAR + + def formula(person, period, parameters): + itemizes = person.tax_unit("tax_unit_itemizes", period) + is_joint = person.tax_unit("tax_unit_is_joint", period) + deductions_if_itemizing_amount = person.tax_unit( + "taxable_income_deductions_if_itemizing", period + ) + deductions_if_itemizing = where( + is_joint, + deductions_if_itemizing_amount / 2, + deductions_if_itemizing_amount, + ) + standard_deduction = person( + "basic_standard_deduction_indiv", period + ) + qbid = person("qualified_business_income_deduction_person", period) + return where( + itemizes, deductions_if_itemizing, standard_deduction + qbid + ) + + class taxable_income_indiv(Variable): + value_type = float + entity = Person + label = "IRS taxable income for each person" + unit = USD + definition_period = YEAR + + def formula(person, period, parameters): + agi = person("adjusted_gross_income_person", period) + exemption_amount = person.tax_unit("exemptions", period) + is_joint = person.tax_unit("tax_unit_is_joint", period) + exemptions = where( + is_joint, exemption_amount / 2, exemption_amount + ) + deductions = person("taxable_income_deductions_person", period) + return max_(0, agi - exemptions - deductions) + + class income_tax_main_rates_indiv(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Income tax main rates" + reference = "https://www.law.cornell.edu/uscode/text/26/1" + unit = USD + + def formula(tax_unit, period, parameters): + # compute taxable income that is taxed at the main rates + person = tax_unit.members + full_taxable_income = person("taxable_income_indiv", period) + cg_exclusion = ( + tax_unit("capital_gains_excluded_from_taxable_income", period) + / 2 + ) + taxinc = max_(0, full_taxable_income - cg_exclusion) + # compute tax using bracket rates and thresholds + p = parameters(period).gov.irs.income + bracket_tops = p.bracket.thresholds + bracket_rates = p.bracket.rates + + tax = 0 + bracket_bottom = 0 + for i in range(1, len(list(bracket_rates.__iter__())) + 1): + b = str(i) + bracket_top = bracket_tops[b]["SINGLE"] + tax += bracket_rates[b] * amount_between( + taxinc, bracket_bottom, bracket_top + ) + bracket_bottom = bracket_top + return tax_unit.sum(tax) + + class income_tax_before_credits_indiv(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "income tax before credits" + unit = USD + documentation = ( + "Total (regular + AMT) income tax liability before credits" + ) + + adds = [ + "income_tax_main_rates_indiv", + "capital_gains_tax", + "alternative_minimum_tax", + ] + + class income_tax_capped_non_refundable_credits_indiv(Variable): + value_type = float + entity = TaxUnit + label = "non-refundable tax credits" + unit = USD + documentation = "Capped value of non-refundable tax credits" + definition_period = YEAR + adds = ["income_tax_non_refundable_credits_pre_ctc"] + subtracts = ["income_tax_unavailable_non_refundable_credits"] + + class income_tax_unavailable_non_refundable_credits(Variable): + value_type = float + entity = TaxUnit + label = "unavailable non-refundable tax credits" + unit = USD + documentation = "Total value of non-refundable tax credits that were not available to the filer due to having too low income tax." + definition_period = YEAR + + def formula(tax_unit, period, parameters): + return -min_( + tax_unit("income_tax_before_credits", period), + tax_unit("income_tax_non_refundable_credits_pre_ctc", period), + ) + tax_unit("income_tax_non_refundable_credits_pre_ctc", period) + + class income_tax_before_refundable_credits_indiv(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + unit = USD + label = "Federal income tax before refundable credits" + documentation = "Income tax liability (including other taxes) after non-refundable credits are used, but before refundable credits are applied" + + def formula(tax_unit, period, parameters): + if parameters( + period + ).gov.contrib.ubi_center.flat_tax.abolish_federal_income_tax: + return 0 + else: + added_components = add( + tax_unit, + period, + [ + "income_tax_before_credits_indiv", + "net_investment_income_tax", + "recapture_of_investment_credit", + "unreported_payroll_tax", + "qualified_retirement_penalty", + ], + ) + subtracted_components = add( + tax_unit, + period, + ["income_tax_capped_non_refundable_credits_indiv"], + ) + return added_components - subtracted_components + + class income_tax_pre_ctc_indiv(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + unit = USD + label = "Federal income tax" + documentation = "Total federal individual income tax liability." + + def formula(person, period, parameters): + if parameters( + period + ).gov.contrib.ubi_center.flat_tax.abolish_federal_income_tax: + return 0 + else: + added_components = add( + person, + period, + ["income_tax_before_refundable_credits_indiv"], + ) + subtracted_components = add( + person, period, ["income_tax_refundable_credits_pre_ctc"] + ) + return added_components - subtracted_components + + class reform(Reform): + def apply(self): + self.neutralize_variable("eitc") + self.neutralize_variable("head_of_household_eligible") + self.neutralize_variable("cdcc") + self.update_variable(ctc_phase_in) + self.update_variable(income_tax) + self.update_variable(income_tax_pre_ctc) + self.update_variable(income_tax_unavailable_non_refundable_credits) + self.update_variable(income_tax_refundable_credits_pre_ctc) + self.update_variable(income_tax_non_refundable_credits_pre_ctc) + self.update_variable(income_tax_capped_non_refundable_credits) + self.update_variable(maximum_benefits) + self.update_variable(income_tax_before_refundable_credits_indiv) + self.update_variable(income_tax_before_credits_indiv) + self.update_variable( + income_tax_capped_non_refundable_credits_indiv + ) + self.update_variable(income_tax_main_rates_indiv) + self.update_variable(taxable_income_indiv) + self.update_variable(taxable_income_deductions_person) + self.update_variable(basic_standard_deduction_indiv) + self.update_variable(income_tax_pre_ctc_indiv) + + return reform + + +def create_expanded_ctc_reform(parameters, period, bypass: bool = False): + if bypass: + return create_expanded_ctc() + + p = parameters(period).gov.contrib.ctc.eppc.expanded_ctc + + if p.in_effect: + return create_expanded_ctc() + else: + return None + + +expanded_ctc = create_expanded_ctc_reform(None, None, bypass=True) diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 10c69f679eb..f465859398a 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -65,6 +65,9 @@ from .second_earner import ( create_second_earner_tax_reform, ) +from .ctc.eppc import ( + create_expanded_ctc_reform, +) from policyengine_core.reforms import Reform @@ -148,6 +151,7 @@ def create_structural_reforms_from_parameters(parameters, period): second_earner_tax_reform = create_second_earner_tax_reform( parameters, period ) + expanded_ctc = create_expanded_ctc_reform(parameters, period) reforms = [ afa_reform, @@ -179,6 +183,7 @@ def create_structural_reforms_from_parameters(parameters, period): repeal_state_dependent_exemptions, ctc_older_child_supplement, second_earner_tax_reform, + expanded_ctc, ] reforms = tuple(filter(lambda x: x is not None, reforms)) diff --git a/policyengine_us/tests/policy/contrib/ctc/eppc/expanded_ctc.yaml b/policyengine_us/tests/policy/contrib/ctc/eppc/expanded_ctc.yaml new file mode 100644 index 00000000000..0d6e3ce7f13 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/ctc/eppc/expanded_ctc.yaml @@ -0,0 +1,173 @@ +- name: Base test + period: 2024 + reforms: policyengine_us.reforms.ctc.eppc.expanded_ctc.expanded_ctc + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: true + income_tax_pre_ctc: 800 + snap: 200 + tanf: 500 + maximum_benefits: 900 + output: + ctc_phase_in: 150 + +- name: Negative pre-ctc income tax + period: 2024 + reforms: policyengine_us.reforms.ctc.eppc.expanded_ctc.expanded_ctc + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: true + income_tax_pre_ctc: -1_000 + maximum_benefits: 0 + output: + ctc_phase_in: 0 + +- name: Single parent of one child, $20k income + period: 2024 + reforms: policyengine_us.reforms.ctc.eppc.expanded_ctc.expanded_ctc + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: true + people: + person1: + age: 40 + employment_income: 20_000 + person2: + age: 10 + tax_units: + tax_unit: + members: [person1, person2] + output: + non_refundable_ctc: 540 + refundable_ctc: 1_460 + income_tax_pre_ctc: 540 + income_tax: -1_460 + +- name: Single parent of one child, $20k income - no reform, no eitc + period: 2024 + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: false + people: + person1: + age: 40 + employment_income: 20_000 + person2: + age: 10 + tax_units: + tax_unit: + members: [person1, person2] + eitc: 0 + output: + non_refundable_ctc: 300 + refundable_ctc: 1_700 + income_tax: -1_700 + +- name: Joint household with one child, $10k income + period: 2024 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.ctc.eppc.expanded_ctc.expanded_ctc + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: true + people: + person1: + age: 40 + employment_income: 10_000 + person2: + age: 40 + person3: + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + non_refundable_ctc: 1_748 + refundable_ctc: 252 + ctc_phase_in: 252 + household_benefits: 8_562 + maximum_benefits: 10_242 + income_tax_pre_ctc: 0 + income_tax: -252 + +- name: Joint household with one child, $10k income - no reform, no eitc + period: 2024 + absolute_error_margin: 1 + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: false + people: + person1: + age: 40 + employment_income: 10_000 + person2: + age: 40 + person3: + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3] + eitc: 0 + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + non_refundable_ctc: 875 + refundable_ctc: 1_125 + ctc_phase_in: 1_125 + household_benefits: 8_562 + income_tax: -1_125 + +- name: Joint household with one child, $60k income + period: 2024 + absolute_error_margin: 4 + reforms: policyengine_us.reforms.ctc.eppc.expanded_ctc.expanded_ctc + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: true + people: + person1: + age: 40 + employment_income: 60_000 + person2: + age: 40 + person3: + age: 10 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + non_refundable_ctc: 2_000 + refundable_ctc: 0 + ctc_phase_in: 2_021 + income_tax_pre_ctc: 3_232 + income_tax: 1_232 + +- name: Joint household with one child, $60k income, one high and one low earner + period: 2024 + absolute_error_margin: 4 + reforms: policyengine_us.reforms.ctc.eppc.expanded_ctc.expanded_ctc + input: + gov.contrib.ctc.eppc.expanded_ctc.in_effect: true + people: + person1: + age: 40 + employment_income: 50_000 + person2: + age: 40 + employment_income: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + non_refundable_ctc: 0 + refundable_ctc: 0 + income_tax_pre_ctc: 3_232 + income_tax_main_rates_indiv: 4_016 + income_tax: 3_232