diff --git a/changelog.d/co-ccap-parent-fee.fixed.md b/changelog.d/co-ccap-parent-fee.fixed.md new file mode 100644 index 00000000000..d5326139f60 --- /dev/null +++ b/changelog.d/co-ccap-parent-fee.fixed.md @@ -0,0 +1 @@ +The model now computes Colorado's Child Care Assistance Program parent fee under 8 CCR 1403-1 section 3.124. Each household pays one fee: 1% of gross income plus 14% of income above the poverty guideline Colorado sets each October, and $15 for each child in care after the first when income exceeds that guideline. The model drops the cents, charges 55% of the fee when every child in care receives part-time care, and takes 20% off at a provider in the top three quality levels. A household whose children have no recorded hours of care now pays the full-time fee, where the model used to charge nothing. From August 2028 the model applies the marginal rates of section 3.124(Q). diff --git a/changelog.d/co-ccap-parent-fee.removed.md b/changelog.d/co-ccap-parent-fee.removed.md new file mode 100644 index 00000000000..2b0c2f4c84d --- /dev/null +++ b/changelog.d/co-ccap-parent-fee.removed.md @@ -0,0 +1 @@ +Removed the Colorado Child Care Assistance Program parameters `parent_fee/base` and `parent_fee_rate_by_child_care_hours`. The household parent fee under section 3.124 replaces them. diff --git a/policyengine_us/parameters/gov/states/co/ccap/is_quality_rating_discounted.yaml b/policyengine_us/parameters/gov/states/co/ccap/is_quality_rating_discounted.yaml index 46d8a7763c5..81a63c20465 100644 --- a/policyengine_us/parameters/gov/states/co/ccap/is_quality_rating_discounted.yaml +++ b/policyengine_us/parameters/gov/states/co/ccap/is_quality_rating_discounted.yaml @@ -1,12 +1,14 @@ -description: The Colorado Child Care Assistance Program uses this discount rate based on the value of the maximum child care facility rating for an SPM unit compared to the outlined thresholds. +description: Colorado multiplies the parent fee by this factor, by the highest quality rating among the household's child care providers, under the Child Care Assistance Program. metadata: type: single_amount threshold_unit: Child care facility quality rating amount_unit: /1 - label: Colorado Child Care Assistance Program offers discount to quality rating child care facilities + label: Colorado Child Care Assistance Program parent fee factor by provider quality rating reference: - - title: Colorado Child Care Assistance Program Rules and Regulations - 3.111(H) - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=41 + - title: 8 CCR 1403-1 Section 3.124(H), Parent fees (effective 2026-08-14) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=81 +# Levels 3 to 5 are the top three levels of Colorado Shines, the Department's +# quality rating system, and cut the fee by 20%. brackets: - threshold: 2022-10-01: 0 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/add_on.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/add_on.yaml index a1a68e73e37..29c8c311eea 100644 --- a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/add_on.yaml +++ b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/add_on.yaml @@ -1,10 +1,13 @@ -description: Colorado multiplies the number of additional eligible children by this amount under the Child Care Assistance Program to calculate add on parent fee. +description: Colorado adds this amount to the parent fee for each child in care after the first when gross countable income exceeds the federal poverty guideline under the Child Care Assistance Program. +values: + 2021-07-01: 15 + # Section 3.124(P), which holds this fee, runs through July 31, 2028; the + # section 3.124(Q) formula that follows has no fee for additional children. + 2028-08-01: 0 metadata: + unit: currency-USD period: month + label: Colorado Child Care Assistance Program parent fee for each additional child reference: - - title: Colorado Child Care Assistance Program Rules and Regulations - 3.111(P) - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=43 - label: Colorado Child Care Assistance Program parent fee add on rate - unit: currency-USD -values: - 2022-10-01: 15 + - title: "8 CCR 1403-1 Section 3.124(P) and (Q), Parent fees (effective 2026-08-14) #page 83-84" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=83 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/base.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/base.yaml deleted file mode 100644 index 3a3deac3937..00000000000 --- a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/base.yaml +++ /dev/null @@ -1,19 +0,0 @@ -description: Colorado multiplies the ratio of countable income to the federal poverty guideline by this rate under the Child Care Assistance Program to calculate the base parent fee. -metadata: - type: marginal_rate - threshold_unit: currency-USD - rate_unit: /1 - period: month - label: Colorado Child Care Assistance Program parent fee base rate - reference: - - title: Colorado Child Care Assistance Program Rules and Regulations - 3.111(P) - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=43 -brackets: - - rate: - 2022-10-01: 0.01 - threshold: - 2022-10-01: 0 - - rate: - 2022-10-01: 0.14 - threshold: - 2022-10-01: 1 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/income_rate.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/income_rate.yaml new file mode 100644 index 00000000000..dddd3e10420 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/income_rate.yaml @@ -0,0 +1,11 @@ +description: Colorado charges this share of gross countable income as part of the parent fee under the Child Care Assistance Program. +values: + 2021-07-01: 0.01 + # Section 3.124(Q) replaces this flat share with marginal rates alone. + 2028-08-01: 0 +metadata: + unit: /1 + label: Colorado Child Care Assistance Program parent fee rate on gross income + reference: + - title: "8 CCR 1403-1 Section 3.124(P) and (Q), Parent fees (effective 2026-08-14) #page 83-84" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=83 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/marginal_rate.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/marginal_rate.yaml new file mode 100644 index 00000000000..0555cc827c3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/marginal_rate.yaml @@ -0,0 +1,30 @@ +description: Colorado charges these marginal rates on gross countable income, by income as a multiple of the federal poverty guideline, as part of the parent fee under the Child Care Assistance Program. +metadata: + type: marginal_rate + threshold_unit: /1 + rate_unit: /1 + label: Colorado Child Care Assistance Program parent fee marginal rates + reference: + - title: "8 CCR 1403-1 Section 3.124(P) and (Q), Parent fees (effective 2026-08-14) #page 83-84" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=83 +brackets: + - threshold: + 2021-07-01: 0 + rate: + 2021-07-01: 0 + 2028-08-01: 0.01 + - threshold: + 2021-07-01: 1 + rate: + 2021-07-01: 0.14 + 2028-08-01: 0.03 + - threshold: + 2021-07-01: 1.3 + rate: + 2021-07-01: 0.14 + 2028-08-01: 0.05 + - threshold: + 2021-07-01: 1.6 + rate: + 2021-07-01: 0.14 + 2028-08-01: 0.07 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/full_time_equivalent_units.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/full_time_equivalent_units.yaml new file mode 100644 index 00000000000..bef5a3b081a --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/full_time_equivalent_units.yaml @@ -0,0 +1,30 @@ +description: Colorado counts a day of care as this many full-time equivalent units, by the hours of care that day, under the Child Care Assistance Program. +metadata: + type: single_amount + threshold_unit: hour + amount_unit: /1 + label: Colorado Child Care Assistance Program full-time equivalent units per day of care + reference: + - title: "8 CCR 1403-1 Sections 3.103(ZZ) and 3.130(C) (effective 2026-08-14) #page 10,119" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=10 +# Each band covers days of more than its threshold, up to the next threshold: +# a part-time unit (up to 5 hours) counts 0.55 of a full-time unit, a +# full-time/part-time day (more than 12 hours, up to 17) counts 1.55, and a +# full-time/full-time day counts 2. +brackets: + - threshold: + 2022-10-01: 0 + amount: + 2022-10-01: 0.55 + - threshold: + 2022-10-01: 5 + amount: + 2022-10-01: 1 + - threshold: + 2022-10-01: 12 + amount: + 2022-10-01: 1.55 + - threshold: + 2022-10-01: 17 + amount: + 2022-10-01: 2 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/rate.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/rate.yaml new file mode 100644 index 00000000000..0a2aaa4dfb7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/rate.yaml @@ -0,0 +1,9 @@ +description: Colorado charges this share of the parent fee when every child in care receives part-time care under the Child Care Assistance Program. +values: + 2022-10-01: 0.55 +metadata: + unit: /1 + label: Colorado Child Care Assistance Program part-time parent fee rate + reference: + - title: 8 CCR 1403-1 Section 3.124(U), Parent fees (effective 2026-08-14) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=85 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/unit_threshold.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/unit_threshold.yaml new file mode 100644 index 00000000000..b3f572870b9 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/unit_threshold.yaml @@ -0,0 +1,10 @@ +description: Colorado treats a child's care as part time for the parent fee when it averages fewer than this many full-time equivalent units a month under the Child Care Assistance Program. +values: + 2022-10-01: 13 +metadata: + unit: /1 + period: month + label: Colorado Child Care Assistance Program part-time care unit threshold + reference: + - title: "8 CCR 1403-1 Sections 3.103(ZZ) and 3.124(U) (effective 2026-08-14) #page 10,85" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=10 diff --git a/policyengine_us/parameters/gov/states/co/ccap/parent_fee_rate_by_child_care_hours.yaml b/policyengine_us/parameters/gov/states/co/ccap/parent_fee_rate_by_child_care_hours.yaml deleted file mode 100644 index fde6f96fcd9..00000000000 --- a/policyengine_us/parameters/gov/states/co/ccap/parent_fee_rate_by_child_care_hours.yaml +++ /dev/null @@ -1,26 +0,0 @@ -description: Colorado Child Care Assistance Program issues these rates when calculating parent fee, depending on child care hours per day. -metadata: - type: single_amount - threshold_unit: hours - amount_unit: /1 - label: Colorado Child Care Assistance Program parent fee rate by hours - reference: - - title: Colorado Child Care Assistance Program Rules and Regulations - 3.115.1(C) - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=62 -brackets: - - threshold: - 2022-10-01: 0 - amount: - 2022-10-01: 0.55 - - threshold: - 2022-10-01: 5 - amount: - 2022-10-01: 1 - - threshold: - 2022-10-01: 12 - amount: - 2022-10-01: 1.55 - - threshold: - 2022-10-01: 17 - amount: - 2022-10-01: 2 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_add_on_parent_fee.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_add_on_parent_fee.yaml index 292250f580c..720f9734b97 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_add_on_parent_fee.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_add_on_parent_fee.yaml @@ -1,79 +1,69 @@ -- name: 2023 one eligible child for cccap, gross income is less than the family poverty guideline. - period: 2023 +# 8 CCR 1403-1 3.124(P)(3), PDF page 84 of ruleVersionId 12658: $15 for each +# child in care after the first when income exceeds the guideline. Section +# 3.124(P) runs through July 31, 2028, and (Q) has no such fee. +- name: Case 1, three children in care above the guideline. + period: 2026-01 input: - co_ccap_eligible_children: 1 - co_ccap_countable_income: 60_000 - spm_unit_fpg: 120_000 + state_code: + 2026: CO + co_ccap_countable_income: 3_000 + co_ccap_fpg: 2_000 + co_ccap_children_in_care: 3 output: - co_ccap_add_on_parent_fee: - 2023-01: 0 + # (3 - 1) * 15 = 30 + co_ccap_add_on_parent_fee: 30 -- name: 2023 one eligible child for cccap, gross income is more than the family poverty guideline. - period: 2023 +- name: Case 2, one child in care above the guideline. + period: 2026-01 input: - co_ccap_eligible_children: 1 - co_ccap_countable_income: 80_000 - spm_unit_fpg: 40_000 + state_code: + 2026: CO + co_ccap_countable_income: 3_000 + co_ccap_fpg: 2_000 + co_ccap_children_in_care: 1 output: - co_ccap_add_on_parent_fee: - 2023-01: 0 + co_ccap_add_on_parent_fee: 0 -- name: 2023 two eligible children for cccap, gross income is less than the family poverty guideline. - period: 2023 +- name: Case 3, income at the guideline. + period: 2026-01 input: - co_ccap_eligible_children: 2 - co_ccap_countable_income: 60_000 - spm_unit_fpg: 120_000 + state_code: + 2026: CO + co_ccap_countable_income: 2_000 + co_ccap_fpg: 2_000 + co_ccap_children_in_care: 3 output: - co_ccap_add_on_parent_fee: - 2023-01: 0 + co_ccap_add_on_parent_fee: 0 -- name: 2023 two eligible children for cccap, gross income is less than the family poverty guideline. - period: 2023 +- name: Case 4, no child in care. + period: 2026-01 input: - co_ccap_eligible_children: 2 - co_ccap_countable_income: 80_000 - spm_unit_fpg: 40_000 + state_code: + 2026: CO + co_ccap_countable_income: 3_000 + co_ccap_fpg: 2_000 + co_ccap_children_in_care: 0 output: - co_ccap_add_on_parent_fee: - 2023-01: 15 + co_ccap_add_on_parent_fee: 0 -- name: 2022 one eligible child for cccap, gross income is less than the family poverty guideline. - period: 2022 +- name: Case 5, July 2028 still charges the fee. + period: 2028-07 input: - co_ccap_eligible_children: 1 - co_ccap_countable_income: 60_000 - spm_unit_fpg: 120_000 + state_code: + 2028: CO + co_ccap_countable_income: 3_000 + co_ccap_fpg: 2_000 + co_ccap_children_in_care: 3 output: - co_ccap_add_on_parent_fee: - 2022-12: 0 + co_ccap_add_on_parent_fee: 30 -- name: 2022 one eligible child for cccap, gross income is more than the family poverty guideline. - period: 2022 +- name: Case 6, from August 2028 section 3.124(Q) has no fee for additional children. + period: 2028-08 input: - co_ccap_eligible_children: 1 - co_ccap_countable_income: 80_000 - spm_unit_fpg: 40_000 + state_code: + 2028: CO + co_ccap_countable_income: 3_000 + co_ccap_fpg: 2_000 + co_ccap_children_in_care: 3 output: - co_ccap_add_on_parent_fee: - 2022-12: 0 - -- name: 2022 two eligible children for cccap, gross income is less than the family poverty guideline. - period: 2022 - input: - co_ccap_eligible_children: 2 - co_ccap_countable_income: 60_000 - spm_unit_fpg: 120_000 - output: - co_ccap_add_on_parent_fee: - 2022-12: 0 - -- name: 2022 two eligible children for cccap, gross income is less than the family poverty guideline. - period: 2022 - input: - co_ccap_eligible_children: 2 - co_ccap_countable_income: 80_000 - spm_unit_fpg: 40_000 - output: - co_ccap_add_on_parent_fee: - 2022-12: 15 + co_ccap_add_on_parent_fee: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_base_parent_fee.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_base_parent_fee.yaml index 6db227e1238..851380c90f6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_base_parent_fee.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_base_parent_fee.yaml @@ -1,39 +1,145 @@ -- name: 2023 gross income is less than the family poverty guideline. - period: 2023 +# 8 CCR 1403-1 3.124(P) and (Q), PDF pages 83-84 of ruleVersionId 12658. +# Through July 2028, (P)(1) charges 1% of gross income up to the guideline, and +# (P)(2) charges 1% of income plus 14% of income above it. From August 2028, +# (Q) charges 1%, 3%, 5% and 7% on income in its bands. The fee comes in whole +# dollars ((A)). +- name: Case 1, income below the guideline pays 1% of income. + period: 2026-01 + absolute_error_margin: 0.01 input: - co_ccap_countable_income: 60_000 - spm_unit_fpg: 120_000 - state_code: CO + state_code: + 2026: CO + co_ccap_countable_income: 1_500 + co_ccap_fpg: 2_000 output: - co_ccap_base_parent_fee: - 2023-01: 50 + # 0.01 * 1,500 = 15 + co_ccap_base_parent_fee: 15 -- name: 2023 gross income is more than the family poverty guideline. - period: 2023 +- name: Case 2, income at the guideline pays 1% of income. + period: 2026-01 + absolute_error_margin: 0.01 input: - co_ccap_countable_income: 80_000 - spm_unit_fpg: 40_000 - state_code: CO + state_code: + 2026: CO + co_ccap_countable_income: 2_000 + co_ccap_fpg: 2_000 output: - co_ccap_base_parent_fee: - 2023-01: 500 + # 0.01 * 2,000 = 20 + co_ccap_base_parent_fee: 20 -- name: 2022 gross income is less than the family poverty guideline. - period: 2022 +- name: Case 3, income above the guideline pays 1% of all income plus 14% of the excess. + period: 2026-01 + absolute_error_margin: 0.01 input: - co_ccap_countable_income: 60_000 - spm_unit_fpg: 120_000 - state_code: CO + state_code: + 2026: CO + co_ccap_countable_income: 3_000 + co_ccap_fpg: 2_000 output: - co_ccap_base_parent_fee: - 2022-12: 50 + # 0.01 * 3,000 + 0.14 * (3,000 - 2,000) = 30 + 140 = 170 + co_ccap_base_parent_fee: 170 -- name: 2022 gross income is more than the family poverty guideline. - period: 2022 +- name: Case 4, no income pays nothing. + period: 2026-01 input: - co_ccap_countable_income: 80_000 - spm_unit_fpg: 40_000 - state_code: CO + state_code: + 2026: CO + co_ccap_countable_income: 0 + co_ccap_fpg: 2_000 output: - co_ccap_base_parent_fee: - 2022-12: 500 + co_ccap_base_parent_fee: 0 + +- name: Case 5, a net loss counts as no income. + period: 2026-01 + input: + state_code: + 2026: CO + co_ccap_countable_income: -500 + co_ccap_fpg: 2_000 + output: + co_ccap_base_parent_fee: 0 + +- name: Case 6, July 2028 still uses section 3.124(P). + period: 2028-07 + absolute_error_margin: 0.01 + input: + state_code: + 2028: CO + co_ccap_countable_income: 3_500 + co_ccap_fpg: 2_000 + output: + # 0.01 * 3,500 + 0.14 * 1,500 = 35 + 210 = 245 + co_ccap_base_parent_fee: 245 + +- name: Case 7, August 2028 uses the marginal rates of section 3.124(Q). + period: 2028-08 + absolute_error_margin: 0.01 + input: + state_code: + 2028: CO + co_ccap_countable_income: 3_500 + co_ccap_fpg: 2_000 + output: + # 1% of 2,000, 3% of 600 (to 130%), 5% of 600 (to 160%) and 7% of the + # last 300: 20 + 18 + 30 + 21 = 89 + co_ccap_base_parent_fee: 89 + +- name: Case 8, under section 3.124(Q), income between 100% and 130% of the guideline. + period: 2028-09 + absolute_error_margin: 0.01 + input: + state_code: + 2028: CO + co_ccap_countable_income: 2_400 + co_ccap_fpg: 2_000 + output: + # 0.01 * 2,000 + 0.03 * 400 = 20 + 12 = 32 + co_ccap_base_parent_fee: 32 + +- name: Case 9, under section 3.124(Q), income below the guideline. + period: 2028-10 + absolute_error_margin: 0.01 + input: + state_code: + 2028: CO + co_ccap_countable_income: 1_500 + co_ccap_fpg: 2_000 + output: + # 0.01 * 1,500 = 15 + co_ccap_base_parent_fee: 15 + +- name: Case 10, the cents drop. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: + 2026: CO + co_ccap_countable_income: 3_333.33 + co_ccap_fpg: 2_220.83 + output: + # 0.01 * 3,333.33 + 0.14 * 1,112.50 = 33.33 + 155.75 = 189.08, so 189. + co_ccap_base_parent_fee: 189 + +- name: Case 11, a fee just short of a whole dollar drops to the dollar below. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: + 2026: CO + co_ccap_countable_income: 1_999.97 + co_ccap_fpg: 2_220.83 + output: + # 0.01 * 1,999.97 = 19.9997, so 19. + co_ccap_base_parent_fee: 19 + +- name: Case 12, a fee of exactly a whole dollar keeps it. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: + 2026: CO + co_ccap_countable_income: 1_765 + co_ccap_fpg: 1_762.50 + output: + # 0.01 * 1,765 + 0.14 * 2.50 = 17.65 + 0.35 = 18. + co_ccap_base_parent_fee: 18 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_care_hours_per_day.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_care_hours_per_day.yaml new file mode 100644 index 00000000000..53ed32d698a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_care_hours_per_day.yaml @@ -0,0 +1,79 @@ +# 8 CCR 1403-1 3.130(C) sets units of care by hours a day (PDF page 119 of +# ruleVersionId 12658). With only weekly hours reported, the model spreads them +# over the child's days of care in a week: the larger of days a week and +# attending days a month x 12 / 52, else five weekdays. +- name: Case 1, weekly hours spread over the days of care. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + daily_reported: + age: 3 + childcare_hours_per_day: 4 + childcare_hours_per_week: 40 + weekly_over_two_days: + age: 3 + childcare_hours_per_week: 20 + childcare_days_per_week: 2 + weekly_only: + age: 3 + childcare_hours_per_week: 20 + five_hour_days: + age: 3 + childcare_hours_per_week: 25 + attending_days: + age: 3 + childcare_hours_per_week: 15 + childcare_attending_days_per_month: 13 + both_day_counts: + age: 3 + childcare_hours_per_week: 20 + childcare_days_per_week: 4 + childcare_attending_days_per_month: 13 + more_attending_days: + age: 3 + childcare_hours_per_week: 20 + childcare_days_per_week: 2 + childcare_attending_days_per_month: 20 + spm_units: + spm_unit: + members: [parent, daily_reported, weekly_over_two_days, weekly_only, five_hour_days, attending_days, both_day_counts, more_attending_days] + households: + household: + members: [parent, daily_reported, weekly_over_two_days, weekly_only, five_hour_days, attending_days, both_day_counts, more_attending_days] + state_code: CO + output: + # parent: no hours of care. + # daily_reported: daily hours win, 4. + # weekly_over_two_days: 20 / 2 = 10. + # weekly_only: 20 / 5 weekdays = 4. + # five_hour_days: 25 / 5 = 5. + # attending_days: 13 * 12 / 52 = 3 days a week, 15 / 3 = 5. + # both_day_counts: 4 days a week exceed 13 * 12 / 52 = 3, 20 / 4 = 5. + # more_attending_days: 20 * 12 / 52 = 4.62 days a week exceed 2, + # 20 / 4.62 = 4.33. + co_ccap_care_hours_per_day: [0, 4, 10, 4, 5, 5, 5, 4.33] + +- name: Case 2, weekly hours worked out from a daily schedule leave the daily hours alone. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + child: + age: 3 + childcare_hours_per_day: 8 + childcare_days_per_week: 3 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + # childcare_hours_per_week comes to 8 * 3 = 24, and 24 / 3 = 8 either way. + co_ccap_care_hours_per_day: [0, 8] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_care.yaml new file mode 100644 index 00000000000..03b5297a3d9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_care.yaml @@ -0,0 +1,87 @@ +# 8 CCR 1403-1 3.124(A) and (P)(3), PDF pages 79 and 84 of ruleVersionId 12658: +# the fee counts the children for whom the household requests care. Reported +# hours or days show care, and so does a child care or after-school expense. +- name: Case 1, schedules and expenses each show care for an eligible child. + period: 2026-01 + input: + people: + parent: + age: 35 + hours: + age: 4 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + expense: + age: 4 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 6_000 + nothing: + age: 4 + is_tax_unit_dependent: true + teenager: + age: 15 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + weekly_days: + age: 7 + is_tax_unit_dependent: true + childcare_days_per_week: 3 + after_school: + age: 10 + is_tax_unit_dependent: true + after_school_expenses: 2_000 + monthly_days: + age: 2 + is_tax_unit_dependent: true + childcare_attending_days_per_month: 12 + weekly_hours: + age: 6 + is_tax_unit_dependent: true + childcare_hours_per_week: 20 + spm_units: + spm_unit: + members: [parent, hours, expense, nothing, teenager, weekly_days, after_school, monthly_days, weekly_hours] + households: + household: + members: [parent, hours, expense, nothing, teenager, weekly_days, after_school, monthly_days, weekly_hours] + state_code: CO + output: + # The parent is no eligible child, the 4-year-old with nothing reported + # has no care, and the 15-year-old is past the age limit. + co_ccap_child_in_care: [false, true, true, false, false, true, true, true, true] + co_ccap_children_in_care: 6 + +- name: Case 2, the SPM unit's child care expense puts each eligible child in care. + period: 2026-01 + input: + people: + parent: + age: 35 + child1: + age: 3 + is_tax_unit_dependent: true + child2: + age: 5 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child1, child2] + spm_unit_pre_subsidy_childcare_expenses: 12_000 + households: + household: + members: [parent, child1, child2] + state_code: CO + output: + # Each child holds 6,000 of the expense. + co_ccap_child_in_care: [false, true, true] + co_ccap_children_in_care: 2 + +- name: Case 3, a child outside Colorado. + period: 2026-01 + input: + age: 4 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + state_code: TX + output: + co_ccap_child_in_care: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_part_time_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_part_time_care.yaml new file mode 100644 index 00000000000..2cee829404d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_part_time_care.yaml @@ -0,0 +1,150 @@ +# 8 CCR 1403-1 3.124(U) and (W), PDF page 85 of ruleVersionId 12658. Care is +# part time when it averages fewer than 13 full-time equivalent units a month, +# and a child in grades 1 to 12 with part-time days (up to 5 hours, 3.130(C)) +# has part-time care. The model takes a child in school aged 6 or older to be +# in grade 1 or above, and reads such a child whose only reported care is an +# after-school expense as in part-time care. +- name: Case 1, part-time care by units and by school grade. + period: 2026-01 + input: + people: + parent: + age: 35 + is_in_k12_school: false + short_days: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_day: 4 + many_short_days: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 24 + grade_school: + age: 8 + is_tax_unit_dependent: true + is_in_k12_school: true + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 24 + kindergarten: + age: 5 + is_tax_unit_dependent: true + is_in_k12_school: true + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 24 + few_days: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_attending_days_per_month: 10 + full_days: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_day: 8 + twelve_full_days: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_day: 8 + childcare_attending_days_per_month: 12 + out_of_school: + age: 8 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 24 + weekly_twenty: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_week: 20 + weekly_forty: + age: 3 + is_tax_unit_dependent: true + is_in_k12_school: false + childcare_hours_per_week: 40 + grade_school_weekly: + age: 8 + is_tax_unit_dependent: true + is_in_k12_school: true + childcare_hours_per_week: 20 + childcare_attending_days_per_month: 24 + after_school_only: + age: 8 + is_tax_unit_dependent: true + is_in_k12_school: true + after_school_expenses: 3_000 + preschool_after_school: + age: 4 + is_tax_unit_dependent: true + is_in_k12_school: false + after_school_expenses: 3_000 + after_school_and_other_care: + age: 8 + is_tax_unit_dependent: true + is_in_k12_school: true + after_school_expenses: 3_000 + pre_subsidy_childcare_expenses: 3_000 + after_school_long_days: + age: 8 + is_tax_unit_dependent: true + is_in_k12_school: true + after_school_expenses: 3_000 + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, short_days, many_short_days, grade_school, kindergarten, few_days, full_days, twelve_full_days, out_of_school, weekly_twenty, weekly_forty, grade_school_weekly, after_school_only, preschool_after_school, after_school_and_other_care, after_school_long_days] + households: + household: + members: [parent, short_days, many_short_days, grade_school, kindergarten, few_days, full_days, twelve_full_days, out_of_school, weekly_twenty, weekly_forty, grade_school_weekly, after_school_only, preschool_after_school, after_school_and_other_care, after_school_long_days] + state_code: CO + output: + # short_days: 0.55 * 21.67 = 11.92 units, under 13. + # many_short_days: 0.55 * 24 = 13.2 units. + # grade_school: 13.2 units, but section 3.124(W) covers an 8-year-old in school. + # kindergarten: 13.2 units, and kindergarten precedes grade 1. + # few_days: 10 days of unknown length, 10 units. + # full_days: 21.67 units. + # twelve_full_days: 12 units. + # out_of_school: 13.2 units, and the child attends no school. + # weekly_twenty: 20 / 5 = 4 hours a day, 11.92 units. + # weekly_forty: 8 hours a day, 21.67 units. + # grade_school_weekly: 24 * 12 / 52 = 5.54 days a week, 3.61 hours a day, + # 13.2 units, but section 3.124(W) covers an 8-year-old in school. + # after_school_only: section 3.124(W), with after-school care alone. + # preschool_after_school: not in school, so 21.67 units. + # after_school_and_other_care: other care leaves the schedule unknown. + # after_school_long_days: 8 hours a day, 21.67 units. + co_ccap_child_in_part_time_care: [false, true, false, true, false, true, false, true, false, true, false, true, true, false, false, false] + +- name: Case 2, an after-school expense with reported days of care. + period: 2026-01 + input: + people: + parent: + age: 35 + five_days_a_week: + age: 8 + is_tax_unit_dependent: true + after_school_expenses: 3_000 + childcare_days_per_week: 5 + twenty_attending_days: + age: 8 + is_tax_unit_dependent: true + after_school_expenses: 3_000 + childcare_attending_days_per_month: 20 + spm_units: + spm_unit: + members: [parent, five_days_a_week, twenty_attending_days] + households: + household: + members: [parent, five_days_a_week, twenty_attending_days] + state_code: CO + output: + # five_days_a_week: reported days, so not after-school care alone, and one + # full-time unit a day gives 21.67 units. + # twenty_attending_days: reported days too, 20 units. + co_ccap_child_in_part_time_care: [false, false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_fpg.yaml new file mode 100644 index 00000000000..d559c3fe679 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_fpg.yaml @@ -0,0 +1,116 @@ +# 8 CCR 1403-1 3.111(H)(2) lists the monthly federal poverty guideline through +# September 30, 2026, and 3.111(H)(3) from October 1, 2026 (PDF pages 41-42 of +# ruleVersionId 12658). The first is the 2025 HHS guideline and the second the +# 2026 guideline, each divided by 12. Yearly inputs carry their year, since a +# test of a month other than January doesn't apply undated yearly inputs. +- name: Case 1, January 2026 uses the guideline that took effect in October 2025. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: {} + person2: {} + person3: {} + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: + 2026: CO + output: + # (15,650 + 2 * 5,500) / 12 = 2,220.83, the 3.111(H)(2) amount for three. + co_ccap_fpg: 2_220.83 + +- name: Case 2, September 2026 still uses that guideline. + period: 2026-09 + absolute_error_margin: 0.01 + input: + people: + person1: {} + person2: {} + person3: {} + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: + 2026: CO + output: + co_ccap_fpg: 2_220.83 + +- name: Case 3, October 2026 moves to the 3.111(H)(3) guideline. + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + person1: {} + person2: {} + person3: {} + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: + 2026: CO + output: + # (15,960 + 2 * 5,680) / 12 = 2,276.67, the 3.111(H)(3) amount for three. + co_ccap_fpg: 2_276.67 + +- name: Case 4, one person in December 2025. + period: 2025-12 + absolute_error_margin: 0.01 + input: + state_code: + 2025: CO + output: + # 15,650 / 12 = 1,304.17 + co_ccap_fpg: 1_304.17 + +- name: Case 5, one person in October 2026. + period: 2026-10 + absolute_error_margin: 0.01 + input: + state_code: + 2026: CO + output: + # 15,960 / 12 = 1,330 + co_ccap_fpg: 1_330 + +- name: Case 6, eight people in January 2026. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: {} + person2: {} + person3: {} + person4: {} + person5: {} + person6: {} + person7: {} + person8: {} + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8] + state_code: + 2026: CO + output: + # (15,650 + 7 * 5,500) / 12 = 4,512.50 + co_ccap_fpg: 4_512.50 + +- name: Case 7, a household outside Colorado. + period: 2026-01 + input: + state_code: + 2026: TX + output: + co_ccap_fpg: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_full_time_equivalent_units.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_full_time_equivalent_units.yaml new file mode 100644 index 00000000000..110f69db0aa --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_full_time_equivalent_units.yaml @@ -0,0 +1,163 @@ +# 8 CCR 1403-1 3.103(ZZ) counts each part-time unit as 0.55 of a full-time +# unit, and 3.130(C) sets the daily units: part time up to 5 hours, full time +# up to 12, full time and part time up to 17, and two full-time units above +# that (PDF pages 10 and 119 of ruleVersionId 12658). A day of unknown length +# counts as one full-time unit, and weekly hours without daily hours spread +# over the days of care in a week. Days of care come from the larger of days a +# week x 52 / 12 and attending days a month, and a child with neither attends +# each weekday: 5 * 52 / 12 = 21.67 days a month. +- name: Case 1, units from daily hours and reported days. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + four_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 4 + eight_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + unknown_hours: + age: 3 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 6_000 + fourteen_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 14 + childcare_attending_days_per_month: 10 + twenty_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 20 + childcare_days_per_week: 2 + five_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 5 + childcare_attending_days_per_month: 20 + five_and_a_half_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 5.5 + childcare_attending_days_per_month: 12 + both_day_counts: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 10 + childcare_days_per_week: 5 + drop_in_days: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + childcare_days_per_week: 2 + childcare_attending_days_per_month: 12 + weekly_twenty: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_week: 20 + weekly_over_two_days: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_week: 20 + childcare_days_per_week: 2 + weekly_forty: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [parent, four_hours, eight_hours, unknown_hours, fourteen_hours, twenty_hours, five_hours, five_and_a_half_hours, both_day_counts, drop_in_days, weekly_twenty, weekly_over_two_days, weekly_forty] + households: + household: + members: [parent, four_hours, eight_hours, unknown_hours, fourteen_hours, twenty_hours, five_hours, five_and_a_half_hours, both_day_counts, drop_in_days, weekly_twenty, weekly_over_two_days, weekly_forty] + state_code: CO + output: + # parent: not in care. + # four_hours: 0.55 * 21.67 = 11.92. + # eight_hours: 1 * 21.67. + # unknown_hours: one full-time unit a day, 21.67. + # fourteen_hours: 1.55 * 10 = 15.5. + # twenty_hours: 2 * 2 * 52 / 12 = 17.33. + # five_hours: 5 hours is still part time, 0.55 * 20 = 11. + # five_and_a_half_hours: 1 * 12 = 12. + # both_day_counts: a five-day week (21.67 days) beats 10 attending days, + # 0.55 * 21.67 = 11.92. + # drop_in_days: 12 attending days beat a two-day week (8.67 days), + # 1 * 12 = 12. + # weekly_twenty: 20 / 5 = 4 hours a day, 0.55 * 21.67 = 11.92. + # weekly_over_two_days: 10 hours a day, 1 * 2 * 52 / 12 = 8.67. + # weekly_forty: 8 hours a day, 1 * 21.67. + co_ccap_full_time_equivalent_units: [0, 11.92, 21.67, 21.67, 15.5, 17.33, 11, 12, 11.92, 12, 11.92, 8.67, 21.67] + +- name: Case 2, each side of the 12-hour and 17-hour band edges. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + twelve_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 12 + childcare_attending_days_per_month: 10 + twelve_and_a_half_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 12.5 + childcare_attending_days_per_month: 10 + seventeen_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 17 + childcare_attending_days_per_month: 10 + seventeen_and_a_half_hours: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 17.5 + childcare_attending_days_per_month: 10 + spm_units: + spm_unit: + members: [parent, twelve_hours, twelve_and_a_half_hours, seventeen_hours, seventeen_and_a_half_hours] + households: + household: + members: [parent, twelve_hours, twelve_and_a_half_hours, seventeen_hours, seventeen_and_a_half_hours] + state_code: CO + output: + # twelve_hours: full time through 12 hours, 1 * 10 = 10. + # twelve_and_a_half_hours: full time and part time, 1.55 * 10 = 15.5. + # seventeen_hours: full time and part time through 17 hours, 15.5. + # seventeen_and_a_half_hours: two full-time units, 2 * 10 = 20. + co_ccap_full_time_equivalent_units: [0, 10, 15.5, 15.5, 20] + +- name: Case 3, weekly hours and units take the same larger day count. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + child: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_week: 20 + childcare_days_per_week: 2 + childcare_attending_days_per_month: 20 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + # 20 attending days exceed a two-day week (8.67 days). They make + # 20 * 12 / 52 = 4.62 days a week, so 20 / 4.62 = 4.33 hours a day, part + # time: 0.55 * 20 = 11. + co_ccap_full_time_equivalent_units: [0, 11] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_parent_fee.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_parent_fee.yaml index 612af5a8ee0..037a7c36d46 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_parent_fee.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_parent_fee.yaml @@ -1,273 +1,578 @@ -- name: One parent with two children, agi > hhs_fpg, both children eligible for cccap but not eligible for quality rating facility discount. - absolute_error_margin: 0.1 - period: 2023 +# 8 CCR 1403-1 3.124, PDF pages 79-85 of ruleVersionId 12658. A county charges +# one fee per household: the base fee on gross income ((P)(1) and (P)(2)) plus +# $15 for each child in care after the first above the guideline ((P)(3)), in +# whole dollars ((A)), at 55% when every child in care receives part-time +# care ((U) and (W)), and 20% lower at a provider in the top three quality +# levels ((H)). CDEC's parent fee tool drops the cents after each step, in that order: +# https://drive.google.com/file/d/1HtPiC2qxclzWfBa7LRo2Uohrg-RCBkyZ/view +# January to September 2026 use the guideline in 3.111(H)(2), and October to +# December 2026 the one in 3.111(H)(3). +- name: Case 1, a parent earning $40,000 with two children in full-time care. + period: 2026-01 + absolute_error_margin: 0.01 input: people: parent: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + age: 35 + employment_income: 40_000 child1: - is_disabled: false + age: 3 is_tax_unit_dependent: true - age: 6 - childcare_hours_per_day: 20 - co_quality_rating_of_child_care_facility: 1 + childcare_hours_per_day: 8 child2: - is_disabled: true + age: 5 is_tax_unit_dependent: true - age: 17 - childcare_hours_per_day: 4 - co_quality_rating_of_child_care_facility: 1 + childcare_hours_per_day: 8 spm_units: spm_unit: members: [parent, child1, child2] - co_ccap_eligible: - 2023-01: true - co_ccap_countable_income: 46_000 - co_ccap_base_parent_fee: - 2023-01: 287.18 - co_ccap_add_on_parent_fee: - 2023-01: 15 households: - household: + household: members: [parent, child1, child2] - county_str: BACA_COUNTY_CO state_code: CO output: - co_ccap_parent_fee: - 2023-01: 770.56 - # base + add_on: 287.18 + 15 = 302.18; - # child1: 302.18 * 2 + child2: 302.18 * .55 = 770.56; - # not qualified for quality rating discount; - # final parent fee: 770.56 + # 40,000 / 12 = 3,333.33 + co_ccap_countable_income: 3_333.33 + # (15,650 + 2 * 5,500) / 12 = 2,220.83 + co_ccap_fpg: 2_220.83 + # 0.01 * 3,333.33 + 0.14 * (3,333.33 - 2,220.83) = 33.33 + 155.75 = 189.08, + # which drops its cents to 189. + co_ccap_base_parent_fee: 189 + # (2 - 1) * 15 + co_ccap_add_on_parent_fee: 15 + # 189 + 15 = 204. + co_ccap_parent_fee: 204 + +- name: Case 2, the same family in October 2026, matching CDEC's parent fee tool. + period: 2026-10 + absolute_error_margin: 0.01 + # A test of a month other than January applies yearly inputs only when they + # carry their year. + input: + people: + parent: + age: + 2026: 35 + employment_income: + 2026: 40_000 + child1: + age: + 2026: 3 + is_tax_unit_dependent: + 2026: true + childcare_hours_per_day: + 2026: 8 + child2: + age: + 2026: 5 + is_tax_unit_dependent: + 2026: true + childcare_hours_per_day: + 2026: 8 + spm_units: + spm_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: + 2026: CO + output: + # (15,960 + 2 * 5,680) / 12 = 2,276.67 + co_ccap_fpg: 2_276.67 + # 0.01 * 3,333.33 + 0.14 * (3,333.33 - 2,276.67) = 33.3333 + 147.9324 = + # 181.2657, which drops its cents to 181. + co_ccap_base_parent_fee: 181 + # 181 + 15 = 196. The tool, with a family of 3, $40,000 a year and 2 + # children, gives the same full-time fee. + co_ccap_parent_fee: 196 -- name: Two parents with two children, agi > hhs_fpg, both children eligible for cccap and one eligible for quality rating facility discount. - absolute_error_margin: 0.1 - period: 2023 +- name: Case 3, with no recorded hours the children count as in full-time care. + period: 2026-01 input: people: - parent1: - is_disabled: true - is_tax_unit_dependent: false - age: 45 - parent2: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + parent: + age: 35 + employment_income: 40_000 child1: - is_disabled: false + age: 3 is_tax_unit_dependent: true - age: 6 - childcare_hours_per_day: 20 - co_quality_rating_of_child_care_facility: 3 child2: - is_disabled: true + age: 5 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child1, child2] + spm_unit_pre_subsidy_childcare_expenses: 12_000 + households: + household: + members: [parent, child1, child2] + state_code: CO + output: + co_ccap_children_in_care: 2 + co_ccap_child_in_part_time_care: [false, false, false] + co_ccap_parent_fee: 204 + +- name: Case 4, both children in part-time care pay 55% of the fee. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child1: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 4 + child2: + age: 4 is_tax_unit_dependent: true - age: 17 childcare_hours_per_day: 4 - co_quality_rating_of_child_care_facility: 1 spm_units: spm_unit: - members: [parent1, parent2, child1, child2] - co_ccap_eligible: - 2023-01: true - co_ccap_countable_income: 90_000 - co_ccap_base_parent_fee: - 2023-01: 749.38 - co_ccap_add_on_parent_fee: - 2023-01: 15 + members: [parent, child1, child2] households: - household: - members: [parent1, parent2, child1, child2] - county_str: BACA_COUNTY_CO + household: + members: [parent, child1, child2] state_code: CO output: - co_ccap_parent_fee: - 2023-01: 1_559.34 - # base + add_on: 749.38 + 15 = 764.38; - # child1: 764.38 * 2 + child2: 764.38 * .55 = 1_949.17; - # quality rating discount: 1_949.17 * .8 = 1_559.34 - # final parent fee: 1_559.34 + # Each child has 0.55 * 21.67 = 11.92 units a month, under 13. + co_ccap_child_in_part_time_care: [false, true, true] + # 204 * 0.55 = 112.2, which drops its cents to 112. + co_ccap_parent_fee: 112 -- name: Two parents with two children, agi <= hhs_fpg, both children eligible for cccap and one eligible for quality rating facility discount. - absolute_error_margin: 0.1 - period: 2023 +- name: Case 5, one child in full-time care keeps the full fee. + period: 2026-01 input: people: - parent1: - is_disabled: true - is_tax_unit_dependent: false - age: 45 - parent2: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + parent: + age: 35 + employment_income: 40_000 child1: - is_disabled: false + age: 3 is_tax_unit_dependent: true - age: 6 - childcare_hours_per_day: 20 - co_quality_rating_of_child_care_facility: 3 + childcare_hours_per_day: 4 child2: - is_disabled: true + age: 4 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: CO + output: + co_ccap_child_in_part_time_care: [false, true, false] + co_ccap_parent_fee: 204 + +- name: Case 6, the part-time rate and then the quality discount, each dropping cents. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 28_020 + child: + age: 3 is_tax_unit_dependent: true - age: 17 childcare_hours_per_day: 4 - co_quality_rating_of_child_care_facility: 4 + co_quality_rating_of_child_care_facility: 3 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + # 28,020 / 12 = 2,335; the guideline for two is 21,150 / 12 = 1,762.50. + # 0.01 * 2,335 + 0.14 * (2,335 - 1,762.50) = 23.35 + 80.15 = 103.50, so 103. + co_ccap_base_parent_fee: 103 + # Full time: 103. Part time: 103 * 0.55 = 56.65, so 56. Quality level 3: + # 56 * 0.8 = 44.8, so 44. The discount before the part-time rate would + # give 45, and so would rounding once at the end. + co_ccap_parent_fee: 44 + +- name: Case 7, a child in school with short days pays the part-time fee under section 3.124(W). + period: 2026-01 + input: + people: + parent: + age: 30 + employment_income: 30_000 + child: + age: 8 + is_tax_unit_dependent: true + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 24 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + # 0.55 * 24 = 13.2 units, but section 3.124(W) covers the 8-year-old. + co_ccap_full_time_equivalent_units: [0, 13.2] + co_ccap_child_in_part_time_care: [false, true] + # 0.01 * 2,500 + 0.14 * (2,500 - 1,762.50) = 25 + 103.25 = 128.25, so 128; + # 128 * 0.55 = 70.4, so 70. + co_ccap_parent_fee: 70 + +- name: Case 8, a kindergartner with the same schedule pays the full fee. + period: 2026-01 + input: + people: + parent: + age: 30 + employment_income: 30_000 + child: + age: 5 + is_tax_unit_dependent: true + childcare_hours_per_day: 4 + childcare_attending_days_per_month: 24 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + co_ccap_child_in_part_time_care: [false, false] + co_ccap_parent_fee: 128 + +- name: Case 9, a family with no income pays no fee. + period: 2026-01 + input: + people: + parent: + age: 30 + child: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + co_ccap_parent_fee: 0 + +- name: Case 10, a family with no child in care pays no fee. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child: + age: 4 + is_tax_unit_dependent: true spm_units: spm_unit: - members: [parent1, parent2, child1, child2] - co_ccap_eligible: - 2023-01: true - co_ccap_countable_income: 20_000 - co_ccap_base_parent_fee: - 2023-01: 16.67 - co_ccap_add_on_parent_fee: - 2023-01: 0 + members: [parent, child] households: - household: - members: [parent1, parent2, child1, child2] - county_str: CROWLEY_COUNTY_CO + household: + members: [parent, child] state_code: CO output: - co_ccap_parent_fee: - 2023-01: 34.01 - # base + add_on: 16.67 + 0 = 16.67; - # child1: 16.67 * 2 + child2: 16.67 * .55 = 42.51; - # quality rating discount: 42.51 * .8 = 34.01 - # final parent fee: 34.01 + co_ccap_children_in_care: 0 + co_ccap_parent_fee: 0 -- name: Two parents with one child, agi <= hhs_fpg, child eligible for cccap and quality rating facility discount. - absolute_error_margin: 0.1 - period: 2023 +- name: Case 11, income below the guideline adds no fee for the second child. + period: 2026-01 input: people: - parent1: - is_disabled: false - is_tax_unit_dependent: false - age: 45 - parent2: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + parent: + age: 35 + employment_income: 20_000 child1: - is_disabled: false + age: 3 is_tax_unit_dependent: true - age: 6 - childcare_hours_per_day: 15 - co_quality_rating_of_child_care_facility: 5 + childcare_hours_per_day: 8 + child2: + age: 5 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 spm_units: spm_unit: - members: [parent1, parent2, child1] - co_ccap_eligible: - 2023-01: true - co_ccap_countable_income: 20_000 - co_ccap_base_parent_fee: - 2023-01: 16.67 - co_ccap_add_on_parent_fee: - 2023-01: 0 + members: [parent, child1, child2] households: - household: - members: [parent1, parent2, child1] - county_str: CROWLEY_COUNTY_CO + household: + members: [parent, child1, child2] state_code: CO output: - co_ccap_parent_fee: - 2023-01: 20.67 - # base + add_on: 16.67 + 0 = 16.67; - # child1: 16.67 * 1.55 = 25.84; - # quality rating discount: 25.84 * .8 = 20.67 - # final parent fee: 20.67 + co_ccap_add_on_parent_fee: 0 + # 0.01 * 1,666.67 = 16.67, so 16. + co_ccap_parent_fee: 16 -- name: Two parents with two children, agi <= hhs_fpg, one eligible for cccap and quality rating facility discount, the other not. - absolute_error_margin: 0.1 - period: 2022 +- name: Case 12, a teenager sharing the expense adds no fee for an additional child. + period: 2026-01 + absolute_error_margin: 0.01 input: people: - parent1: - is_disabled: true - is_tax_unit_dependent: false - age: 45 - parent2: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + parent: + age: 40 + employment_income: 50_000 child1: - is_disabled: false + age: 3 is_tax_unit_dependent: true + child2: age: 6 - childcare_hours_per_day: 20 - co_quality_rating_of_child_care_facility: 3 + is_tax_unit_dependent: true + teenager: + age: 15 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child1, child2, teenager] + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [parent, child1, child2, teenager] + state_code: CO + output: + co_ccap_child_in_care: [false, true, true, false] + # 50,000 / 12 = 4,166.67; the guideline for four is 32,150 / 12 = 2,679.17. + # 0.01 * 4,166.67 + 0.14 * 1,487.50 = 41.67 + 208.25 = 249.92, so 249. + co_ccap_base_parent_fee: 249 + co_ccap_add_on_parent_fee: 15 + # 249 + 15 = 264. + co_ccap_parent_fee: 264 + +- name: Case 13, one provider in the top three quality levels is enough. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child1: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + co_quality_rating_of_child_care_facility: 2 child2: - is_disabled: false + age: 5 is_tax_unit_dependent: true - age: 17 - childcare_hours_per_day: 4 + childcare_hours_per_day: 8 co_quality_rating_of_child_care_facility: 4 spm_units: spm_unit: - members: [parent1, parent2, child1, child2] - co_ccap_eligible: - 2022-12: true - co_ccap_countable_income: 20_000 - co_ccap_base_parent_fee: - 2022-12: 16.67 - co_ccap_add_on_parent_fee: - 2022-12: 0 + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: CO + output: + # 204 * 0.8 = 163.2, so 163. + co_ccap_parent_fee: 163 + +- name: Case 14, a rated provider for a child outside care gives no discount. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + teenager: + age: 15 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + co_quality_rating_of_child_care_facility: 5 + spm_units: + spm_unit: + members: [parent, child, teenager] + households: + household: + members: [parent, child, teenager] + state_code: CO + output: + # One child in care, so no fee for an additional child: 189.08, so 189. + co_ccap_parent_fee: 189 + +- name: Case 15, a household outside Colorado. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + co_ccap_parent_fee: 0 + +- name: Case 16, weekly hours of care set the part-time rate. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_week: 20 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + # 20 hours over five weekdays: 4 hours a day, 0.55 * 21.67 = 11.92 units. + co_ccap_care_hours_per_day: [0, 4] + co_ccap_child_in_part_time_care: [false, true] + # 40,000 / 12 = 3,333.33; the guideline for two is 21,150 / 12 = 1,762.50. + # 0.01 * 3,333.33 + 0.14 * 1,570.83 = 33.33 + 219.92 = 253.25, so 253. + # 253 * 0.55 = 139.15, so 139. + co_ccap_parent_fee: 139 + +- name: Case 17, a child in grade school with only after-school expenses pays the part-time fee. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child: + age: 8 + is_tax_unit_dependent: true + after_school_expenses: 3_000 + spm_units: + spm_unit: + members: [parent, child] households: - household: - members: [parent1, parent2, child1, child2] - county_str: CROWLEY_COUNTY_CO + household: + members: [parent, child] state_code: CO output: - co_ccap_parent_fee: - 2022-12: 26.67 - # base + add_on: 16.67 + 0 = 16.67; - # child1: 16.67 * 2 = 33.34; - # quality rating discount: 33.34 * .8 = 26.67 - # final parent fee: 26.67 + co_ccap_child_in_part_time_care: [false, true] + # 253 * 0.55 = 139.15, so 139. + co_ccap_parent_fee: 139 -- name: One parent with two children, agi > hhs_fpg, one eligible for cccap but not eligible for quality rating facility discount, the other not eligible for cccap. - absolute_error_margin: 0.1 - period: 2022 +- name: Case 18, a fee just short of a whole dollar drops to the dollar below. + period: 2026-01 input: people: parent: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + age: 35 + employment_income: 23_999.64 child1: - is_disabled: false + age: 3 is_tax_unit_dependent: true - age: 6 - childcare_hours_per_day: 20 - co_quality_rating_of_child_care_facility: 1 + childcare_hours_per_day: 8 child2: - is_disabled: false + age: 5 is_tax_unit_dependent: true - age: 17 - childcare_hours_per_day: 4 - co_quality_rating_of_child_care_facility: 4 + childcare_hours_per_day: 8 spm_units: spm_unit: members: [parent, child1, child2] - co_ccap_eligible: - 2022-12: true - co_ccap_countable_income: 46_000 - co_ccap_base_parent_fee: - 2022-12: 287.18 - co_ccap_add_on_parent_fee: - 2022-12: 0 households: - household: + household: members: [parent, child1, child2] - county_str: BACA_COUNTY_CO state_code: CO output: - co_ccap_parent_fee: - 2022-12: 574.36 - # base + add_on: 287.18 + 0 = 287.18; - # child1: 287.18 * 2 = 574.36; - # not qualified for quality rating discount - # final parent fee: 574.36 + # 23,999.64 / 12 = 1,999.97, below the 2,220.83 guideline. + # 0.01 * 1,999.97 = 19.9997, so 19. + co_ccap_parent_fee: 19 + +- name: Case 19, July 2028 still charges the section 3.124(P) fee. + period: 2028-07 + input: + people: + parent: + age: + 2028: 35 + child1: + age: + 2028: 3 + is_tax_unit_dependent: + 2028: true + childcare_hours_per_day: + 2028: 8 + child2: + age: + 2028: 5 + is_tax_unit_dependent: + 2028: true + childcare_hours_per_day: + 2028: 8 + spm_units: + spm_unit: + members: [parent, child1, child2] + co_ccap_countable_income: 3_500 + co_ccap_fpg: 2_000 + households: + household: + members: [parent, child1, child2] + state_code: + 2028: CO + output: + # 0.01 * 3,500 + 0.14 * 1,500 = 35 + 210 = 245, and 15 for the second child. + co_ccap_base_parent_fee: 245 + co_ccap_add_on_parent_fee: 15 + co_ccap_parent_fee: 260 + +- name: Case 20, August 2028 charges the section 3.124(Q) fee, with no add-on. + period: 2028-08 + input: + people: + parent: + age: + 2028: 35 + child1: + age: + 2028: 3 + is_tax_unit_dependent: + 2028: true + childcare_hours_per_day: + 2028: 8 + child2: + age: + 2028: 5 + is_tax_unit_dependent: + 2028: true + childcare_hours_per_day: + 2028: 8 + spm_units: + spm_unit: + members: [parent, child1, child2] + co_ccap_countable_income: 3_500 + co_ccap_fpg: 2_000 + households: + household: + members: [parent, child1, child2] + state_code: + 2028: CO + output: + # 1% of 2,000, 3% of 600 (to 130%), 5% of 600 (to 160%) and 7% of the + # last 300: 20 + 18 + 30 + 21 = 89. + co_ccap_base_parent_fee: 89 + co_ccap_add_on_parent_fee: 0 + co_ccap_parent_fee: 89 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/court_supervision.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/court_supervision.yaml index 7d7253bca2c..1ce3460e0af 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/court_supervision.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/court_supervision.yaml @@ -90,16 +90,19 @@ co_ccap_smi: 7_863.07 co_ccap_smi_eligible: true co_ccap_eligible: true - # Base fee: 0.01 * 1,803.33 + 0.14 * (2,500 - 1,803.33) - # = 18.03 + 97.53 = 115.57 - co_ccap_base_parent_fee: 115.57 - # Add-on: (1 eligible child - 1) * 15 = 0 + # The fee uses the guideline 3.111(H)(2) lists through September 2026: + # (15,650 + 5,500) / 12 = 1,762.50. + co_ccap_fpg: 1_762.50 + # Base fee: 0.01 * 2,500 + 0.14 * (2,500 - 1,762.50) = 25 + 103.25 = 128.25, + # which drops its cents to 128. + co_ccap_base_parent_fee: 128 + # Add-on: (1 child in care - 1) * 15 = 0 co_ccap_add_on_parent_fee: 0 - # 8 hours/day => hours rate 1; facility rating 0 => no discount (1). - # 115.57 * 1 * 1 = 115.57 - co_ccap_parent_fee: 115.57 - # Expenses 12,000 / 12 = 1,000 - 115.57 = 884.43 - co_ccap_subsidy: 884.43 + # 8 hours a day is full-time care, and a facility rating of 0 gives no + # discount. + co_ccap_parent_fee: 128 + # Expenses 12,000 / 12 = 1,000 - 128 = 872 + co_ccap_subsidy: 872 - name: Case 6, without court supervision the same teenager receives nothing. period: 2026-01 diff --git a/policyengine_us/tests/test_co_ccap_parent_fee_properties.py b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py new file mode 100644 index 00000000000..735e1e70d75 --- /dev/null +++ b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py @@ -0,0 +1,463 @@ +"""Colorado's Child Care Assistance Program charges each household one parent fee. + +8 CCR 1403-1 section 3.124 (ruleVersionId 12658, PDF pages 79-85) builds the +fee from gross countable income and household size. Through July 2028 counties +charge 1% of income plus 14% of income above the federal poverty guideline +((P)(1) and (P)(2)), and $15 for each child in care after the first when income +exceeds the guideline ((P)(3)). From August 2028 they charge marginal rates of +1%, 3%, 5% and 7% ((Q)). The fee comes in whole dollars ((A)), at 55% when every +child in care receives part-time care ((U) and (W)), and 20% lower at a provider +in the top three quality levels ((H)). The Department's parent fee tool drops +the cents after each step. + +Invariants, which these tests check on generated households: + +1. The fee never falls below zero and always comes to whole dollars. +2. Through July 2028, at a given income and household size, each child in + full-time care after the first adds $15 to the fee at a provider below + quality level 3 when income exceeds the guideline, and nothing when it does + not. +3. At a provider below quality level 3, the fee falls to 55% of the full-time + fee, without cents, when every child in care receives part-time care, and + stays at the full-time fee when any child in care receives full-time care. +4. A provider in the top three quality levels, for any child in care, cuts the + fee by 20% without cents. A lower rating, or a rating for a child outside + care, changes nothing. + +A differential test compares the model with `reference_fee`, written from the +rule in exact decimal arithmetic, in January and October 2026 and in July and +August 2028, on incomes in whole dollars and in cents. The reference uses the +model's schedule defaults and, for 2028, its projected guideline. +""" + +from decimal import ROUND_FLOOR, ROUND_HALF_EVEN, Decimal + +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 +CENT = Decimal("0.01") +# Each child in care carries this monthly charge, so the child counts as in +# care whatever the schedule says. +MONTHLY_CHARGE = 400 +# Section 3.111(H)(2) lists the guideline through September 2026, and section +# 3.111(H)(3) the one from October 2026: annual dollars for the first person +# and for each additional person. +GUIDELINES = { + "2026-01": (Decimal(15_650), Decimal(5_500)), + "2026-10": (Decimal(15_960), Decimal(5_680)), +} +# Section 3.124(Q) applies from this month. +FIRST_MONTH_OF_MARGINAL_RATES = "2028-08" +# Without a schedule, the model reads care on each weekday. +ASSUMED_DAYS_PER_WEEK = Decimal(5) +ASSUMED_DAYS_PER_MONTH = ASSUMED_DAYS_PER_WEEK * 52 / 12 + + +def floor(amount): + return amount.to_integral_value(rounding=ROUND_FLOOR) + + +def in_cents(amount): + return amount.quantize(CENT, rounding=ROUND_HALF_EVEN) + + +def monthly_income(annual_income): + return in_cents(max(Decimal(annual_income), Decimal(0)) / 12) + + +def daily_units(hours): + """Full-time-equivalent units for a day of care (3.103(ZZ) and 3.130(C)).""" + if hours == 0: + # The model reads unknown hours as a full-time day. + return Decimal(1) + if hours <= 5: + return Decimal("0.55") + if hours <= 12: + return Decimal(1) + if hours <= 17: + return Decimal("1.55") + return Decimal(2) + + +def care_hours(child): + """Reported daily hours, or weekly hours over the child's days of care.""" + if child["hours"]: + return Decimal(str(child["hours"])) + reported_days = max(Decimal(child["week"]), Decimal(child["days"]) * 12 / 52) + days_in_a_week = reported_days or ASSUMED_DAYS_PER_WEEK + return Decimal(str(child["weekly"])) / days_in_a_week + + +def part_time(child): + """Fewer than 13 units a month (3.124(U)), or up to 5 hours a day for a + child in grades 1 to 12 (3.124(W)). PolicyEngine has no grade input, so a + child in school (ages 5 to 17) aged 6 or older counts as in grade 1 or + above, as in the model, and such a child whose only reported care is an + after-school expense counts as in part-time care. Days of care come from + the larger of days a week x 52 / 12 and attending days a month, as in the + model.""" + hours = care_hours(child) + scheduled_days = Decimal(child["week"] * 52) / 12 + days = max(scheduled_days, Decimal(child["days"])) or ASSUMED_DAYS_PER_MONTH + in_grade_one_or_above = 6 <= child["age"] <= 17 + no_schedule = hours == 0 and child["week"] == 0 and child["days"] == 0 + after_school_only = child["after_school"] and no_schedule + return daily_units(hours) * days < 13 or ( + in_grade_one_or_above and (0 < hours <= 5 or after_school_only) + ) + + +def guideline(month, children): + """The monthly guideline in cents for a parent and these children.""" + first, additional = GUIDELINES[month] + return in_cents((first + additional * len(children)) / 12) + + +def marginal_fee(income, limit): + """Section 3.124(Q): each rate applies to the income in its band.""" + bands = [ + (Decimal("0.01"), Decimal(0), limit), + (Decimal("0.03"), limit, limit * Decimal("1.3")), + (Decimal("0.05"), limit * Decimal("1.3"), limit * Decimal("1.6")), + ] + fee = sum(rate * max(min(income, upper) - lower, 0) for rate, lower, upper in bands) + return fee + Decimal("0.07") * max(income - limit * Decimal("1.6"), 0) + + +def reference_fee(month, annual_income, children, limit): + """Section 3.124, step by step, dropping cents as the Department's tool does.""" + in_care = [child for child in children if child["in_care"]] + if not in_care: + return Decimal(0) + income = monthly_income(annual_income) + if month >= FIRST_MONTH_OF_MARGINAL_RATES: + base = marginal_fee(income, limit) + add_on = 0 + else: + base = income * Decimal("0.01") + max(income - limit, 0) * Decimal("0.14") + add_on = 15 * (len(in_care) - 1) if income > limit else 0 + fee = floor(base + add_on) + if all(part_time(child) for child in in_care): + fee = floor(fee * Decimal("0.55")) + if max(child["rating"] for child in in_care) >= 3: + fee = floor(fee * Decimal("0.8")) + return fee + + +def simulate(month, households): + """One simulation of these households. + + Each household holds one parent and the given children. A child in care + has the given schedule and a monthly charge, after school if the child + says so; a child outside care has neither.""" + year = int(month[:4]) + groups = ("tax_units", "spm_units", "families", "marital_units", "households") + situation = {"people": {}, **{group: {} for group in groups}} + for i, household in enumerate(households): + parent = f"parent_{i}" + members = [parent] + situation["people"][parent] = { + "age": {year: 35}, + "employment_income": {year: float(household["income"])}, + "self_employment_income": {year: household.get("loss", 0)}, + } + for j, child in enumerate(household["children"]): + name = f"child_{i}_{j}" + person = { + "age": {year: child["age"]}, + "is_tax_unit_dependent": {year: True}, + "co_quality_rating_of_child_care_facility": {year: child["rating"]}, + } + if child["in_care"]: + person["childcare_hours_per_day"] = {year: child["hours"]} + person["childcare_attending_days_per_month"] = {year: child["days"]} + person["childcare_days_per_week"] = {year: child["week"]} + if child["weekly"]: + person["childcare_hours_per_week"] = {year: child["weekly"]} + charge = {year: MONTHLY_CHARGE * 12} + if child["after_school"]: + person["after_school_expenses"] = charge + else: + person["pre_subsidy_childcare_expenses"] = charge + situation["people"][name] = person + members.append(name) + for group in groups: + situation[group][f"{group}_{i}"] = {"members": list(members)} + situation["marital_units"][f"marital_units_{i}"] = {"members": [parent]} + situation["households"][f"households_{i}"]["state_name"] = {year: "CO"} + return Simulation(situation=situation) + + +def fees(month, households): + """The model's fee for each household, from one simulation.""" + simulation = simulate(month, households) + return [float(fee) for fee in simulation.calculate("co_ccap_parent_fee", month)] + + +def child( + age=3, in_care=True, hours=8, days=0, week=0, rating=0, weekly=0, after_school=False +): + return dict( + age=age, + in_care=in_care, + hours=hours, + days=days, + week=week, + rating=rating, + weekly=weekly, + after_school=after_school, + ) + + +months = st.sampled_from(sorted(GUIDELINES)) +# Either side of the switch to section 3.124(Q), as well as the published months. +months_through_2028 = st.sampled_from([*sorted(GUIDELINES), "2028-07", "2028-08"]) +incomes = st.integers(min_value=0, max_value=150_000) +# Twelve times a monthly amount in cents, so the month comes to whole cents. +incomes_in_cents = st.integers(min_value=0, max_value=1_250_000).map( + lambda cents: Decimal(cents) * 12 / 100 +) +# Hours straddle each band edge in section 3.130(C); 0 means unknown. +daily_hours = st.sampled_from([0, 0, 0, 1, 4.5, 5, 5.5, 8, 12, 12.5, 16, 17, 18]) +# Weekly hours count only when daily hours are unknown; 0 means none reported. +weekly_hours = st.sampled_from([0, 10, 15, 20, 25, 36, 40, 60]) +# Attending days per month; 0 means none reported. +monthly_days = st.integers(min_value=0, max_value=23) +# Scheduled days per week; 0 means none reported. +weekly_days = st.integers(min_value=0, max_value=7) +child_ages = st.integers(min_value=0, max_value=12) +any_children = st.builds( + child, + age=child_ages, + in_care=st.booleans(), + hours=daily_hours, + days=monthly_days, + week=weekly_days, + rating=st.integers(min_value=0, max_value=5), + weekly=weekly_hours, + after_school=st.booleans(), +) + + +@settings(max_examples=25, deadline=None) +# The Department's tool formula: $40,000, a family of 3 and 2 children, full time. +@example(month="2026-01", income=40_000, children=[child(), child(age=5)]) +@example(month="2026-10", income=40_000, children=[child(), child(age=5)]) +# $1,199 a year gives a base fee of $0.9992, which drops to $0. +@example(month="2026-01", income=1_199, children=[child()]) +# $21,500 for two gives a base fee just above $22. +@example(month="2026-01", income=21_500, children=[child()]) +# $23,999.64 gives $19.9997, and $11,999.64 gives $9.9997: each drops a dollar. +@example(month="2026-01", income=Decimal("23999.64"), children=[child(), child()]) +@example(month="2026-01", income=Decimal("11999.64"), children=[child()]) +# $21,180 for two gives a fee of exactly $18, which float64 arithmetic takes to +# $17.9999999999999. +@example(month="2026-01", income=21_180, children=[child()]) +# $26,713.32 for three gives $23.0003 on the guideline in cents, $2,220.83. +# Unrounded, the guideline of $2,220.8333 would give $22.9998. +@example(month="2026-01", income=Decimal("26713.32"), children=[child(), child()]) +# Income at the guideline adds nothing for the second child. +@example(month="2026-01", income=26_650, children=[child(), child()]) +# 13 days of 8 hours make 13 units, full time; 12 days make 12, part time. +@example(month="2026-01", income=60_000, children=[child(days=13), child(days=12)]) +# 12 attending days beat a two-day week (8.67 days): 12 units, part time. +# A three-day week (13 days) beats 10 attending days: 13 units, full time. +@example(month="2026-01", income=60_000, children=[child(week=2, days=12)]) +@example(month="2026-01", income=60_000, children=[child(week=3, days=10)]) +# A 6-year-old with 5-hour days is part time under (W) at 24 days; a +# 5-year-old with the same days has 13.2 units, full time. +@example(month="2026-01", income=30_000, children=[child(age=6, hours=5, days=24)]) +@example(month="2026-01", income=30_000, children=[child(age=5, hours=5, days=24)]) +# The part-time rate, then the quality discount. +@example(month="2026-01", income=28_020, children=[child(hours=4, rating=3)]) +# $28,056 for two gives a base fee of $103.95, which drops to $103 before the +# part-time rate: 55% of $103 comes to $56.65, so $56. +@example(month="2026-01", income=28_056, children=[child(hours=4)]) +# A child outside care with a top-rated provider. +@example( + month="2026-01", + income=50_000, + children=[child(hours=4), child(age=9, in_care=False, rating=5)], +) +# 20 hours a week over five weekdays: 4 hours a day, 11.92 units, part time. +@example(month="2026-01", income=40_000, children=[child(hours=0, weekly=20)]) +# 20 hours a week over two days: 10 hours a day, 8.67 units, part time. +@example(month="2026-01", income=40_000, children=[child(hours=0, weekly=20, week=2)]) +# 40 hours a week over five weekdays: 8 hours a day, full time. +@example(month="2026-01", income=40_000, children=[child(hours=0, weekly=40)]) +# 15 hours a week over 13 attending days (three a week): 5 hours a day. +@example(month="2026-01", income=40_000, children=[child(hours=0, weekly=15, days=13)]) +# 20 attending days give more days a week (4.62) than a two-day week: 4.33 hours +# a day, 11 units, part time. +@example( + month="2026-01", + income=40_000, + children=[child(hours=0, weekly=20, week=2, days=20)], +) +# An 8-year-old with only after-school expenses receives part-time care; a +# 4-year-old with the same expense has no school day to fit it around. +@example( + month="2026-01", + income=40_000, + children=[child(age=8, hours=0, after_school=True)], +) +@example( + month="2026-01", + income=40_000, + children=[child(age=4, hours=0, after_school=True)], +) +# The switch to section 3.124(Q). +@example(month="2028-07", income=60_000, children=[child(), child()]) +@example(month="2028-08", income=60_000, children=[child(), child()]) +@given( + month=months_through_2028, + income=st.one_of(incomes, incomes_in_cents), + children=st.lists(any_children, min_size=1, max_size=4), +) +def test_the_fee_matches_an_independent_implementation(month, income, children): + simulation = simulate(month, [{"income": income, "children": children}]) + fee = float(simulation.calculate("co_ccap_parent_fee", month)[0]) + if month in GUIDELINES: + limit = guideline(month, children) + else: + limit = in_cents(Decimal(float(simulation.calculate("co_ccap_fpg", month)[0]))) + expected = float(reference_fee(month, income, children, limit)) + assert fee == pytest.approx(expected, abs=TOLERANCE) + + +@settings(max_examples=15, deadline=None) +@example(month="2026-01", income=0, loss=-20_000, children=[child()]) +@example(month="2028-10", income=150_000, loss=0, children=[child(), child()]) +@given( + month=st.sampled_from(["2026-01", "2026-10", "2028-10"]), + income=incomes, + loss=st.integers(min_value=-50_000, max_value=0), + children=st.lists(any_children, min_size=1, max_size=4), +) +def test_the_fee_is_never_negative_and_in_whole_dollars(month, income, loss, children): + household = {"income": income, "loss": loss, "children": children} + (fee,) = fees(month, [household]) + assert fee >= 0 + assert fee == pytest.approx(round(fee), abs=TOLERANCE) + + +@settings(max_examples=15, deadline=None) +@example(month="2026-01", income=40_000, ages=[3, 5]) +@example(month="2026-01", income=20_000, ages=[3, 5, 8]) +# The guideline for four from October 2026 is $33,000. +@example(month="2026-10", income=33_000, ages=[1, 2, 3]) +@example(month="2026-10", income=33_001, ages=[1, 2, 3]) +@given( + month=months, + income=incomes, + ages=st.lists(child_ages, min_size=2, max_size=4), +) +def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( + month, income, ages +): + # The same household with 0, 1, ... of its children in full-time care. + households = [ + { + "income": income, + "children": [ + child(age=age, in_care=j < count) for j, age in enumerate(ages) + ], + } + for count in range(len(ages) + 1) + ] + result = fees(month, households) + above = monthly_income(income) > guideline(month, ages) + assert result[0] == 0 + for count in range(1, len(ages) + 1): + added = 15 * (count - 1) if above else 0 + assert result[count] - result[1] == pytest.approx(added, abs=TOLERANCE) + + +@settings(max_examples=15, deadline=None) +@example(month="2026-01", income=40_000, children=[child(hours=4), child(hours=4)]) +@example(month="2026-01", income=40_000, children=[child(hours=4), child(hours=8)]) +@example(month="2026-01", income=30_000, children=[child(age=8, hours=4, days=24)]) +@example(month="2026-01", income=40_000, children=[child(hours=0, weekly=20)]) +@given( + month=months, + income=incomes, + children=st.lists( + st.builds( + child, + age=child_ages, + hours=daily_hours, + days=monthly_days, + week=weekly_days, + weekly=weekly_hours, + after_school=st.booleans(), + ), + min_size=1, + max_size=4, + ), +) +def test_the_part_time_rate_applies_only_when_every_child_in_care_is_part_time( + month, income, children +): + full_time = [ + dict(child, hours=8, days=0, week=0, weekly=0, after_school=False) + for child in children + ] + fee, full_time_fee = fees( + month, + [ + {"income": income, "children": children}, + {"income": income, "children": full_time}, + ], + ) + if all(part_time(child) for child in children): + expected = floor(Decimal(round(full_time_fee)) * Decimal("0.55")) + else: + expected = full_time_fee + assert fee == pytest.approx(float(expected), abs=TOLERANCE) + + +@settings(max_examples=15, deadline=None) +@example(month="2026-01", income=40_000, children=[child(), child()], rating=3) +@example(month="2026-01", income=40_000, children=[child(hours=4)], rating=2) +@example( + month="2026-01", + income=28_020, + children=[child(hours=4), child(age=10, in_care=False)], + rating=5, +) +@given( + month=months, + income=incomes, + children=st.lists(any_children, min_size=1, max_size=4), + rating=st.integers(min_value=0, max_value=5), +) +def test_a_top_three_provider_cuts_the_fee_by_20_percent( + month, income, children, rating +): + unrated = [dict(child, rating=0) for child in children] + # Make sure a child is in care, and give that child the rating. + unrated[0]["in_care"] = True + rated = [dict(child) for child in unrated] + rated[0]["rating"] = rating + # The same rating for each child outside care instead. + outside = [ + dict(child, rating=0 if child["in_care"] else rating) for child in unrated + ] + unrated_fee, rated_fee, outside_fee = fees( + month, + [ + {"income": income, "children": unrated}, + {"income": income, "children": rated}, + {"income": income, "children": outside}, + ], + ) + if rating >= 3: + expected = floor(Decimal(round(unrated_fee)) * Decimal("0.8")) + else: + expected = unrated_fee + assert rated_fee == pytest.approx(float(expected), abs=TOLERANCE) + assert outside_fee == pytest.approx(unrated_fee, abs=TOLERANCE) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py index e6382e0de6e..7df8c158e09 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py @@ -1,6 +1,6 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( - co_ccap_parameter_instant, +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parent_fee_arithmetic import ( + income_and_guideline_in_cents, ) @@ -8,24 +8,21 @@ class co_ccap_add_on_parent_fee(Variable): value_type = float entity = SPMUnit label = "Colorado Child Care Assistance Program add on parent fee" + documentation = ( + "The fee for each child in care after the first, charged when gross " + "countable income exceeds the federal poverty guideline." + ) reference = ( - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=41", - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=62", + # PDF pages 83-84 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=83", ) unit = USD definition_period = MONTH + defined_for = StateCode.CO def formula(spm_unit, period, parameters): - instant_str = co_ccap_parameter_instant(period) - p = parameters(instant_str).gov.states.co.ccap - # Calculate base parent fee and add on parent fee. - gross_income = spm_unit("co_ccap_countable_income", period) - fpg = spm_unit("spm_unit_fpg", period) - eligible_children = spm_unit("co_ccap_eligible_children", period) - # Calculate add-on parent fee based on the number of eligible - # children in a household and income: - # When income <= fpg: 0 - # When income > fpg: 15 for each additional child - add_on_parent_fee_amount = (eligible_children - 1) * p.parent_fee.add_on - add_on_parent_fee_applies = gross_income > fpg - return add_on_parent_fee_amount * add_on_parent_fee_applies + p = parameters(period).gov.states.co.ccap.parent_fee + income, fpg = income_and_guideline_in_cents(spm_unit, period) + children_in_care = spm_unit("co_ccap_children_in_care", period) + additional_children = max_(children_in_care - 1, 0) + return additional_children * p.add_on * (income > fpg) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py index a8e2a3163c5..81535104546 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py @@ -1,6 +1,7 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( - co_ccap_parameter_instant, +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parent_fee_arithmetic import ( + drop_cents, + income_and_guideline_in_cents, ) @@ -8,23 +9,29 @@ class co_ccap_base_parent_fee(Variable): value_type = float entity = SPMUnit label = "Colorado Child Care Assistance Program base parent fee" + documentation = ( + "The parent fee on gross countable income, in whole dollars, before " + "the fee for additional children, the part-time rate and the quality " + "discount. Through July 2028 counties charge 1% of income plus 14% of " + "income above the federal poverty guideline; from August 2028 they " + "charge marginal rates of 1%, 3%, 5% and 7%." + ) reference = ( - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=41", - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=62", + # PDF pages 82-84 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=82", ) unit = USD definition_period = MONTH + defined_for = StateCode.CO def formula(spm_unit, period, parameters): - instant_str = co_ccap_parameter_instant(period) - p = parameters(instant_str).gov.states.co.ccap - # Calculate base parent fee and add on parent fee. - gross_income = spm_unit("co_ccap_countable_income", period) - fpg = spm_unit("spm_unit_fpg", period) - # Calculate base parent fee scaled (note income is monthly): - # When income_scaled <= 1: income_scaled * 0.01 - # When income_scaled > 1: [1 * 0.01 + (income_scaled - 1) * 0.14] - # Multiply by fpg afterward to scale back up - gross_income_fpg_ratio = gross_income / fpg - base_parent_fee_scaled = p.parent_fee.base.calc(gross_income_fpg_ratio) - return base_parent_fee_scaled * fpg + p = parameters(period).gov.states.co.ccap.parent_fee + # Families with no income pay no fee (section 3.124(I)(7)); a net loss + # counts as no income. + income, fpg = income_and_guideline_in_cents(spm_unit, period) + income_to_fpg = np.zeros_like(income) + has_fpg = fpg > 0 + income_to_fpg[has_fpg] = income[has_fpg] / fpg[has_fpg] + return drop_cents( + p.income_rate * income + p.marginal_rate.calc(income_to_fpg) * fpg + ) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_care_hours_per_day.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_care_hours_per_day.py new file mode 100644 index 00000000000..52d6558007e --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_care_hours_per_day.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + +# Without a schedule, the model reads a five-day week of care. It imputes +# this, so the number stays out of the parameters. +WEEKDAYS_PER_WEEK = 5 + + +class co_ccap_care_hours_per_day(Variable): + value_type = float + entity = Person + label = "Colorado Child Care Assistance Program hours of care a day" + unit = "hour" + documentation = ( + "A child's hours of care a day: the reported daily hours, or weekly " + "hours spread over the child's days of care in a week. Zero means the " + "model has no hours for the child." + ) + reference = ( + # 8 CCR 1403-1 3.130(C), PDF page 119: units of care follow hours a day. + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=119", + ) + definition_period = MONTH + defined_for = StateCode.CO + + def formula(person, period, parameters): + year = period.this_year + daily_hours = person("childcare_hours_per_day", year) + weekly_hours = person("childcare_hours_per_week", year) + days_per_week = person("childcare_days_per_week", year) + attending_days = person("childcare_attending_days_per_month", year) + # co_ccap_full_time_equivalent_units takes the same larger day count. + reported_days = max_( + days_per_week, attending_days * MONTHS_IN_YEAR / WEEKS_IN_YEAR + ) + days_in_a_week = where(reported_days > 0, reported_days, WEEKDAYS_PER_WEEK) + return where(daily_hours > 0, daily_hours, weekly_hours / days_in_a_week) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_care.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_care.py new file mode 100644 index 00000000000..369e4fc6f31 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_care.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class co_ccap_child_in_care(Variable): + value_type = bool + entity = Person + label = "Child in care under the Colorado Child Care Assistance Program" + documentation = ( + "An eligible child for whom the household requests care. Reported care " + "hours or days show care, and so does a child care or after-school " + "expense, since surveys ask what families pay for care but not when " + "the care happens." + ) + reference = ( + # PDF pages 79, 84 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=79", + ) + definition_period = MONTH + defined_for = StateCode.CO + + def formula(person, period, parameters): + year = period.this_year + eligible = person("co_ccap_child_eligible", period) + has_schedule = ( + (person("childcare_hours_per_day", year) > 0) + | (person("childcare_hours_per_week", year) > 0) + | (person("childcare_days_per_week", year) > 0) + | (person("childcare_attending_days_per_month", year) > 0) + ) + expenses = add( + person, + year, + ["pre_subsidy_childcare_expenses", "after_school_expenses"], + ) + return eligible & (has_schedule | (expenses > 0)) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_part_time_care.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_part_time_care.py new file mode 100644 index 00000000000..29f9493b6ee --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_part_time_care.py @@ -0,0 +1,55 @@ +from policyengine_us.model_api import * + +# PolicyEngine has no school grade input, so a child in school aged 6 or older +# is taken to be in grade 1 or above. This is an imputation, not a rule, so it +# is not a parameter. +FIRST_GRADE_AGE = 6 + + +class co_ccap_child_in_part_time_care(Variable): + value_type = bool + entity = Person + label = "Child in part-time care for the Colorado Child Care Assistance Program parent fee" + documentation = ( + "Care averaging fewer than 13 full-time equivalent units a month is part " + "time (section 3.124(U)), and so is care of up to 5 hours a day for a " + "child in grades 1 to 12 (section 3.124(W)). The model also reads a " + "child in grades 1 to 12 whose only reported care is an after-school " + "expense as in part-time care. Care schedules do not change within a " + "year here, so the majority-of-months rule in section 3.124(V) gives " + "the same answer." + ) + reference = ( + # PDF pages 10, 85, 119 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=10", + ) + definition_period = MONTH + defined_for = "co_ccap_child_in_care" + + def formula(person, period, parameters): + p = parameters(period).gov.states.co.ccap.parent_fee.part_time + units = person("co_ccap_full_time_equivalent_units", period) + part_time_units = units < p.unit_threshold + year = period.this_year + daily_hours = person("co_ccap_care_hours_per_day", period) + part_time_days = (daily_hours > 0) & ( + p.full_time_equivalent_units.calc(daily_hours, right=True) < 1 + ) + in_grade_one_or_above = person("is_in_k12_school", year) & ( + person("age", year) >= FIRST_GRADE_AGE + ) + # After-school care fills part of a school day, and a child with no + # other schedule or care expense has that care in most months (3.124(V)). + no_schedule = ( + (daily_hours == 0) + & (person("childcare_days_per_week", year) == 0) + & (person("childcare_attending_days_per_month", year) == 0) + ) + after_school_only = ( + no_schedule + & (person("after_school_expenses", year) > 0) + & (person("pre_subsidy_childcare_expenses", year) == 0) + ) + return part_time_units | ( + in_grade_one_or_above & (part_time_days | after_school_only) + ) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_children_in_care.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_children_in_care.py new file mode 100644 index 00000000000..d12f4811d3a --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_children_in_care.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class co_ccap_children_in_care(Variable): + value_type = int + entity = SPMUnit + label = ( + "Number of children in care under the Colorado Child Care Assistance Program" + ) + reference = ( + # PDF pages 79, 84 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=79", + ) + definition_period = MONTH + defined_for = StateCode.CO + adds = ["co_ccap_child_in_care"] diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_fpg.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_fpg.py new file mode 100644 index 00000000000..720f4613125 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_fpg.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) + + +class co_ccap_fpg(Variable): + value_type = float + entity = SPMUnit + label = "Colorado Child Care Assistance Program monthly federal poverty guideline" + unit = USD + documentation = ( + "The monthly federal poverty guideline that Colorado's Child Care " + "Assistance Program uses for parent fees. Section 3.111(H) sets the " + "guideline each October 1, so January to September use the guideline " + "published the previous calendar year." + ) + reference = ( + # PDF pages 41-42, 84 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=41", + ) + definition_period = MONTH + defined_for = StateCode.CO + + def formula(spm_unit, period, parameters): + size = spm_unit("spm_unit_size", period.this_year) + state_group = spm_unit.household("state_group_str", period.this_year) + instant_str = co_ccap_parameter_instant(period) + return fpg(size, state_group, instant_str, parameters) / MONTHS_IN_YEAR diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_full_time_equivalent_units.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_full_time_equivalent_units.py new file mode 100644 index 00000000000..4262b3e4fa0 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_full_time_equivalent_units.py @@ -0,0 +1,45 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_care_hours_per_day import ( + WEEKDAYS_PER_WEEK, +) + + +class co_ccap_full_time_equivalent_units(Variable): + value_type = float + entity = Person + label = "Colorado Child Care Assistance Program full-time equivalent units of care per month" + documentation = ( + "A child's monthly units of care, with each part-time unit counted as " + "0.55 of a full-time unit. A day of care of unknown length counts as " + "one full-time unit. Days of care come from the larger of the weekly " + "schedule (days a week times 52 / 12) and attending days a month, and " + "a child with neither attends each weekday." + ) + reference = ( + # 8 CCR 1403-1 3.103(ZZ), 3.124(U) and (V), and 3.130(C) and (F), PDF + # pages 10, 85 and 119-120: full-time equivalent units, part-time + # fees, daily units and drop-in days. + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=10", + ) + definition_period = MONTH + defined_for = "co_ccap_child_in_care" + + def formula(person, period, parameters): + p = parameters(period).gov.states.co.ccap.parent_fee.part_time + year = period.this_year + daily_hours = person("co_ccap_care_hours_per_day", period) + # Zero is also the input default for unknown hours. + daily_units = where( + daily_hours > 0, + p.full_time_equivalent_units.calc(daily_hours, right=True), + 1, + ) + weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR + # Counties assess the fee on the authorized care schedule (3.124(V)), + # and attending days above it count the drop-in days a county may pay + # (3.130(F)). + weekly_days = person("childcare_days_per_week", year) + attending_days = person("childcare_attending_days_per_month", year) + days = max_(weekly_days * weeks_per_month, attending_days) + default_days = WEEKDAYS_PER_WEEK * weeks_per_month + return daily_units * where(days > 0, days, default_days) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py index 1aea57f8d1e..7efd8d8c896 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py @@ -1,6 +1,6 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( - co_ccap_parameter_instant, +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parent_fee_arithmetic import ( + drop_cents, ) @@ -8,41 +8,44 @@ class co_ccap_parent_fee(Variable): value_type = float entity = SPMUnit label = "Colorado Child Care Assistance Program parent fee" + documentation = ( + "One monthly fee for the household, in whole dollars: the base fee " + "plus the fee for additional children, at 55% when every child in " + "care receives part-time care, and 20% lower when a provider rates in " + "the top three quality levels. Each step drops the cents, as the " + "Department's parent fee tool does." + ) reference = ( - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=41", - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11042&fileName=8%20CCR%201403-1#page=62", + # PDF pages 79, 81-85 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=79", + "https://drive.google.com/file/d/1HtPiC2qxclzWfBa7LRo2Uohrg-RCBkyZ/view", ) unit = USD definition_period = MONTH + defined_for = StateCode.CO def formula(spm_unit, period, parameters): - instant_str = co_ccap_parameter_instant(period) - p = parameters(instant_str).gov.states.co.ccap - eligible_children = spm_unit("co_ccap_eligible_children", period) - base_parent_fee = spm_unit("co_ccap_base_parent_fee", period) - add_on_parent_fee = spm_unit("co_ccap_add_on_parent_fee", period) + p = parameters(period).gov.states.co.ccap + # Both come in whole dollars, which float32 stores exactly. + base_fee = spm_unit("co_ccap_base_parent_fee", period) + add_on_fee = spm_unit("co_ccap_add_on_parent_fee", period) + full_time_fee = np.asarray(base_fee + add_on_fee, dtype=np.float64) - # Childcare-hours-per-day also affects parent fee. - # Since each child may need different hours of childcare per day, we - # have to calculate parent fee one by one and sum them up. person = spm_unit.members - child_age_eligible = person("co_ccap_child_eligible", period) - childcare_hours_per_day = person("childcare_hours_per_day", period.this_year) - rate = p.parent_fee_rate_by_child_care_hours.calc( - childcare_hours_per_day, right=True + in_care = person("co_ccap_child_in_care", period) + part_time = person("co_ccap_child_in_part_time_care", period) + children_in_care = spm_unit("co_ccap_children_in_care", period) + all_part_time = (children_in_care > 0) & ( + spm_unit.sum(in_care & ~part_time) == 0 ) - childs_parent_fee = spm_unit.project(base_parent_fee + add_on_parent_fee) - unrounded_non_discounted_fee = spm_unit.sum( - child_age_eligible * childs_parent_fee * rate + fee = where( + all_part_time, + drop_cents(full_time_fee * p.parent_fee.part_time.rate), + full_time_fee, ) - non_discounted_fee = np.round(unrounded_non_discounted_fee, 2) - # Rating of child care facilities also affects parent fee. - # For households utilizing multiple child care providers, only one - # child care provider is required to be eligible for the reduced - # parent fee to apply. + # One provider in the top three quality levels is enough. rating = person("co_quality_rating_of_child_care_facility", period.this_year) - maximum_rating = spm_unit.max(rating * child_age_eligible) - discounted_rate = p.is_quality_rating_discounted.calc(maximum_rating) - unrounded = non_discounted_fee * discounted_rate - return np.round(unrounded, 2) + highest_rating = spm_unit.max(rating * in_care) + quality_factor = p.is_quality_rating_discounted.calc(highest_rating) + return where(children_in_care > 0, drop_cents(fee * quality_factor), 0) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee_arithmetic.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee_arithmetic.py new file mode 100644 index 00000000000..35ed465f502 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee_arithmetic.py @@ -0,0 +1,23 @@ +import numpy as np + +# Section 3.111(H) lists the guideline in cents and the model rounds monthly +# income to cents, so an exact fee has at most five decimal places. Float64 +# arithmetic on those amounts strays from the exact fee by about $0.000000000001. +# This tolerance brings a fee that should come to whole dollars up to them, and +# it can't reach the next possible fee, $0.00001 away. +WHOLE_DOLLAR_TOLERANCE = 1e-9 + + +def drop_cents(amount): + return np.floor(amount + WHOLE_DOLLAR_TOLERANCE) + + +def income_and_guideline_in_cents(spm_unit, period): + """Return monthly countable income, never below zero, and the guideline. + + Variables store float32, which can't hold most amounts in cents, so this + rounds each amount to cents in float64. + """ + income = np.asarray(spm_unit("co_ccap_countable_income", period), dtype=np.float64) + guideline = np.asarray(spm_unit("co_ccap_fpg", period), dtype=np.float64) + return np.round(np.maximum(income, 0), 2), np.round(guideline, 2) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 12772c7bdde..add787f3d73 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -1622,6 +1622,48 @@ candidates: note: 'PE-US follows the statute: the 2026 allowance is $1,153.31, from a $3,252.14 couple standard, twice the $1,626.07 individual rate (allowance/out_of_home_care.yaml), and the file''s comment lists the sources on each side. Verify the per-recipient calculation and document the inconsistent 2026 summary tables.' +- id: us-co:ccap-parent-fee-income-share + program: child-care-assistance + years: + from: 2021 + to: 2028 + type: calendar + question: Above the poverty guideline, does Colorado's parent fee start from 1% of the household's income, as 8 + CCR 1403-1 section 3.124(P)(2) says, or from 1% of the guideline, as one of CDEC's parent fee tools computes? + sources: + - kind: regulation + citation: 8 CCR 1403-1 section 3.124(P)(1) and (2) (ruleVersionId 12658) + url: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=83 + - kind: calculator + citation: CDEC Above 100% FPG Parent Fee Tool (English), Parent Fee Tool sheet, cell I9 + url: https://drive.google.com/file/d/1HtPiC2qxclzWfBa7LRo2Uohrg-RCBkyZ/view + - kind: calculator + citation: CDEC At 100% FPG Parent Fee Tool (English), Parent Fee Tool sheet, cell I9 + url: https://drive.google.com/file/d/1EEc3z8Iwu_KRTlBtd2NssDDEx_FITqVq/view + readings: + statute: Section 3.124(P)(2) charges a household above the guideline "one percent (1%) of their income plus a + marginal rate increase of fourteen percent (14%)" on each dollar above the guideline. CDEC's Above 100% FPG + tool computes (E4*0.01+((E4-I8)*0.14))/12, 1% of annual income E4 plus 14% above the guideline I8. + other: CDEC's At 100% FPG tool computes (I8*0.01+((E4-I8)*0.14))/12 above the guideline, 1% of the guideline + rather than of income. CDEC tells households above the guideline to use + the other tool, so this branch serves no one the tool is for. PE-US used the same formula before PR 10721. + example: A family of three with $40,000 a year and two children in full-time care in January 2026, under the + $26,650 guideline in section 3.111(H)(2), pays (0.01 x $40,000 + 0.14 x $13,350) / 12 + $15 = $204.08, or $204, + under section 3.124(P)(2). The At 100% FPG tool's formula gives (0.01 x $26,650 + 0.14 x $13,350) / 12 + $15 = + $192.96, or $192. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py + - policyengine_us/parameters/gov/states/co/ccap/parent_fee/income_rate.yaml + - policyengine_us/parameters/gov/states/co/ccap/parent_fee/marginal_rate.yaml + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10721 + state: ruled + note: PE-US follows section 3.124(P)(2) as written, 1% of all gross countable income plus 14% of income above the + guideline (income_rate.yaml, marginal_rate.yaml, co_ccap_base_parent_fee.py). The rule and CDEC's tool for + households above the guideline agree; only the At 100% FPG tool's branch for incomes above the guideline differs, + by 1% of the income above the guideline. Ask CDEC which formula counties apply. - id: us-co:claim-of-right-subtraction-practice program: income-tax years: