diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb2d..2c2b0fe996e 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - CDCTC reform that makes families eligible if at least one parent works (instead of requiring both parents to work) diff --git a/policyengine_us/parameters/gov/contrib/cdcc/single_parent_work_requirement/in_effect.yaml b/policyengine_us/parameters/gov/contrib/cdcc/single_parent_work_requirement/in_effect.yaml new file mode 100644 index 00000000000..efb4f6e137a --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/cdcc/single_parent_work_requirement/in_effect.yaml @@ -0,0 +1,8 @@ +description: The CDCTC single parent work requirement reform is in effect. +metadata: + unit: bool + period: year + label: CDCTC single parent work requirement reform in effect + +values: + 0001-01-01: false diff --git a/policyengine_us/reforms/cdcc/__init__.py b/policyengine_us/reforms/cdcc/__init__.py new file mode 100644 index 00000000000..509a67d587f --- /dev/null +++ b/policyengine_us/reforms/cdcc/__init__.py @@ -0,0 +1,3 @@ +from policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement import ( + create_cdcc_single_parent_work_requirement_reform, +) diff --git a/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py b/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py new file mode 100644 index 00000000000..214f4f2127f --- /dev/null +++ b/policyengine_us/reforms/cdcc/cdcc_single_parent_work_requirement.py @@ -0,0 +1,81 @@ +from policyengine_us.model_api import * +from policyengine_core.periods import period as period_ + + +def create_cdcc_single_parent_work_requirement() -> Reform: + """ + Reform that changes CDCTC eligibility to require at least one parent to work + instead of both parents. + + Current law: For married couples, childcare expenses are capped at the + minimum of the two spouses' earnings, effectively requiring both to work. + + This reform: Changes the cap to the maximum of the two spouses' earnings, + allowing families to qualify if at least one parent works. + + Note: We override cdcc_relevant_expenses directly (rather than + min_head_spouse_earned) to avoid affecting state programs that also + use min_head_spouse_earned. + """ + + class cdcc_relevant_expenses(Variable): + value_type = float + entity = TaxUnit + label = "CDCC-relevant care expenses" + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/21#c", + "https://www.law.cornell.edu/uscode/text/26/21#d_1", + ) + + def formula(tax_unit, period, parameters): + expenses = tax_unit("tax_unit_childcare_expenses", period) + cdcc_limit = tax_unit("cdcc_limit", period) + eligible_capped_expenses = min_(expenses, cdcc_limit) + + # Reform: Use max of head/spouse earnings instead of min + # This allows eligibility if at least one parent works + is_joint = tax_unit("tax_unit_is_joint", period) + head_earnings = tax_unit("head_earned", period) + spouse_earnings = tax_unit("spouse_earned", period) + earnings_cap = where( + is_joint, max_(head_earnings, spouse_earnings), head_earnings + ) + + return min_(eligible_capped_expenses, earnings_cap) + + class reform(Reform): + def apply(self): + self.update_variable(cdcc_relevant_expenses) + + return reform + + +def create_cdcc_single_parent_work_requirement_reform( + parameters, period, bypass: bool = False +): + if bypass: + return create_cdcc_single_parent_work_requirement() + + p = parameters.gov.contrib.cdcc.single_parent_work_requirement + + # Check if reform is active in current period or next 5 years + 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_cdcc_single_parent_work_requirement() + else: + return None + + +cdcc_single_parent_work_requirement = ( + create_cdcc_single_parent_work_requirement_reform(None, None, bypass=True) +) diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 5c0a65cf3bf..1962bf2e51e 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -132,6 +132,9 @@ create_aca_ptc_simplified_bracket_reform, create_aca_ptc_700_fpl_cliff_reform, ) +from .cdcc import ( + create_cdcc_single_parent_work_requirement_reform, +) from policyengine_core.reforms import Reform @@ -298,6 +301,9 @@ def create_structural_reforms_from_parameters(parameters, period): aca_ptc_700_fpl_cliff = create_aca_ptc_700_fpl_cliff_reform( parameters, period ) + cdcc_single_parent_work_requirement = ( + create_cdcc_single_parent_work_requirement_reform(parameters, period) + ) reforms = [ afa_reform, @@ -358,6 +364,7 @@ def create_structural_reforms_from_parameters(parameters, period): aca_ptc_additional_bracket, aca_ptc_simplified_bracket, aca_ptc_700_fpl_cliff, + cdcc_single_parent_work_requirement, ] reforms = tuple(filter(lambda x: x is not None, reforms)) diff --git a/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml b/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml new file mode 100644 index 00000000000..b19d62d4314 --- /dev/null +++ b/policyengine_us/tests/policy/reform/cdcc_single_parent_work_requirement.yaml @@ -0,0 +1,154 @@ +# Tests for CDCTC reform that makes families eligible if at least one parent works +# instead of requiring both parents to work +# +# NOTE: This reform overrides cdcc_relevant_expenses directly (not min_head_spouse_earned) +# to avoid affecting state programs that use min_head_spouse_earned. + +- name: One-earner married couple gets CDCC under reform + period: 2024 + reforms: policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement.cdcc_single_parent_work_requirement + input: + people: + adult1: + age: 35 + is_tax_unit_head: true + employment_income: 50_000 + adult2: + age: 35 + is_tax_unit_spouse: true + employment_income: 0 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult1, adult2, child] + tax_unit_childcare_expenses: 5_000 + households: + household: + members: [adult1, adult2, child] + state_code: CA + output: + # min_head_spouse_earned is NOT modified by this reform (unchanged from baseline) + min_head_spouse_earned: 0 + # Reform calculates: min(expenses, cdcc_limit, max(head, spouse)) + # = min($5,000, $3,000, max($50,000, $0)) = min($5,000, $3,000, $50,000) = $3,000 + cdcc_relevant_expenses: 3_000 + +- name: One-earner married couple - baseline would give zero + period: 2024 + # No reform - baseline behavior + input: + people: + adult1: + age: 35 + is_tax_unit_head: true + employment_income: 50_000 + adult2: + age: 35 + is_tax_unit_spouse: true + employment_income: 0 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult1, adult2, child] + tax_unit_childcare_expenses: 5_000 + households: + household: + members: [adult1, adult2, child] + state_code: CA + output: + # Baseline uses min(head_earned, spouse_earned) = min($50,000, $0) = $0 + min_head_spouse_earned: 0 + # No relevant expenses since capped at $0 + cdcc_relevant_expenses: 0 + cdcc: 0 + +- name: Two-earner married couple unchanged by reform + period: 2024 + reforms: policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement.cdcc_single_parent_work_requirement + input: + people: + adult1: + age: 35 + is_tax_unit_head: true + employment_income: 50_000 + adult2: + age: 35 + is_tax_unit_spouse: true + employment_income: 30_000 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult1, adult2, child] + tax_unit_childcare_expenses: 5_000 + households: + household: + members: [adult1, adult2, child] + state_code: CA + output: + # min_head_spouse_earned unchanged: min($50,000, $30,000) = $30,000 + min_head_spouse_earned: 30_000 + # Reform: min($5,000, $3,000, max($50,000, $30,000)) = min($5,000, $3,000, $50,000) = $3,000 + # Same as baseline since both earnings exceed the $3,000 limit + cdcc_relevant_expenses: 3_000 + +- name: Single parent unaffected by reform + period: 2024 + reforms: policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement.cdcc_single_parent_work_requirement + input: + people: + adult: + age: 35 + is_tax_unit_head: true + employment_income: 40_000 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult, child] + tax_unit_childcare_expenses: 4_000 + households: + household: + members: [adult, child] + state_code: CA + output: + # Single filer - min_head_spouse_earned uses head_earned + min_head_spouse_earned: 40_000 + # Reform: min($4,000, $3,000, $40,000) = $3,000 (same as baseline) + cdcc_relevant_expenses: 3_000 + +- name: Spouse is the only earner - reform enables credit + period: 2024 + reforms: policyengine_us.reforms.cdcc.cdcc_single_parent_work_requirement.cdcc_single_parent_work_requirement + input: + people: + adult1: + age: 35 + is_tax_unit_head: true + employment_income: 0 + adult2: + age: 35 + is_tax_unit_spouse: true + employment_income: 60_000 + child: + age: 3 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult1, adult2, child] + tax_unit_childcare_expenses: 8_000 + households: + household: + members: [adult1, adult2, child] + state_code: CA + output: + # min_head_spouse_earned unchanged: min($0, $60,000) = $0 + min_head_spouse_earned: 0 + # Reform: min($8,000, $3,000, max($0, $60,000)) = min($8,000, $3,000, $60,000) = $3,000 + cdcc_relevant_expenses: 3_000 diff --git a/uv.lock b/uv.lock index 96f38a064ed..0f361a82060 100644 --- a/uv.lock +++ b/uv.lock @@ -718,7 +718,7 @@ name = "importlib-metadata" version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ @@ -1535,7 +1535,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.496.2" +version = "1.498.0" source = { editable = "." } dependencies = [ { name = "microdf-python" },