From 1fb05cc7cfbd68246a998643e86b9deaccf405fa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 14:55:16 -0500 Subject: [PATCH 1/6] Charge Colorado families one CCCAP parent fee under 8 CCR 1403-1 3.124 Compute one household fee from gross countable income: 1% of income plus 14% above the poverty guideline that 3.111(H)(2) and (3) set each October, $15 for each child in care after the first above it, in whole dollars, 55% when every child in care is in part-time care, and 20% off at a top-three provider. Drop the per-child hours multiplier, which gave a child with no recorded hours a fee of zero. Co-Authored-By: Claude Opus 5.5 --- changelog.d/co-ccap-parent-fee.fixed.md | 1 + .../co/ccap/is_quality_rating_discounted.yaml | 10 ++-- .../gov/states/co/ccap/parent_fee/add_on.yaml | 17 +++--- .../gov/states/co/ccap/parent_fee/base.yaml | 19 ------ .../co/ccap/parent_fee/income_rate.yaml | 11 ++++ .../co/ccap/parent_fee/marginal_rate.yaml | 30 ++++++++++ .../part_time/full_time_equivalent_units.yaml | 30 ++++++++++ .../co/ccap/parent_fee/part_time/rate.yaml | 9 +++ .../parent_fee/part_time/unit_threshold.yaml | 10 ++++ .../parent_fee_rate_by_child_care_hours.yaml | 26 -------- .../co/ccap/co_ccap_add_on_parent_fee.py | 27 ++++----- .../states/co/ccap/co_ccap_base_parent_fee.py | 32 ++++++---- .../states/co/ccap/co_ccap_child_in_care.py | 35 +++++++++++ .../ccap/co_ccap_child_in_part_time_care.py | 43 +++++++++++++ .../co/ccap/co_ccap_children_in_care.py | 16 +++++ .../gov/states/co/ccap/co_ccap_fpg.py | 30 ++++++++++ .../co_ccap_full_time_equivalent_units.py | 47 +++++++++++++++ .../gov/states/co/ccap/co_ccap_parent_fee.py | 60 +++++++++++-------- 18 files changed, 346 insertions(+), 107 deletions(-) create mode 100644 changelog.d/co-ccap-parent-fee.fixed.md delete mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee/base.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee/income_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee/marginal_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/full_time_equivalent_units.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/rate.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee/part_time/unit_threshold.yaml delete mode 100644 policyengine_us/parameters/gov/states/co/ccap/parent_fee_rate_by_child_care_hours.yaml create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_care.py create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_part_time_care.py create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_children_in_care.py create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_fpg.py create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_full_time_equivalent_units.py 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..6470b22c196 --- /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 is in 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 October 2028 the model applies the marginal rates of section 3.124(Q). 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..adb56a8192c --- /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 is in 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/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..42fab34d0a6 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 @@ -8,24 +8,23 @@ 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(instant_str).gov.states.co.ccap.parent_fee + income = spm_unit("co_ccap_countable_income", period) + fpg = spm_unit("co_ccap_fpg", 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..d6788640c43 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 @@ -8,23 +8,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, before the fee for " + "additional children, rounding, the part-time rate and the quality " + "discount. Through July 2028 it is 1% of income plus 14% of income " + "above the federal poverty guideline; from August 2028 it is a " + "marginal schedule 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(instant_str).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 = max_(spm_unit("co_ccap_countable_income", period), 0) + fpg = spm_unit("co_ccap_fpg", period) + income_to_fpg = np.zeros_like(income) + has_fpg = fpg > 0 + income_to_fpg[has_fpg] = income[has_fpg] / fpg[has_fpg] + return p.income_rate * income + p.marginal_rate.calc(income_to_fpg) * fpg 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..8b506f4c112 --- /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 survey data records 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..dbfe1295c33 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_in_part_time_care.py @@ -0,0 +1,43 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) + +# 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)). 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): + instant_str = co_ccap_parameter_instant(period) + p = parameters(instant_str).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("childcare_hours_per_day", year) + 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 + ) + return part_time_units | (in_grade_one_or_above & part_time_days) 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..5287a08d677 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_full_time_equivalent_units.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) + +# With no days of care reported, assume care on each weekday. This is an +# imputation, not a rule, so it is not a parameter. +WEEKDAYS_PER_WEEK = 5 + + +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, and a child with no reported days is assumed to " + "attend each weekday." + ) + 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): + instant_str = co_ccap_parameter_instant(period) + p = parameters(instant_str).gov.states.co.ccap.parent_fee.part_time + year = period.this_year + daily_hours = person("childcare_hours_per_day", year) + # 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 + monthly_days = person("childcare_attending_days_per_month", year) + weekly_days = person("childcare_days_per_week", year) + days = select( + [monthly_days > 0, weekly_days > 0], + [monthly_days, weekly_days * weeks_per_month], + default=WEEKDAYS_PER_WEEK * weeks_per_month, + ) + return daily_units * 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..92ee6fea164 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 @@ -3,46 +3,58 @@ co_ccap_parameter_instant, ) +# Fees drop their cents. Income reaches the fee in float32, so a fee that should +# come to whole dollars can fall a hair short of them; this tolerance keeps such +# a fee from losing a dollar. +WHOLE_DOLLAR_TOLERANCE = 0.001 + + +def drop_cents(amount): + return np.floor(amount + WHOLE_DOLLAR_TOLERANCE) + 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 is in 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) + 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 = drop_cents(base_fee + add_on_fee) - # 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) From 24c65d7678d82a481fc205e27477e3f25aa9835c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 15:25:07 -0500 Subject: [PATCH 2/6] Add hand-computed cases for Colorado's CCCAP parent fee Cases cover the guideline Colorado sets each October, the base fee on gross income, the $15 fee for each additional child above the guideline, which children count as in care, full-time-equivalent units, part-time care under sections 3.124(U) and (W), the quality discount, and the order in which the fee drops its cents. Co-Authored-By: Claude Opus 5.5 --- .../co/ccap/co_ccap_add_on_parent_fee.yaml | 114 ++-- .../co/ccap/co_ccap_base_parent_fee.yaml | 126 +++- .../states/co/ccap/co_ccap_child_in_care.yaml | 87 +++ .../ccap/co_ccap_child_in_part_time_care.yaml | 74 +++ .../gov/states/co/ccap/co_ccap_fpg.yaml | 116 ++++ .../co_ccap_full_time_equivalent_units.yaml | 69 +++ .../states/co/ccap/co_ccap_parent_fee.yaml | 540 +++++++++++------- .../gov/states/co/ccap/court_supervision.yaml | 20 +- 8 files changed, 852 insertions(+), 294 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_care.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_part_time_care.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_fpg.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_full_time_equivalent_units.yaml 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..776b2e4dec1 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, September 2028 still charges the fee. + period: 2028-09 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 October 2028 section 3.124(Q) has no fee for additional children. + period: 2028-10 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..3c836f25dbc 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,109 @@ -- 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 model reads the +# rates each October, so (Q) applies from October 2028. +- 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, September 2028 still uses section 3.124(P). + period: 2028-09 + 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, October 2028 uses the marginal rates of section 3.124(Q). + period: 2028-10 + 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-10 + 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 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..fb3471e5df0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_part_time_care.yaml @@ -0,0 +1,74 @@ +# 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. +- 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 + spm_units: + spm_unit: + members: [parent, short_days, many_short_days, grade_school, kindergarten, few_days, full_days, twelve_full_days, out_of_school] + households: + household: + members: [parent, short_days, many_short_days, grade_school, kindergarten, few_days, full_days, twelve_full_days, out_of_school] + 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. + co_ccap_child_in_part_time_care: [false, true, false, true, false, true, false, true, 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..6db9abe7d56 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_full_time_equivalent_units.yaml @@ -0,0 +1,69 @@ +# 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 a child with no reported days 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 + 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] + households: + household: + members: [parent, four_hours, eight_hours, unknown_hours, fourteen_hours, twenty_hours, five_hours, five_and_a_half_hours, both_day_counts] + 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: monthly days come first, 0.55 * 10 = 5.5. + co_ccap_full_time_equivalent_units: [0, 11.92, 21.67, 21.67, 15.5, 17.33, 11, 12, 5.5] 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..155a907c460 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,423 @@ -- 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 is in 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 + co_ccap_base_parent_fee: 189.08 + # (2 - 1) * 15 + co_ccap_add_on_parent_fee: 15 + # 189.08 + 15 = 204.08, which drops its cents to 204. + co_ccap_parent_fee: 204 -- 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 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: - parent1: - is_disabled: true - is_tax_unit_dependent: false - age: 45 - parent2: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + parent: + age: + 2026: 35 + employment_income: + 2026: 40_000 child1: - is_disabled: false + 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.33 + 147.93 + co_ccap_base_parent_fee: 181.27 + # 181.27 + 15 = 196.27, which drops its cents to 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: Case 3, with no recorded hours the children count as in full-time care. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + child1: + 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 - 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] + spm_unit_pre_subsidy_childcare_expenses: 12_000 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 + co_ccap_children_in_care: 2 + co_ccap_child_in_part_time_care: [false, false, false] + co_ccap_parent_fee: 204 -- 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 4, both children in part-time care pay 55% of the 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 - age: 17 childcare_hours_per_day: 4 - co_quality_rating_of_child_care_facility: 4 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, child1, child2] households: - household: - members: [parent1, parent2, child1, child2] - county_str: CROWLEY_COUNTY_CO + household: + members: [parent, child1, child2] 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 + # 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 one child, agi <= hhs_fpg, child eligible for cccap and 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: false - 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: 15 - co_quality_rating_of_child_care_facility: 5 + childcare_hours_per_day: 4 + child2: + age: 4 + 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_child_in_part_time_care: [false, true, false] + co_ccap_parent_fee: 204 -- 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 6, the part-time rate and then the quality discount, each dropping cents. + 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 - child1: - is_disabled: false + parent: + age: 30 + employment_income: 28_020 + child: + age: 3 is_tax_unit_dependent: true - age: 6 - childcare_hours_per_day: 20 + childcare_hours_per_day: 4 co_quality_rating_of_child_care_facility: 3 - child2: - is_disabled: false + 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 + co_ccap_base_parent_fee: 103.50 + # 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 - age: 17 childcare_hours_per_day: 4 - co_quality_rating_of_child_care_facility: 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: [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, 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_parent_fee: 0 -- 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 10, a family with no child in care pays no fee. + period: 2026-01 input: people: parent: - is_disabled: false - is_tax_unit_dependent: false - age: 45 + age: 35 + employment_income: 40_000 + child: + age: 4 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + co_ccap_children_in_care: 0 + co_ccap_parent_fee: 0 + +- name: Case 11, income below the guideline adds no fee for the second child. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 20_000 child1: - is_disabled: false + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + child2: + age: 5 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_add_on_parent_fee: 0 + # 0.01 * 1,666.67 = 16.67, so 16. + co_ccap_parent_fee: 16 + +- 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: + parent: + age: 40 + employment_income: 50_000 + child1: + age: 3 + is_tax_unit_dependent: true + child2: age: 6 - childcare_hours_per_day: 20 - co_quality_rating_of_child_care_facility: 1 + 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 + co_ccap_base_parent_fee: 249.92 + co_ccap_add_on_parent_fee: 15 + # 249.92 + 15 = 264.92, so 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: [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 + # 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 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..a88a1940836 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,18 @@ 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 + co_ccap_base_parent_fee: 128.25 + # 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; 128.25 drops its cents to 128. + 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 From 8cd27158529f85f2f29257e15efd172b1cecc9c2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 15:42:30 -0500 Subject: [PATCH 3/6] Test the CCCAP parent fee's invariants and keep fees within a dollar Property tests check that the fee is never negative and in whole dollars, that each child in care after the first adds $15 above the guideline and nothing else, that the 55% rate applies only when every child in care is part time, and that a provider in the top three quality levels cuts the fee by 20%. A differential test compares the model with an exact-decimal implementation of section 3.124 and CDEC's parent fee tool. The differential test found that the 0.001 whole-dollar tolerance overcharged $1 on whole-dollar annual incomes 1,199 more than a multiple of 1,200 below the guideline: $1,199 a year gives a base fee of $0.99917. A tolerance of 0.0005 still covers float32 noise (at most $0.00017 over 1,450 households) and floors those fees correctly. The source-conflict candidates record the reading of section 3.124(P)(2): 1% of income, as the rule and CDEC's tool for households above the guideline compute, not 1% of the guideline. Co-Authored-By: Claude Opus 5.5 --- .../test_co_ccap_parent_fee_properties.py | 317 ++++++++++++++++++ .../gov/states/co/ccap/co_ccap_parent_fee.py | 11 +- source_conflicts/candidates.yaml | 42 +++ 3 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 policyengine_us/tests/test_co_ccap_parent_fee_properties.py 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..fa3c1e58771 --- /dev/null +++ b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py @@ -0,0 +1,317 @@ +"""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: 1% of income plus 14% of +income above the federal poverty guideline ((P)(1) and (P)(2)), $15 for each +child in care after the first when income exceeds the guideline ((P)(3)), in +whole dollars ((A)), at 55% when every child in care is in 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, for every household: + +1. The fee is never negative, and it is a whole number of dollars. +2. With income and household size fixed, each child in care after the first + adds $15 to the fee when income exceeds the guideline and nothing when it + does not. The number of children in care changes the fee in no other way. +3. When every child in care is in part-time care, the fee is 55% of the + full-time fee with the cents dropped. When any child in care is in + full-time care, the fee is the full-time fee. +4. A provider in the top three quality levels, for any child in care, cuts the + fee by 20% with the cents dropped. A lower rating, or a rating for a child + outside care, changes nothing. + +A differential test checks the model against `reference_fee`, an independent +implementation of the rule and the Department's tool in exact decimal +arithmetic, with the guidelines that section 3.111(H)(2) and (H)(3) list. +""" + +from decimal import ROUND_FLOOR, Decimal + +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 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)), +} +# With no days of care reported, the model assumes care on each weekday. +ASSUMED_DAYS_PER_MONTH = Decimal(5 * 52) / 12 + + +def floor(amount): + return amount.to_integral_value(rounding=ROUND_FLOOR) + + +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 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.""" + hours = Decimal(str(child["hours"])) + days = Decimal(child["days"]) if child["days"] > 0 else ASSUMED_DAYS_PER_MONTH + in_grade_one_or_above = 6 <= child["age"] <= 17 + return daily_units(hours) * days < 13 or (in_grade_one_or_above and 0 < hours <= 5) + + +def guideline(month, children): + """The annual guideline for a parent and these children.""" + first, additional = GUIDELINES[month] + return first + additional * len(children) + + +def reference_fee(month, annual_income, children): + """Section 3.124 and the Department's tool, step by step.""" + in_care = [child for child in children if child["in_care"]] + if not in_care: + return Decimal(0) + income = max(Decimal(annual_income), Decimal(0)) + limit = guideline(month, children) + base = (income * Decimal("0.01") + max(income - limit, 0) * Decimal("0.14")) / 12 + 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 fees(month, households): + """The model's fee for each household, from one simulation. + + Each household holds one parent and the given children. A child in care + has the given schedule and a monthly charge; 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: 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["pre_subsidy_childcare_expenses"] = {year: MONTHLY_CHARGE * 12} + 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"} + simulation = Simulation(situation=situation) + return [float(fee) for fee in simulation.calculate("co_ccap_parent_fee", month)] + + +def child(age=3, in_care=True, hours=8, days=0, rating=0): + return dict(age=age, in_care=in_care, hours=hours, days=days, rating=rating) + + +months = st.sampled_from(sorted(GUIDELINES)) +incomes = st.integers(min_value=0, max_value=150_000) +# Hours straddle each band edge in section 3.130(C); 0 means unknown. +daily_hours = st.sampled_from([0, 1, 4.5, 5, 5.5, 8, 12, 12.5, 16, 17, 18]) +# Attending days per month; 0 means none reported. +monthly_days = st.integers(min_value=0, max_value=23) +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, + rating=st.integers(min_value=0, max_value=5), +) + + +@settings(max_examples=25, deadline=None) +# The Department's tool: $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.99917, which drops to $0. +@example(month="2026-01", income=1_199, children=[child()]) +# $21,500 for two gives a base fee of exactly $22. +@example(month="2026-01", income=21_500, children=[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)]) +# 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)]) +# 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)], +) +@given( + month=months, + income=incomes, + children=st.lists(any_children, min_size=1, max_size=4), +) +def test_the_fee_matches_an_independent_implementation(month, income, children): + (fee,) = fees(month, [{"income": income, "children": children}]) + expected = float(reference_fee(month, income, children)) + 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 = 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)]) +@given( + month=months, + income=incomes, + children=st.lists( + st.builds(child, age=child_ages, hours=daily_hours, days=monthly_days), + 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) 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_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py index 92ee6fea164..04d54188e11 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 @@ -3,10 +3,13 @@ co_ccap_parameter_instant, ) -# Fees drop their cents. Income reaches the fee in float32, so a fee that should -# come to whole dollars can fall a hair short of them; this tolerance keeps such -# a fee from losing a dollar. -WHOLE_DOLLAR_TOLERANCE = 0.001 +# Fees drop their cents. Income reaches the fee in float32, which leaves a fee +# that should come to whole dollars up to about $0.0002 short of them, so the +# floor adds this tolerance first. It stays below $1/1200 (about $0.00083): +# whole-dollar annual income can give a fee that much short of a whole dollar, +# such as $0.99917 on $1,199 a year, and that fee must still drop to the dollar +# below. +WHOLE_DOLLAR_TOLERANCE = 0.0005 def drop_cents(amount): diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 12772c7bdde..4e041c5ace7 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, a fee continuous at the guideline. 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: From caf8fbb21feb806f5476993f005ad6b837b10105 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 16:16:35 -0500 Subject: [PATCH 4/6] Count Colorado's days of care as the county-rate PR does The fee's full-time equivalent units now take days of care as the larger of days a week x 52 / 12 and attending days a month, and a five-day week when neither is reported, as co_ccap_care_days_per_month in #10723 does. Counties assess the fee on the authorized schedule (8 CCR 1403-1 3.124(V)), and attending days above it count drop-in days (3.130(F)). The property test's reference implementation reads days the same way and draws days a week as well as attending days. New cases cover a month with drop-in days and a weekly schedule above the attending days. Fee descriptions now say a child "receives" part-time care. Co-Authored-By: Claude Opus 5.5 --- changelog.d/co-ccap-parent-fee.fixed.md | 2 +- .../co/ccap/parent_fee/part_time/rate.yaml | 2 +- .../co_ccap_full_time_equivalent_units.yaml | 22 +++++--- .../states/co/ccap/co_ccap_parent_fee.yaml | 6 +-- .../test_co_ccap_parent_fee_properties.py | 53 +++++++++++++------ .../co_ccap_full_time_equivalent_units.py | 27 +++++----- .../gov/states/co/ccap/co_ccap_parent_fee.py | 2 +- 7 files changed, 73 insertions(+), 41 deletions(-) diff --git a/changelog.d/co-ccap-parent-fee.fixed.md b/changelog.d/co-ccap-parent-fee.fixed.md index 6470b22c196..8d10835cad1 100644 --- a/changelog.d/co-ccap-parent-fee.fixed.md +++ b/changelog.d/co-ccap-parent-fee.fixed.md @@ -1 +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 is in 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 October 2028 the model applies the marginal rates of section 3.124(Q). +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 October 2028 the model applies the marginal rates of section 3.124(Q). 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 index adb56a8192c..0a2aaa4dfb7 100644 --- 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 @@ -1,4 +1,4 @@ -description: Colorado charges this share of the parent fee when every child in care is in part-time care under the Child Care Assistance Program. +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: 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 index 6db9abe7d56..d7c6e71e7fe 100644 --- 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 @@ -2,8 +2,9 @@ # 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 a child with no reported days attends each -# weekday: 5 * 52 / 12 = 21.67 days a month. +# counts as one full-time unit. 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 @@ -49,12 +50,18 @@ 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 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] + 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] households: household: - members: [parent, four_hours, eight_hours, unknown_hours, fourteen_hours, twenty_hours, five_hours, five_and_a_half_hours, both_day_counts] + 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] state_code: CO output: # parent: not in care. @@ -65,5 +72,8 @@ # 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: monthly days come first, 0.55 * 10 = 5.5. - co_ccap_full_time_equivalent_units: [0, 11.92, 21.67, 21.67, 15.5, 17.33, 11, 12, 5.5] + # 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. + co_ccap_full_time_equivalent_units: [0, 11.92, 21.67, 21.67, 15.5, 17.33, 11, 12, 11.92, 12] 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 155a907c460..cadfa2069a1 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,9 +1,9 @@ # 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 is in 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: +# 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). diff --git a/policyengine_us/tests/test_co_ccap_parent_fee_properties.py b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py index fa3c1e58771..ad4f700cd4d 100644 --- a/policyengine_us/tests/test_co_ccap_parent_fee_properties.py +++ b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py @@ -4,19 +4,20 @@ fee from gross countable income and household size: 1% of income plus 14% of income above the federal poverty guideline ((P)(1) and (P)(2)), $15 for each child in care after the first when income exceeds the guideline ((P)(3)), in -whole dollars ((A)), at 55% when every child in care is in 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. +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, for every household: -1. The fee is never negative, and it is a whole number of dollars. -2. With income and household size fixed, each child in care after the first - adds $15 to the fee when income exceeds the guideline and nothing when it - does not. The number of children in care changes the fee in no other way. -3. When every child in care is in part-time care, the fee is 55% of the - full-time fee with the cents dropped. When any child in care is in - full-time care, the fee is the full-time fee. +1. The fee never falls below zero and always comes to whole dollars. +2. Until October 2028, with income and household size fixed, each child in + care after the first adds $15 to the fee when income exceeds the guideline + and nothing when it does not. The number of children in care changes the + fee in no other way. +3. When every child in care receives part-time care, the fee falls to 55% of + the full-time fee with the cents dropped. When any child in care receives + full-time care, the household pays the full-time fee. 4. A provider in the top three quality levels, for any child in care, cuts the fee by 20% with the cents dropped. A lower rating, or a rating for a child outside care, changes nothing. @@ -45,7 +46,7 @@ "2026-01": (Decimal(15_650), Decimal(5_500)), "2026-10": (Decimal(15_960), Decimal(5_680)), } -# With no days of care reported, the model assumes care on each weekday. +# Without a schedule, the model reads care on each weekday. ASSUMED_DAYS_PER_MONTH = Decimal(5 * 52) / 12 @@ -71,9 +72,11 @@ 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.""" + above, as in the model. Days of care come from the larger of days a week + x 52 / 12 and attending days a month, as in the model.""" hours = Decimal(str(child["hours"])) - days = Decimal(child["days"]) if child["days"] > 0 else ASSUMED_DAYS_PER_MONTH + 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 return daily_units(hours) * days < 13 or (in_grade_one_or_above and 0 < hours <= 5) @@ -128,6 +131,7 @@ def fees(month, households): 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"]} person["pre_subsidy_childcare_expenses"] = {year: MONTHLY_CHARGE * 12} situation["people"][name] = person members.append(name) @@ -139,8 +143,10 @@ def fees(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, rating=0): - return dict(age=age, in_care=in_care, hours=hours, days=days, rating=rating) +def child(age=3, in_care=True, hours=8, days=0, week=0, rating=0): + return dict( + age=age, in_care=in_care, hours=hours, days=days, week=week, rating=rating + ) months = st.sampled_from(sorted(GUIDELINES)) @@ -149,6 +155,8 @@ def child(age=3, in_care=True, hours=8, days=0, rating=0): daily_hours = st.sampled_from([0, 1, 4.5, 5, 5.5, 8, 12, 12.5, 16, 17, 18]) # 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, @@ -156,6 +164,7 @@ def child(age=3, in_care=True, hours=8, days=0, rating=0): in_care=st.booleans(), hours=daily_hours, days=monthly_days, + week=weekly_days, rating=st.integers(min_value=0, max_value=5), ) @@ -172,6 +181,10 @@ def child(age=3, in_care=True, hours=8, days=0, rating=0): @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)]) @@ -251,7 +264,13 @@ def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( month=months, income=incomes, children=st.lists( - st.builds(child, age=child_ages, hours=daily_hours, days=monthly_days), + st.builds( + child, + age=child_ages, + hours=daily_hours, + days=monthly_days, + week=weekly_days, + ), min_size=1, max_size=4, ), @@ -259,7 +278,7 @@ def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( 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) for child in children] + full_time = [dict(child, hours=8, days=0, week=0) for child in children] fee, full_time_fee = fees( month, [ 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 index 5287a08d677..9f377cdbaa5 100644 --- 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 @@ -3,8 +3,8 @@ co_ccap_parameter_instant, ) -# With no days of care reported, assume care on each weekday. This is an -# imputation, not a rule, so it is not a parameter. +# 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 @@ -15,11 +15,14 @@ class co_ccap_full_time_equivalent_units(Variable): 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, and a child with no reported days is assumed to " - "attend each weekday." + "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 = ( - # PDF pages 10, 85, 119 + # 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 @@ -37,11 +40,11 @@ def formula(person, period, parameters): 1, ) weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR - monthly_days = person("childcare_attending_days_per_month", 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) - days = select( - [monthly_days > 0, weekly_days > 0], - [monthly_days, weekly_days * weeks_per_month], - default=WEEKDAYS_PER_WEEK * weeks_per_month, - ) - return daily_units * days + 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 04d54188e11..32d13052334 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 @@ -23,7 +23,7 @@ class co_ccap_parent_fee(Variable): 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 is in part-time care, and 20% lower when a provider rates 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." ) From 26d55ce93f6118acf331afe6df7c7ad442e738ce Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 05:00:21 -0500 Subject: [PATCH 5/6] Answer the caf8fbb21f review of Colorado's parent fee Read the fee parameters at the month, so 3.124(Q) starts in August 2028 as the rule says. Spread weekly hours over the days of care when a family reports no daily hours, and read after-school care for a child in grade 1 or above as part time. Round income and the guideline to cents and floor in float64, so $23,999.64 a year gives $19 and an exact $18 fee stays $18. Narrow invariants 2 and 3 to the households they hold for. Co-Authored-By: Claude Opus 5.5 --- changelog.d/co-ccap-parent-fee.fixed.md | 2 +- changelog.d/co-ccap-parent-fee.removed.md | 1 + .../co/ccap/co_ccap_add_on_parent_fee.yaml | 8 +- .../co/ccap/co_ccap_base_parent_fee.yaml | 50 +++- .../co/ccap/co_ccap_care_hours_per_day.yaml | 72 ++++++ .../ccap/co_ccap_child_in_part_time_care.yaml | 55 ++++- .../co_ccap_full_time_equivalent_units.yaml | 25 +- .../states/co/ccap/co_ccap_parent_fee.yaml | 179 +++++++++++++- .../gov/states/co/ccap/court_supervision.yaml | 7 +- .../test_co_ccap_parent_fee_properties.py | 223 ++++++++++++++---- .../co/ccap/co_ccap_add_on_parent_fee.py | 10 +- .../states/co/ccap/co_ccap_base_parent_fee.py | 25 +- .../co/ccap/co_ccap_care_hours_per_day.py | 36 +++ .../states/co/ccap/co_ccap_child_in_care.py | 4 +- .../ccap/co_ccap_child_in_part_time_care.py | 32 ++- .../co_ccap_full_time_equivalent_units.py | 13 +- .../gov/states/co/ccap/co_ccap_parent_fee.py | 22 +- .../co/ccap/co_ccap_parent_fee_arithmetic.py | 23 ++ source_conflicts/candidates.yaml | 2 +- 19 files changed, 648 insertions(+), 141 deletions(-) create mode 100644 changelog.d/co-ccap-parent-fee.removed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_care_hours_per_day.yaml create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_care_hours_per_day.py create mode 100644 policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee_arithmetic.py diff --git a/changelog.d/co-ccap-parent-fee.fixed.md b/changelog.d/co-ccap-parent-fee.fixed.md index 8d10835cad1..d5326139f60 100644 --- a/changelog.d/co-ccap-parent-fee.fixed.md +++ b/changelog.d/co-ccap-parent-fee.fixed.md @@ -1 +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 October 2028 the model applies the marginal rates of section 3.124(Q). +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/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 776b2e4dec1..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 @@ -46,8 +46,8 @@ output: co_ccap_add_on_parent_fee: 0 -- name: Case 5, September 2028 still charges the fee. - period: 2028-09 +- name: Case 5, July 2028 still charges the fee. + period: 2028-07 input: state_code: 2028: CO @@ -57,8 +57,8 @@ output: co_ccap_add_on_parent_fee: 30 -- name: Case 6, from October 2028 section 3.124(Q) has no fee for additional children. - period: 2028-10 +- name: Case 6, from August 2028 section 3.124(Q) has no fee for additional children. + period: 2028-08 input: state_code: 2028: CO 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 3c836f25dbc..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,8 +1,8 @@ # 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 model reads the -# rates each October, so (Q) applies from October 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 @@ -59,8 +59,8 @@ output: co_ccap_base_parent_fee: 0 -- name: Case 6, September 2028 still uses section 3.124(P). - period: 2028-09 +- name: Case 6, July 2028 still uses section 3.124(P). + period: 2028-07 absolute_error_margin: 0.01 input: state_code: @@ -71,8 +71,8 @@ # 0.01 * 3,500 + 0.14 * 1,500 = 35 + 210 = 245 co_ccap_base_parent_fee: 245 -- name: Case 7, October 2028 uses the marginal rates of section 3.124(Q). - period: 2028-10 +- 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: @@ -85,7 +85,7 @@ co_ccap_base_parent_fee: 89 - name: Case 8, under section 3.124(Q), income between 100% and 130% of the guideline. - period: 2028-10 + period: 2028-09 absolute_error_margin: 0.01 input: state_code: @@ -107,3 +107,39 @@ 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..afeb859408a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_care_hours_per_day.yaml @@ -0,0 +1,72 @@ +# 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: days a week if reported, else +# 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 + spm_units: + spm_unit: + members: [parent, daily_reported, weekly_over_two_days, weekly_only, five_hour_days, attending_days, both_day_counts] + households: + household: + members: [parent, daily_reported, weekly_over_two_days, weekly_only, five_hour_days, attending_days, both_day_counts] + 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: days a week win, 20 / 4 = 5. + co_ccap_care_hours_per_day: [0, 4, 10, 4, 5, 5, 5] + +- 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_part_time_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_in_part_time_care.yaml index fb3471e5df0..20d46fc9c05 100644 --- 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 @@ -2,7 +2,8 @@ # 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. +# 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: @@ -55,12 +56,50 @@ 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] + 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] + 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. @@ -71,4 +110,12 @@ # full_days: 21.67 units. # twelve_full_days: 12 units. # out_of_school: 13.2 units, and the child attends no school. - co_ccap_child_in_part_time_care: [false, true, false, true, false, true, false, true, false] + # 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] 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 index d7c6e71e7fe..8349477ca95 100644 --- 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 @@ -2,7 +2,8 @@ # 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. Days of care come from the larger of days a +# 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. @@ -56,12 +57,25 @@ 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] + 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] + 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. @@ -76,4 +90,7 @@ # 0.55 * 21.67 = 11.92. # drop_in_days: 12 attending days beat a two-day week (8.67 days), # 1 * 12 = 12. - co_ccap_full_time_equivalent_units: [0, 11.92, 21.67, 21.67, 15.5, 17.33, 11, 12, 11.92, 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] 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 cadfa2069a1..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 @@ -35,11 +35,12 @@ 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 - co_ccap_base_parent_fee: 189.08 + # 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.08 + 15 = 204.08, which drops its cents to 204. + # 189 + 15 = 204. co_ccap_parent_fee: 204 - name: Case 2, the same family in October 2026, matching CDEC's parent fee tool. @@ -79,10 +80,11 @@ 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.33 + 147.93 - co_ccap_base_parent_fee: 181.27 - # 181.27 + 15 = 196.27, which drops its cents to 196. The tool, with a - # family of 3, $40,000 a year and 2 children, gives the same full-time fee. + # 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: Case 3, with no recorded hours the children count as in full-time care. @@ -187,8 +189,8 @@ 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 - co_ccap_base_parent_fee: 103.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. @@ -340,10 +342,10 @@ 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 - co_ccap_base_parent_fee: 249.92 + # 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.92 + 15 = 264.92, so 264. + # 249 + 15 = 264. co_ccap_parent_fee: 264 - name: Case 13, one provider in the top three quality levels is enough. @@ -421,3 +423,156 @@ 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: [parent, child] + state_code: CO + output: + co_ccap_child_in_part_time_care: [false, true] + # 253 * 0.55 = 139.15, so 139. + co_ccap_parent_fee: 139 + +- name: Case 18, a fee just short of a whole dollar drops to the dollar below. + period: 2026-01 + input: + people: + parent: + age: 35 + employment_income: 23_999.64 + child1: + age: 3 + is_tax_unit_dependent: true + childcare_hours_per_day: 8 + child2: + age: 5 + 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: + # 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 a88a1940836..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 @@ -93,12 +93,13 @@ # 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 - co_ccap_base_parent_fee: 128.25 + # 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 a day is full-time care, and a facility rating of 0 gives no - # discount; 128.25 drops its cents to 128. + # discount. co_ccap_parent_fee: 128 # Expenses 12,000 / 12 = 1,000 - 128 = 872 co_ccap_subsidy: 872 diff --git a/policyengine_us/tests/test_co_ccap_parent_fee_properties.py b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py index ad4f700cd4d..3a89c40c133 100644 --- a/policyengine_us/tests/test_co_ccap_parent_fee_properties.py +++ b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py @@ -1,33 +1,37 @@ """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: 1% of income plus 14% of -income above the federal poverty guideline ((P)(1) and (P)(2)), $15 for each -child in care after the first when income exceeds 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)). The Department's parent fee tool drops the cents after each step. +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, for every household: +Invariants, which these tests check on generated households: 1. The fee never falls below zero and always comes to whole dollars. -2. Until October 2028, with income and household size fixed, each child in - care after the first adds $15 to the fee when income exceeds the guideline - and nothing when it does not. The number of children in care changes the - fee in no other way. -3. When every child in care receives part-time care, the fee falls to 55% of - the full-time fee with the cents dropped. When any child in care receives - full-time care, the household pays the full-time fee. +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% with the cents dropped. A lower rating, or a rating for a child - outside care, changes nothing. + fee by 20% without cents. A lower rating, or a rating for a child outside + care, changes nothing. A differential test checks the model against `reference_fee`, an independent implementation of the rule and the Department's tool in exact decimal -arithmetic, with the guidelines that section 3.111(H)(2) and (H)(3) list. +arithmetic, on incomes in whole dollars and in cents. It takes the guidelines +that section 3.111(H)(2) and (H)(3) list, and reads the model's projected +guideline for 2028, which Colorado has not yet published. """ -from decimal import ROUND_FLOOR, Decimal +from decimal import ROUND_FLOOR, ROUND_HALF_EVEN, Decimal import pytest from hypothesis import example, given, settings @@ -36,6 +40,7 @@ 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 @@ -46,14 +51,25 @@ "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_MONTH = Decimal(5 * 52) / 12 +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: @@ -68,34 +84,67 @@ def daily_units(hours): 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"])) + if child["week"]: + days_in_a_week = Decimal(child["week"]) + elif child["days"]: + days_in_a_week = Decimal(child["days"]) * 12 / 52 + else: + days_in_a_week = 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. Days of care come from the larger of days a week - x 52 / 12 and attending days a month, as in the model.""" - hours = Decimal(str(child["hours"])) + 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 - return daily_units(hours) * days < 13 or (in_grade_one_or_above and 0 < hours <= 5) + 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 annual guideline for a parent and these children.""" + """The monthly guideline in cents for a parent and these children.""" first, additional = GUIDELINES[month] - return first + additional * len(children) + 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): +def reference_fee(month, annual_income, children, limit): """Section 3.124 and the Department's tool, step by step.""" in_care = [child for child in children if child["in_care"]] if not in_care: return Decimal(0) - income = max(Decimal(annual_income), Decimal(0)) - limit = guideline(month, children) - base = (income * Decimal("0.01") + max(income - limit, 0) * Decimal("0.14")) / 12 - add_on = 15 * (len(in_care) - 1) if income > limit else 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")) @@ -104,12 +153,12 @@ def reference_fee(month, annual_income, children): return fee -def fees(month, households): - """The model's fee for each household, from one simulation. +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; a child outside care has - neither.""" + 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}} @@ -118,7 +167,7 @@ def fees(month, households): members = [parent] situation["people"][parent] = { "age": {year: 35}, - "employment_income": {year: household["income"]}, + "employment_income": {year: float(household["income"])}, "self_employment_income": {year: household.get("loss", 0)}, } for j, child in enumerate(household["children"]): @@ -132,27 +181,55 @@ def fees(month, households): 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"]} - person["pre_subsidy_childcare_expenses"] = {year: MONTHLY_CHARGE * 12} + 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"} - simulation = Simulation(situation=situation) + 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): +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 + 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, 1, 4.5, 5, 5.5, 8, 12, 12.5, 16, 17, 18]) +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. @@ -166,17 +243,28 @@ def child(age=3, in_care=True, hours=8, days=0, week=0, rating=0): 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: $40,000, a family of 3 and 2 children, full time. +# 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.99917, which drops to $0. +# $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 of exactly $22. +# $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. @@ -191,20 +279,51 @@ def child(age=3, in_care=True, hours=8, days=0, week=0, rating=0): @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)]) +# 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, - income=incomes, + 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): - (fee,) = fees(month, [{"income": income, "children": children}]) - expected = float(reference_fee(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) @@ -249,7 +368,7 @@ def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( for count in range(len(ages) + 1) ] result = fees(month, households) - above = income > guideline(month, ages) + 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 @@ -260,6 +379,7 @@ def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( @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, @@ -270,6 +390,8 @@ def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( hours=daily_hours, days=monthly_days, week=weekly_days, + weekly=weekly_hours, + after_school=st.booleans(), ), min_size=1, max_size=4, @@ -278,7 +400,10 @@ def test_each_additional_child_in_care_adds_15_dollars_above_the_guideline( 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) for child in 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, [ 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 42fab34d0a6..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, ) @@ -21,10 +21,8 @@ class co_ccap_add_on_parent_fee(Variable): 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.parent_fee - income = spm_unit("co_ccap_countable_income", period) - fpg = spm_unit("co_ccap_fpg", period) + 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 d6788640c43..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, ) @@ -9,11 +10,11 @@ class co_ccap_base_parent_fee(Variable): entity = SPMUnit label = "Colorado Child Care Assistance Program base parent fee" documentation = ( - "The parent fee on gross countable income, before the fee for " - "additional children, rounding, the part-time rate and the quality " - "discount. Through July 2028 it is 1% of income plus 14% of income " - "above the federal poverty guideline; from August 2028 it is a " - "marginal schedule of 1%, 3%, 5% and 7%." + "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 = ( # PDF pages 82-84 @@ -24,13 +25,13 @@ class co_ccap_base_parent_fee(Variable): 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.parent_fee + 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 = max_(spm_unit("co_ccap_countable_income", period), 0) - fpg = spm_unit("co_ccap_fpg", period) + 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 p.income_rate * income + p.marginal_rate.calc(income_to_fpg) * 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..bb13575f18a --- /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) + days_in_a_week = select( + [days_per_week > 0, attending_days > 0], + [days_per_week, attending_days * MONTHS_IN_YEAR / WEEKS_IN_YEAR], + default=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 index 8b506f4c112..369e4fc6f31 100644 --- 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 @@ -8,8 +8,8 @@ class co_ccap_child_in_care(Variable): 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 survey data records what families pay for care but " - "not when the care happens." + "expense, since surveys ask what families pay for care but not when " + "the care happens." ) reference = ( # PDF pages 79, 84 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 index dbfe1295c33..29f9493b6ee 100644 --- 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 @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( - co_ccap_parameter_instant, -) # 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 @@ -16,9 +13,11 @@ class co_ccap_child_in_part_time_care(Variable): 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)). Care schedules do not " - "change within a year here, so the majority-of-months rule in section " - "3.124(V) gives the same answer." + "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 @@ -28,16 +27,29 @@ class co_ccap_child_in_part_time_care(Variable): defined_for = "co_ccap_child_in_care" def formula(person, period, parameters): - instant_str = co_ccap_parameter_instant(period) - p = parameters(instant_str).gov.states.co.ccap.parent_fee.part_time + 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("childcare_hours_per_day", 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 ) - return part_time_units | (in_grade_one_or_above & part_time_days) + # 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_full_time_equivalent_units.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_full_time_equivalent_units.py index 9f377cdbaa5..4262b3e4fa0 100644 --- 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 @@ -1,12 +1,8 @@ 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_care_hours_per_day import ( + WEEKDAYS_PER_WEEK, ) -# 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_full_time_equivalent_units(Variable): value_type = float @@ -29,10 +25,9 @@ class co_ccap_full_time_equivalent_units(Variable): defined_for = "co_ccap_child_in_care" def formula(person, period, parameters): - instant_str = co_ccap_parameter_instant(period) - p = parameters(instant_str).gov.states.co.ccap.parent_fee.part_time + p = parameters(period).gov.states.co.ccap.parent_fee.part_time year = period.this_year - daily_hours = person("childcare_hours_per_day", 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, 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 32d13052334..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,20 +1,8 @@ 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, ) -# Fees drop their cents. Income reaches the fee in float32, which leaves a fee -# that should come to whole dollars up to about $0.0002 short of them, so the -# floor adds this tolerance first. It stays below $1/1200 (about $0.00083): -# whole-dollar annual income can give a fee that much short of a whole dollar, -# such as $0.99917 on $1,199 a year, and that fee must still drop to the dollar -# below. -WHOLE_DOLLAR_TOLERANCE = 0.0005 - - -def drop_cents(amount): - return np.floor(amount + WHOLE_DOLLAR_TOLERANCE) - class co_ccap_parent_fee(Variable): value_type = float @@ -37,11 +25,11 @@ class co_ccap_parent_fee(Variable): 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 + 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 = drop_cents(base_fee + add_on_fee) + full_time_fee = np.asarray(base_fee + add_on_fee, dtype=np.float64) person = spm_unit.members in_care = person("co_ccap_child_in_care", period) 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..ca2da89805a --- /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 counties work from monthly +# income in 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 4e041c5ace7..add787f3d73 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -1645,7 +1645,7 @@ candidates: 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, a fee continuous at the guideline. CDEC tells households above the guideline to use + 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, From af9d7b7bfa9c6dcee94d0d96eef92413fc7a7bf7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 06:52:08 -0500 Subject: [PATCH 6/6] Answer the 26d55ce93f review of Colorado's parent fee Weekly hours now spread over the same larger day count that units of care use. YAML cases pin each side of the 12-hour and 17-hour edges and the after-school rule's day checks. The arithmetic comment and the test docstring state what the model and the reference do. Co-Authored-By: Claude Opus 5.5 --- .../co/ccap/co_ccap_care_hours_per_day.yaml | 17 +++-- .../ccap/co_ccap_child_in_part_time_care.yaml | 29 ++++++++ .../co_ccap_full_time_equivalent_units.yaml | 67 +++++++++++++++++++ .../test_co_ccap_parent_fee_properties.py | 26 +++---- .../co/ccap/co_ccap_care_hours_per_day.py | 8 +-- .../co/ccap/co_ccap_parent_fee_arithmetic.py | 4 +- 6 files changed, 128 insertions(+), 23 deletions(-) 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 index afeb859408a..53ed32d698a 100644 --- 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 @@ -1,6 +1,6 @@ # 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: days a week if reported, else +# 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 @@ -32,12 +32,17 @@ 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] + 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] + 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. @@ -46,8 +51,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: days a week win, 20 / 4 = 5. - co_ccap_care_hours_per_day: [0, 4, 10, 4, 5, 5, 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 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 index 20d46fc9c05..2cee829404d 100644 --- 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 @@ -119,3 +119,32 @@ # 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_full_time_equivalent_units.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_full_time_equivalent_units.yaml index 8349477ca95..110f69db0aa 100644 --- 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 @@ -94,3 +94,70 @@ # 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/test_co_ccap_parent_fee_properties.py b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py index 3a89c40c133..735e1e70d75 100644 --- a/policyengine_us/tests/test_co_ccap_parent_fee_properties.py +++ b/policyengine_us/tests/test_co_ccap_parent_fee_properties.py @@ -24,11 +24,10 @@ fee by 20% without cents. A lower rating, or a rating for a child outside care, changes nothing. -A differential test checks the model against `reference_fee`, an independent -implementation of the rule and the Department's tool in exact decimal -arithmetic, on incomes in whole dollars and in cents. It takes the guidelines -that section 3.111(H)(2) and (H)(3) list, and reads the model's projected -guideline for 2028, which Colorado has not yet published. +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 @@ -88,12 +87,8 @@ 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"])) - if child["week"]: - days_in_a_week = Decimal(child["week"]) - elif child["days"]: - days_in_a_week = Decimal(child["days"]) * 12 / 52 - else: - days_in_a_week = ASSUMED_DAYS_PER_WEEK + 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 @@ -134,7 +129,7 @@ def marginal_fee(income, limit): def reference_fee(month, annual_income, children, limit): - """Section 3.124 and the Department's tool, step by step.""" + """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) @@ -296,6 +291,13 @@ def child( @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( 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 index bb13575f18a..52d6558007e 100644 --- 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 @@ -28,9 +28,9 @@ def formula(person, period, parameters): 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) - days_in_a_week = select( - [days_per_week > 0, attending_days > 0], - [days_per_week, attending_days * MONTHS_IN_YEAR / WEEKS_IN_YEAR], - default=WEEKDAYS_PER_WEEK, + # 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_parent_fee_arithmetic.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee_arithmetic.py index ca2da89805a..35ed465f502 100644 --- 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 @@ -1,7 +1,7 @@ import numpy as np -# Section 3.111(H) lists the guideline in cents and counties work from monthly -# income in cents, so an exact fee has at most five decimal places. Float64 +# 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.