diff --git a/changelog.d/cdcc-disabled-dependent-expenses.fixed.md b/changelog.d/cdcc-disabled-dependent-expenses.fixed.md new file mode 100644 index 00000000000..edc9f92b7f5 --- /dev/null +++ b/changelog.d/cdcc-disabled-dependent-expenses.fixed.md @@ -0,0 +1 @@ +Fix the Child and Dependent Care Credit (and its state mirrors) for households whose qualifying individual is a disabled adult: attribute childcare expenses to disabled dependents and spouses of any age, count a disabled spouse who is modeled as the tax unit head, and apply the IRC section 21(d)(2) deemed earned income for a student or incapacitated spouse. Also scope the DC Keep Child Care Affordable Tax Credit to child-only care expenses, so a disabled adult dependent sharing a household with an eligible child does not inflate this under-4 child credit. diff --git a/policyengine_us/parameters/gov/irs/credits/cdcc/deemed_earned_income.yaml b/policyengine_us/parameters/gov/irs/credits/cdcc/deemed_earned_income.yaml new file mode 100644 index 00000000000..6dd883f7f7a --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/cdcc/deemed_earned_income.yaml @@ -0,0 +1,19 @@ +description: The IRS deems a spouse who is a full-time student or incapable of self-care to have at least this earned income for the Child and Dependent Care Credit, based on the number of qualifying individuals. +brackets: + - threshold: + 2013-01-01: 0 + amount: + 2013-01-01: 3_000 + - threshold: + 2013-01-01: 2 + amount: + 2013-01-01: 6_000 +metadata: + type: single_amount + threshold_unit: person + amount_unit: currency-USD + period: year + label: CDCC deemed earned income for student or disabled spouse + reference: + - title: 26 U.S. Code § 21(d)(2) + href: https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section21&num=0&edition=prelim diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc.yaml index d010ed7d1b7..7a1be889164 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc.yaml @@ -31,3 +31,41 @@ cdcc_potential: 1_000 output: cdcc: 0 + +# IRC 21(e)(2): a married taxpayer may only claim the credit on a joint return. +- name: Married filing separately does not receive the credit + period: 2024 + input: + cdcc_credit_limit: 1_000 + cdcc_potential: 1_000 + filing_status: SEPARATE + output: + cdcc: 0 + +- name: Head of household receives the credit + period: 2024 + input: + cdcc_credit_limit: 1_000 + cdcc_potential: 1_000 + filing_status: HEAD_OF_HOUSEHOLD + output: + cdcc: 1_000 + +- name: Married filing separately with a child and care expenses gets zero + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 35 + employment_income: 40_000 + is_tax_unit_head: true + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: SEPARATE + tax_unit_childcare_expenses: 3_000 + output: + cdcc: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_income_floor_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_income_floor_eligible.yaml new file mode 100644 index 00000000000..b860477ba45 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/cdcc_income_floor_eligible.yaml @@ -0,0 +1,40 @@ +- name: Case 1, disabled spouse assigned as tax unit head is eligible for the income floor. + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + tax_units: + tax_unit: + members: [person1, person2] + output: + is_tax_unit_head: [true, false] + is_tax_unit_spouse: [false, true] + cdcc_income_floor_eligible: [true, false] + +- name: Case 2, disabled single tax unit head is not eligible for the spouse income floor. + period: 2024 + input: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: true + output: + cdcc_income_floor_eligible: false + +- name: Case 3, full-time student spouse is eligible for the income floor. + period: 2024 + input: + people: + person1: + age: 50 + person2: + age: 45 + is_full_time_student: true + tax_units: + tax_unit: + members: [person1, person2] + output: + cdcc_income_floor_eligible: [false, true] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/count_cdcc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/count_cdcc_eligible.yaml new file mode 100644 index 00000000000..b0246dc3499 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/count_cdcc_eligible.yaml @@ -0,0 +1,97 @@ +- name: Both spouses incapable of self-care count as one qualifying individual + period: 2024 + input: + people: + person1: + age: 45 + is_incapable_of_self_care: true + person2: + age: 43 + is_incapable_of_self_care: true + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + output: + # Both are is_cdcc_eligible, but only one spouse is a qualifying individual + # (the other is the taxpayer) per IRC 21(b)(1)(C). + is_cdcc_eligible: [true, true] + count_cdcc_eligible: 1 + +- name: Disabled spouse plus a young child count as two qualifying individuals + period: 2024 + input: + people: + person1: + age: 45 + is_incapable_of_self_care: true + person2: + age: 43 + person3: + age: 5 + tax_units: + tax_unit: + members: [person1, person2, person3] + filing_status: JOINT + output: + count_cdcc_eligible: 2 + +- name: Both disabled spouses plus a child count as two qualifying individuals + period: 2024 + input: + people: + person1: + age: 45 + is_incapable_of_self_care: true + person2: + age: 43 + is_incapable_of_self_care: true + person3: + age: 5 + tax_units: + tax_unit: + members: [person1, person2, person3] + filing_status: JOINT + output: + count_cdcc_eligible: 2 + +- name: Two disabled adult dependents count as two qualifying individuals + period: 2024 + input: + people: + person1: + age: 45 + person2: + age: 20 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + person3: + age: 22 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1, person2, person3] + output: + count_cdcc_eligible: 2 + +- name: Healthy couple with two young children count as two qualifying individuals + period: 2024 + input: + people: + person1: + age: 45 + person2: + age: 43 + person3: + age: 5 + person4: + age: 8 + tax_units: + tax_unit: + members: [person1, person2, person3, person4] + filing_status: JOINT + output: + count_cdcc_eligible: 2 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/integration.yaml index c0210fa2edd..c100f878466 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/integration.yaml @@ -95,3 +95,72 @@ output: # expected results from patched TAXSIM35 2023-01-13 version taxsim_tfica: 11_130.24 income_tax: 24_507.2 + +- name: Disabled adult dependent's care expenses reach the CDCC expense base + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 40 + employment_income: 40_000 + is_tax_unit_head: true + person2: + age: 38 + employment_income: 30_000 + person3: # disabled adult dependent, the only qualifying individual + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_pre_subsidy_childcare_expenses: 6_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: KS + output: + # Only the disabled non-head adult is a dependent care recipient. + is_dependent_care_recipient: [false, false, true] + # The $6,000 SPM expense now flows to the tax unit (was $0 before the fix + # because the dependent is no longer a child under 18). This becomes the + # CDCC expense base via cdcc_relevant_expenses. + tax_unit_childcare_expenses: 6_000 + cdcc_relevant_expenses: 3_000 + cdcc: 660 + +- name: Both spouses incapable of self-care are one qualifying individual + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 45 + employment_income: 50_000 + is_incapable_of_self_care: true + person2: + age: 43 + is_incapable_of_self_care: true + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 6_000 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + households: + household: + members: [person1, person2] + state_code: TX + output: + # Two incapacitated spouses = one qualifying individual (the taxpayer is + # not their own), so the cap and the deemed floor are $3,000, not $6,000. + # cdcc = 20% x $3,000 = $600 (was $1,200 before the fix double-counted). + count_cdcc_eligible: 1 + cdcc_limit: 3_000 + cdcc: 600 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml new file mode 100644 index 00000000000..bf0002a18ae --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/is_cdcc_eligible.yaml @@ -0,0 +1,43 @@ +- name: Case 1, dependent child under age 13 qualifies. + period: 2024 + input: + age: 12 + is_tax_unit_head: false + is_tax_unit_spouse: false + output: + is_cdcc_eligible: true + +- name: Case 2, dependent child age 13 does not qualify by age. + period: 2024 + input: + age: 13 + is_tax_unit_head: false + is_tax_unit_spouse: false + output: + is_cdcc_eligible: false + +- name: Case 3, disabled single tax unit head is not their own qualifying individual. + period: 2024 + input: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: true + output: + is_cdcc_eligible: false + +- name: Case 4, disabled spouse assigned as tax unit head qualifies. + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + tax_units: + tax_unit: + members: [person1, person2] + output: + is_tax_unit_head: [true, false] + is_tax_unit_spouse: [false, true] + is_cdcc_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/min_head_spouse_earned.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/min_head_spouse_earned.yaml index b3ab1c31637..56b2a935d09 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/min_head_spouse_earned.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/min_head_spouse_earned.yaml @@ -1,18 +1,34 @@ - name: Head earns less period: 2018 input: - head_earned: 1 - spouse_earned: 2 - tax_unit_is_joint: true + people: + person1: + is_tax_unit_head: true + adjusted_earnings: 1 + person2: + is_tax_unit_spouse: true + adjusted_earnings: 2 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT output: min_head_spouse_earned: 1 - name: Spouse earns less period: 2018 input: - head_earned: 2 - spouse_earned: 1 - tax_unit_is_joint: true + people: + person1: + is_tax_unit_head: true + adjusted_earnings: 2 + person2: + is_tax_unit_spouse: true + adjusted_earnings: 1 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT output: min_head_spouse_earned: 1 @@ -22,3 +38,87 @@ head_earned: 1 output: min_head_spouse_earned: 1 + +- name: Disabled spouse assigned as tax unit head receives one-person income floor + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + employment_income: 50_000 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + output: + is_tax_unit_head: [true, false] + is_tax_unit_spouse: [false, true] + count_cdcc_eligible: 1 + min_head_spouse_earned: 3_000 + +- name: Disabled spouse assigned as tax unit head receives two-person income floor + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + employment_income: 50_000 + person3: + age: 5 + tax_units: + tax_unit: + members: [person1, person2, person3] + filing_status: JOINT + output: + count_cdcc_eligible: 2 + min_head_spouse_earned: 6_000 + +- name: Both spouses floor-eligible only one spouse receives the income floor + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + is_full_time_student: true + person3: + age: 5 + tax_units: + tax_unit: + members: [person1, person2, person3] + filing_status: JOINT + output: + count_cdcc_eligible: 2 + min_head_spouse_earned: 0 + +- name: Both spouses floor-eligible with asymmetric earnings deems only the lower earner + period: 2024 + input: + people: + person1: + age: 40 + is_full_time_student: true + adjusted_earnings: 5_000 + person2: + age: 38 + is_full_time_student: true + adjusted_earnings: 1_000 + person3: + age: 5 + tax_units: + tax_unit: + members: [person1, person2, person3] + filing_status: JOINT + output: + count_cdcc_eligible: 1 + # Deem the lower earner ($1,000 -> $3,000 floor), keep the higher actual + # ($5,000); the lesser is $3,000. Only one spouse may be deemed. + min_head_spouse_earned: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_kccatc.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_kccatc.yaml index 486339bc3bf..a8e6cd2ba33 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_kccatc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_kccatc.yaml @@ -17,10 +17,10 @@ tax_units: tax_unit: members: [person1, person2, person3] - tax_unit_childcare_expenses: 2_000 spm_units: spm_unit: members: [person1, person2, person3] + childcare_expenses: 2_000 households: household: members: [person1, person2, person3] @@ -47,10 +47,10 @@ tax_units: tax_unit: members: [person1, person2, person3] - tax_unit_childcare_expenses: 2_000 spm_units: spm_unit: members: [person1, person2, person3] + childcare_expenses: 2_000 households: household: members: [person1, person2, person3] @@ -77,10 +77,10 @@ tax_units: tax_unit: members: [person1, person2, person3] - tax_unit_childcare_expenses: 2_000 spm_units: spm_unit: members: [person1, person2, person3] + childcare_expenses: 2_000 households: household: members: [person1, person2, person3] @@ -107,10 +107,10 @@ tax_units: tax_unit: members: [person1, person2, person3] - tax_unit_childcare_expenses: 800 spm_units: spm_unit: members: [person1, person2, person3] + childcare_expenses: 800 households: household: members: [person1, person2, person3] @@ -136,14 +136,14 @@ age: 3 person4: is_tax_unit_dependent: true - age: 1 + age: 1 tax_units: tax_unit: members: [person1, person2, person3, person4] - tax_unit_childcare_expenses: 1_600 spm_units: spm_unit: members: [person1, person2, person3, person4] + childcare_expenses: 1_600 households: household: members: [person1, person2, person3, person4] @@ -173,13 +173,72 @@ tax_units: tax_unit: members: [person1, person2, person3, person4] - tax_unit_childcare_expenses: 1_600 spm_units: spm_unit: members: [person1, person2, person3, person4] + childcare_expenses: 1_600 households: household: members: [person1, person2, person3, person4] state_code: DC output: dc_kccatc: 800 + +# Regression test for the disabled-adult expense leak (review finding C1). +# An SPM unit contains two tax units: tax_unit1 has an under-4 child AND a +# disabled adult dependent; tax_unit2 has an under-4 child only. The $1,600 of +# SPM childcare must be split between the two under-4 children ($800 each), so +# each KCCATC is $800. The disabled adult is NOT a KCCATC-eligible child and +# must not pull extra expense share into tax_unit1's child-only credit. +# Before the fix (dc_kccatc read the disability-broadened +# tax_unit_childcare_expenses), tax_unit1 leaked up to $1,020 and tax_unit2 +# dropped to $533.33; after the fix (child-scoped distribution) both are $800. +- name: dc_kccatc regression - disabled adult does not inflate child-only credit + absolute_error_margin: 0.01 + period: 2021 + input: + people: + person1: + is_tax_unit_head: true + age: 35 + employment_income: 40_000 + person2: + is_tax_unit_dependent: true + age: 3 + person3: + is_tax_unit_dependent: true + age: 25 + is_incapable_of_self_care: true + person4: + is_tax_unit_head: true + age: 35 + employment_income: 40_000 + person5: + is_tax_unit_dependent: true + age: 3 + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2] + marital_unit3: + members: [person3] + marital_unit4: + members: [person4] + marital_unit5: + members: [person5] + tax_units: + tax_unit1: + members: [person1, person2, person3] + tax_unit2: + members: [person4, person5] + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + childcare_expenses: 1_600 + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: DC + output: + dc_kccatc: [800, 800] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_cdcc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_cdcc.yaml index a84e622894b..48494e98d3d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_cdcc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_cdcc.yaml @@ -5,3 +5,35 @@ state_code: KS output: ks_cdcc: 0.25 * 1_000 + +- name: Disabled adult dependent's care expenses count toward Kansas CDCC + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 40 + employment_income: 40_000 + is_tax_unit_head: true + person2: + age: 38 + employment_income: 30_000 + person3: + age: 18 + is_incapable_of_self_care: true + is_tax_unit_head: false + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_pre_subsidy_childcare_expenses: 6_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: KS + output: + tax_unit_childcare_expenses: 6_000 + cdcc: 600 + ks_cdcc: 300 diff --git a/policyengine_us/tests/policy/baseline/household/expense/childcare/is_dependent_care_recipient.yaml b/policyengine_us/tests/policy/baseline/household/expense/childcare/is_dependent_care_recipient.yaml new file mode 100644 index 00000000000..c3531fe8597 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/childcare/is_dependent_care_recipient.yaml @@ -0,0 +1,62 @@ +- name: Child under 18 is a dependent care recipient + period: 2024 + input: + age: 10 + is_tax_unit_head: false + output: + is_dependent_care_recipient: true + +- name: Disabled adult dependent is a dependent care recipient + period: 2024 + input: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + output: + is_dependent_care_recipient: true + +- name: Disabled adult dependent at the age 18 boundary is a recipient + period: 2024 + input: + age: 18 + is_incapable_of_self_care: true + is_tax_unit_head: false + is_tax_unit_spouse: false + output: + is_dependent_care_recipient: true + +- name: Disabled head is not a dependent care recipient + period: 2024 + input: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: true + output: + is_dependent_care_recipient: false + +- name: Non-disabled non-head adult is not a dependent care recipient + period: 2024 + input: + age: 40 + is_incapable_of_self_care: false + is_tax_unit_head: false + output: + is_dependent_care_recipient: false + +- name: Disabled spouse assigned as tax unit head is a dependent care recipient + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + tax_units: + tax_unit: + members: [person1, person2] + output: + is_tax_unit_head: [true, false] + is_tax_unit_spouse: [false, true] + is_dependent_care_recipient: [true, false] diff --git a/policyengine_us/tests/policy/baseline/household/expense/childcare/pre_subsidy_care_expenses.yaml b/policyengine_us/tests/policy/baseline/household/expense/childcare/pre_subsidy_care_expenses.yaml index 5bb9a787f1d..dc0bcb7c52d 100644 --- a/policyengine_us/tests/policy/baseline/household/expense/childcare/pre_subsidy_care_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/household/expense/childcare/pre_subsidy_care_expenses.yaml @@ -68,3 +68,46 @@ spm_unit_pre_subsidy_childcare_expenses: 0 output: pre_subsidy_childcare_expenses: [0, 0, 0, 0, 0] + +- name: Disabled non-head adult receives no pre-subsidy expenses (child-scoped) + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 40 + is_tax_unit_head: true + person2: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 6_000 + output: + # Childcare subsidies are child-scoped, so a disabled adult gets nothing + # here; the CDCC's broader attribution lives in tax_unit_childcare_expenses. + pre_subsidy_childcare_expenses: [0, 0] + +- name: Pre-subsidy expenses go to the child, not the disabled non-head adult + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 40 + is_tax_unit_head: true + person2: + age: 8 + is_tax_unit_head: false + person3: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_pre_subsidy_childcare_expenses: 6_000 + output: + pre_subsidy_childcare_expenses: [0, 6_000, 0] diff --git a/policyengine_us/tests/policy/baseline/household/expense/childcare/tax_unit_childcare_expenses.yaml b/policyengine_us/tests/policy/baseline/household/expense/childcare/tax_unit_childcare_expenses.yaml index 7dc082fa9df..9595302efcb 100644 --- a/policyengine_us/tests/policy/baseline/household/expense/childcare/tax_unit_childcare_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/household/expense/childcare/tax_unit_childcare_expenses.yaml @@ -29,3 +29,45 @@ childcare_expenses: 900 output: tax_unit_childcare_expenses: [300, 600] + +- name: tax_unit_childcare_expenses unit test 3 (disabled non-head adult dependent) + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 40 + is_tax_unit_head: true + person2: + age: 40 + is_incapable_of_self_care: true + is_tax_unit_head: false + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + childcare_expenses: 6_000 + output: + tax_unit_childcare_expenses: 6_000 + +- name: tax_unit_childcare_expenses unit test 4 (disabled spouse assigned as head) + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 70 + is_incapable_of_self_care: true + person2: + age: 50 + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + childcare_expenses: 6_000 + output: + tax_unit_childcare_expenses: 6_000 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc.py index 2edb7782635..e8aaf98e3d9 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc.py @@ -15,6 +15,12 @@ def formula(tax_unit, period, parameters): # In 2021, the CDCC was refundable p = parameters(period).gov.irs.credits if "cdcc" in p.refundable: - return potential + credit = potential else: - return min_(credit_limit, potential) + credit = min_(credit_limit, potential) + # IRC 21(e)(2): a married taxpayer may claim the credit only on a joint + # return. A separated taxpayer who qualifies under 21(e)(4) is treated + # as not married and files as head of household, not separately. + filing_status = tax_unit("filing_status", period) + separate = filing_status == filing_status.possible_values.SEPARATE + return where(separate, 0, credit) diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py index b7f1a49b057..401cbca4c8e 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_eligible.py @@ -6,15 +6,19 @@ class is_cdcc_eligible(Variable): entity = Person label = "CDCC-eligible" definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/21#b_1" + reference = "https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section21&num=0&edition=prelim" def formula(person, period, parameters): age = person("age", period) + is_dependent = person("is_tax_unit_dependent", period) # Subsection A. max_age = parameters(period).gov.irs.credits.cdcc.eligibility.child_age - qualifies_by_age = age < max_age + qualifies_by_age = is_dependent & (age < max_age) # Subsection B (dependent) and C (spouse). - non_head = ~person("is_tax_unit_head", period) disabled = person("is_incapable_of_self_care", period) - qualifies_by_disability = non_head & disabled + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + has_spouse = person.tax_unit.sum(head_or_spouse) > 1 + qualifies_by_disability = disabled & ( + is_dependent | (head_or_spouse & has_spouse) + ) return qualifies_by_age | qualifies_by_disability diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_income_floor_eligible.py b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_income_floor_eligible.py new file mode 100644 index 00000000000..2f2f0757f3e --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/cdcc/cdcc_income_floor_eligible.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class cdcc_income_floor_eligible(Variable): + value_type = bool + entity = Person + label = "Eligible for the CDCC spouse earned income floor" + definition_period = YEAR + reference = "https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section21&num=0&edition=prelim" + + def formula(person, period, parameters): + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + has_spouse = person.tax_unit.sum(head_or_spouse) > 1 + student_or_disabled = person("is_full_time_student", period) | person( + "is_incapable_of_self_care", period + ) + return head_or_spouse & has_spouse & student_or_disabled diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/count_cdcc_eligible.py b/policyengine_us/variables/gov/irs/credits/cdcc/count_cdcc_eligible.py index c5de64a0267..0274935569b 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/count_cdcc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/count_cdcc_eligible.py @@ -4,8 +4,18 @@ class count_cdcc_eligible(Variable): value_type = int entity = TaxUnit - label = "CDCC-eligible children" - unit = USD + label = "CDCC qualifying individuals" + unit = "person" definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/21#b_1" - adds = ["is_cdcc_eligible"] + def formula(tax_unit, period, parameters): + person = tax_unit.members + is_eligible = person("is_cdcc_eligible", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + # A married couple contributes at most one qualifying individual via + # the spouse prong (IRC 21(b)(1)(C)): the taxpayer is never their own + # qualifying individual, so two incapacitated spouses count as one. + dependent_count = tax_unit.sum(is_eligible & ~head_or_spouse) + has_eligible_spouse = tax_unit.any(is_eligible & head_or_spouse) + return dependent_count + has_eligible_spouse diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/min_head_spouse_earned.py b/policyengine_us/variables/gov/irs/credits/cdcc/min_head_spouse_earned.py index 34145f65f48..9c2672fb95c 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/min_head_spouse_earned.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/min_head_spouse_earned.py @@ -9,7 +9,36 @@ class min_head_spouse_earned(Variable): definition_period = YEAR def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.credits.cdcc is_joint = tax_unit("tax_unit_is_joint", period) - head_earnings = tax_unit("head_earned", period) - spouse_earnings = tax_unit("spouse_earned", period) - return where(is_joint, min_(head_earnings, spouse_earnings), head_earnings) + person = tax_unit.members + is_head = person("is_tax_unit_head", period) + is_spouse = person("is_tax_unit_spouse", period) + earnings = person("adjusted_earnings", period) + floor_eligible = person("cdcc_income_floor_eligible", period) + + head_earnings = tax_unit.sum(is_head * earnings) + spouse_earnings = tax_unit.sum(is_spouse * earnings) + head_floor_eligible = tax_unit.sum(is_head * floor_eligible) > 0 + spouse_floor_eligible = tax_unit.sum(is_spouse * floor_eligible) > 0 + + # IRC section 21(d)(2): a spouse who is a student or incapable of + # self-care is deemed to earn at least this floor, but only one spouse + # may be deemed. Deem whichever eligible spouse yields the larger + # lesser-of-earnings, leaving the other spouse's actual earnings. + qualifying_individuals = tax_unit("count_cdcc_eligible", period) + floor = p.deemed_earned_income.calc(qualifying_individuals) + no_deem = min_(head_earnings, spouse_earnings) + deem_head = where( + head_floor_eligible, + min_(max_(head_earnings, floor), spouse_earnings), + 0, + ) + deem_spouse = where( + spouse_floor_eligible, + min_(max_(spouse_earnings, floor), head_earnings), + 0, + ) + joint_earnings = max_(no_deem, max_(deem_head, deem_spouse)) + + return where(is_joint, joint_earnings, tax_unit("head_earned", period)) diff --git a/policyengine_us/variables/gov/states/dc/tax/income/credits/dc_kccatc.py b/policyengine_us/variables/gov/states/dc/tax/income/credits/dc_kccatc.py index f7cf221d777..edeb88bcc10 100644 --- a/policyengine_us/variables/gov/states/dc/tax/income/credits/dc_kccatc.py +++ b/policyengine_us/variables/gov/states/dc/tax/income/credits/dc_kccatc.py @@ -31,7 +31,25 @@ def formula(tax_unit, period, parameters): cdcc_eligible_count = tax_unit.sum(cdcc_age_eligible) # calculate KCCATC amount max_kccatc = kccatc_eligible_count * p.kccatc.max_amount - total_care_expenses = tax_unit("tax_unit_childcare_expenses", period) + # The KCCATC is a child-only credit: DC Code § 47-1806.15 limits it to + # dependents under age 4. Distribute the SPM unit's childcare expenses + # by each tax unit's share of CHILDREN, rather than reading + # tax_unit_childcare_expenses, which distributes by the broader + # dependent-care recipient set (children plus disabled dependents and + # spouses) used by the federal-conforming dc_cdcc. Using the child + # share keeps disabled-adult dependent-care dollars out of this + # child-only credit when a disabled adult shares an SPM unit with an + # eligible child. + spm_unit = tax_unit.spm_unit + spm_unit_childcare = spm_unit("childcare_expenses", period) + spm_unit_count_children = add(spm_unit, period, ["is_child"]) + tax_unit_count_children = add(tax_unit, period, ["is_child"]) + child_ratio = np.zeros_like(spm_unit_count_children) + child_mask = spm_unit_count_children > 0 + child_ratio[child_mask] = ( + tax_unit_count_children[child_mask] / spm_unit_count_children[child_mask] + ) + total_care_expenses = spm_unit_childcare * child_ratio ratio = np.zeros_like(cdcc_eligible_count) mask = cdcc_eligible_count > 0 ratio[mask] = kccatc_eligible_count[mask] / cdcc_eligible_count[mask] diff --git a/policyengine_us/variables/household/expense/childcare/childcare_expenses.py b/policyengine_us/variables/household/expense/childcare/childcare_expenses.py index e668a95657f..d411f17449a 100644 --- a/policyengine_us/variables/household/expense/childcare/childcare_expenses.py +++ b/policyengine_us/variables/household/expense/childcare/childcare_expenses.py @@ -9,8 +9,15 @@ class childcare_expenses(Variable): unit = USD def formula(spm_unit, period, parameters): - pre_subsidy_childcare_expenses = add( - spm_unit, period, ["pre_subsidy_childcare_expenses"] - ) + # Total pre-subsidy childcare expense for the SPM unit: the larger of + # the SPM-level input total and the person-level sum. Re-summing the + # person-level distribution alone drops to $0 when the only care + # recipient is not a child (e.g. an incapacitated spouse with no + # children), which would zero out the CDCC chain downstream; falling + # back to the SPM total avoids that while still respecting directly-set + # person-level values. + person_sum = add(spm_unit, period, ["pre_subsidy_childcare_expenses"]) + spm_total = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period) + pre_subsidy = max_(person_sum, spm_total) subsidies = spm_unit("child_care_subsidies", period) - return max_(pre_subsidy_childcare_expenses - subsidies, 0) + return max_(pre_subsidy - subsidies, 0) diff --git a/policyengine_us/variables/household/expense/childcare/is_dependent_care_recipient.py b/policyengine_us/variables/household/expense/childcare/is_dependent_care_recipient.py new file mode 100644 index 00000000000..fe3438e0be0 --- /dev/null +++ b/policyengine_us/variables/household/expense/childcare/is_dependent_care_recipient.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class is_dependent_care_recipient(Variable): + value_type = bool + entity = Person + label = "Eligible to have broad childcare expenses attributed to them" + definition_period = YEAR + + def formula(person, period, parameters): + is_child = person("is_child", period) + disabled = person("is_incapable_of_self_care", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + has_spouse = person.tax_unit.sum(head_or_spouse) > 1 + dependent_or_spouse = person("is_tax_unit_dependent", period) | ( + head_or_spouse & has_spouse + ) + return is_child | (disabled & dependent_or_spouse) diff --git a/policyengine_us/variables/household/expense/childcare/pre_subsidy_childcare_expenses.py b/policyengine_us/variables/household/expense/childcare/pre_subsidy_childcare_expenses.py index 5a08ad777cf..dd1e4cfb46c 100644 --- a/policyengine_us/variables/household/expense/childcare/pre_subsidy_childcare_expenses.py +++ b/policyengine_us/variables/household/expense/childcare/pre_subsidy_childcare_expenses.py @@ -9,8 +9,10 @@ class pre_subsidy_childcare_expenses(Variable): unit = USD def formula(person, period, parameters): - # distribute the SPM unit's childcare expenses evenly across - # children in SPM unit's Tax units + # Distribute the SPM unit's childcare expenses evenly across children + # in the SPM unit. Childcare subsidies (CCDF/state CCAP) are for + # children only, so this stays child-scoped; the CDCC's broader + # qualifying-individual attribution lives in tax_unit_childcare_expenses. spm_unit = person.spm_unit childcare_expenses = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period) is_child = person("is_child", period) diff --git a/policyengine_us/variables/household/expense/childcare/tax_unit_childcare_expenses.py b/policyengine_us/variables/household/expense/childcare/tax_unit_childcare_expenses.py index 19289bb4109..df1dbe27871 100644 --- a/policyengine_us/variables/household/expense/childcare/tax_unit_childcare_expenses.py +++ b/policyengine_us/variables/household/expense/childcare/tax_unit_childcare_expenses.py @@ -7,20 +7,27 @@ class tax_unit_childcare_expenses(Variable): label = "Childcare expenses" unit = USD definition_period = YEAR + reference = "https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section21&num=0&edition=prelim" def formula(tax_unit, period, parameters): - # distribute the SPM unit's childcare expenses evenly across - # children in SPM unit's Tax units + # Distribute the SPM unit's childcare expenses across tax units in + # proportion to each tax unit's share of dependent-care recipients + # (children under 18 and disabled dependents or spouses). The CDCC + # narrows this to qualifying individuals downstream via cdcc_limit. spm_unit = tax_unit.spm_unit spm_unit_childcare = spm_unit("childcare_expenses", period) - spm_unit_count_children = add(spm_unit, period, ["is_child"]) - tax_unit_count_children = add(tax_unit, period, ["is_child"]) + spm_unit_count_recipients = add( + spm_unit, period, ["is_dependent_care_recipient"] + ) + tax_unit_count_recipients = add( + tax_unit, period, ["is_dependent_care_recipient"] + ) # avoid array divide-by-zero warning by not using where() function # see the following GitHub issue for more details: # https://github.com/PolicyEngine/policyengine-us/issues/2494 - child_ratio = np.zeros_like(spm_unit_count_children) - mask = spm_unit_count_children > 0 - child_ratio[mask] = ( - tax_unit_count_children[mask] / spm_unit_count_children[mask] + recipient_ratio = np.zeros_like(spm_unit_count_recipients) + mask = spm_unit_count_recipients > 0 + recipient_ratio[mask] = ( + tax_unit_count_recipients[mask] / spm_unit_count_recipients[mask] ) - return spm_unit_childcare * child_ratio + return spm_unit_childcare * recipient_ratio