From 2eeea71ed32075b474f09bce95b858020f903550 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 18:22:36 -0500 Subject: [PATCH] Pay Colorado's child care assistance only for eligible children 8 CCR 1403-1 3.132(D) and (E) pay for the care of children under 13, and of children under 19 who can't care for themselves or who are under court supervision. co_ccap_subsidy summed every SPM unit member's child care and after-school charges, a teenager's among them; it now sums the charges of members who pass co_ccap_child_eligible, as co_ccap_parent_fee already does. Adds YAML cases at the age boundaries, two Hypothesis properties and a towncrier fragment. Co-Authored-By: Claude Opus 5.5 --- .../co-ccap-eligible-children.fixed.md | 1 + .../gov/states/co/ccap/co_ccap_subsidy.yaml | 225 +++++++++++++++++- ...st_co_ccap_eligible_children_properties.py | 174 ++++++++++++++ .../gov/states/co/ccap/co_ccap_subsidy.py | 19 +- 4 files changed, 406 insertions(+), 13 deletions(-) create mode 100644 changelog.d/co-ccap-eligible-children.fixed.md create mode 100644 policyengine_us/tests/test_co_ccap_eligible_children_properties.py diff --git a/changelog.d/co-ccap-eligible-children.fixed.md b/changelog.d/co-ccap-eligible-children.fixed.md new file mode 100644 index 00000000000..2b63661d787 --- /dev/null +++ b/changelog.d/co-ccap-eligible-children.fixed.md @@ -0,0 +1 @@ +Pay Colorado's Child Care Assistance Program only for eligible children: those under 13, and those under 19 who can't care for themselves or who are under court supervision. It had paid every SPM unit member's child care and after-school charges, a teenager's among them. diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_subsidy.yaml index ff9daf2d40f..a3531bc5989 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_subsidy.yaml @@ -1,21 +1,224 @@ -- name: In 2023, the total expenses is 2_400, and parent fee is 800, the subsidy would be 1_400. +- name: Case 1, in 2023, a child's $2,200 of charges less an $800 parent fee leaves $1,400. period: 2023 input: - pre_subsidy_childcare_expenses: 1_000 - after_school_expenses: 1_200 - co_ccap_parent_fee: 800 - co_ccap_eligible: true + people: + parent: + age: 30 + child: + age: 4 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 1_000 + after_school_expenses: 1_200 + spm_units: + spm_unit: + members: [parent, child] + co_ccap_parent_fee: 800 + co_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: CO output: co_ccap_subsidy: 1_400 -- name: In 2022, the total annual expenses is 28_800, and parent fee for December is 800, the subsidy for December would be 1_400. +- name: Case 2, in December 2022, a child's $2,200 of monthly charges less an $800 parent fee leaves $1,400. period: 2022 input: - pre_subsidy_childcare_expenses: 12 * 1_000 - after_school_expenses: 12 * 1_200 - co_ccap_parent_fee: - 2022-12: 800 - co_ccap_eligible: true + people: + parent: + age: 30 + child: + age: 4 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12 * 1_000 + after_school_expenses: 12 * 1_200 + spm_units: + spm_unit: + members: [parent, child] + co_ccap_parent_fee: + 2022-12: 800 + co_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: CO output: co_ccap_subsidy: 2022-12: 1_400 + +- name: Case 3, a teenager without additional care needs adds nothing to the payment. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + young_child: + age: 3 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + teen: + age: 15 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + spm_units: + spm_unit: + members: [parent, young_child, teen] + co_ccap_parent_fee: 200 + co_ccap_eligible: true + households: + household: + members: [parent, young_child, teen] + state_code: CO + output: + co_ccap_child_eligible: [false, true, false] + # 8 CCR 1403-1 3.132(D) pays for children under 13: the 3-year-old's + # $1,000 a month less the $200 fee, and nothing toward the 15-year-old's + # $1,000. + co_ccap_subsidy: 800 + +- name: Case 4, a teenager who can't care for themselves is an eligible child. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + young_child: + age: 3 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + teen: + age: 15 + is_tax_unit_dependent: true + is_disabled: true + pre_subsidy_childcare_expenses: 12_000 + spm_units: + spm_unit: + members: [parent, young_child, teen] + co_ccap_parent_fee: 200 + co_ccap_eligible: true + households: + household: + members: [parent, young_child, teen] + state_code: CO + output: + co_ccap_child_eligible: [false, true, true] + # 3.132(E) covers a child under 19 who can't care for themselves: + # $1,000 + $1,000 - $200. + co_ccap_subsidy: 1_800 + +- name: Case 5, a teenager under court supervision is an eligible child. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + young_child: + age: 3 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + teen: + age: 15 + is_tax_unit_dependent: true + is_under_court_supervision: true + pre_subsidy_childcare_expenses: 12_000 + spm_units: + spm_unit: + members: [parent, young_child, teen] + co_ccap_parent_fee: 200 + co_ccap_eligible: true + households: + household: + members: [parent, young_child, teen] + state_code: CO + output: + co_ccap_child_eligible: [false, true, true] + co_ccap_subsidy: 1_800 + +- name: Case 6, a 13-year-old's after-school care adds nothing to the payment. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + young_child: + age: 4 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + older_child: + age: 13 + is_tax_unit_dependent: true + after_school_expenses: 6_000 + spm_units: + spm_unit: + members: [parent, young_child, older_child] + co_ccap_parent_fee: 200 + co_ccap_eligible: true + households: + household: + members: [parent, young_child, older_child] + state_code: CO + output: + co_ccap_child_eligible: [false, true, false] + # $1,000 - $200; the 13-year-old's $500 a month doesn't count. + co_ccap_subsidy: 800 + +- name: Case 7, a 12-year-old's after-school care counts. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + young_child: + age: 4 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + older_child: + age: 12 + is_tax_unit_dependent: true + after_school_expenses: 6_000 + spm_units: + spm_unit: + members: [parent, young_child, older_child] + co_ccap_parent_fee: 200 + co_ccap_eligible: true + households: + household: + members: [parent, young_child, older_child] + state_code: CO + output: + co_ccap_child_eligible: [false, true, true] + # $1,000 + $500 - $200. + co_ccap_subsidy: 1_300 + +- name: Case 8, a teenager's charge doesn't absorb a fee above the eligible child's charge. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + young_child: + age: 3 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 1_200 + teen: + age: 15 + is_tax_unit_dependent: true + pre_subsidy_childcare_expenses: 12_000 + spm_units: + spm_unit: + members: [parent, young_child, teen] + co_ccap_parent_fee: 200 + co_ccap_eligible: true + households: + household: + members: [parent, young_child, teen] + state_code: CO + output: + # The 3-year-old's $100 a month falls short of the $200 fee. + co_ccap_subsidy: 0 diff --git a/policyengine_us/tests/test_co_ccap_eligible_children_properties.py b/policyengine_us/tests/test_co_ccap_eligible_children_properties.py new file mode 100644 index 00000000000..52257546ef5 --- /dev/null +++ b/policyengine_us/tests/test_co_ccap_eligible_children_properties.py @@ -0,0 +1,174 @@ +"""Colorado's Child Care Assistance Program pays only for eligible children. + +8 CCR 1403-1 3.132(D) and (E) pay for the care of children under 13, and of +children under 19 who can't care for themselves or who are under court +supervision (3.103(UU) and 3.111(F) define the eligible child the same way). + +1. A member outside those limits, such as a 15-year-old without additional + care needs or a 19-year-old, adds nothing to the payment, whatever that + member's charges, the eligible child's charges or the parent fee. +2. The payment equals the eligible children's child care and after-school + charges less the parent fee, and never falls below zero. The model has no + county reimbursement rate yet (3.125 and 3.130), so nothing caps a charge; + once a rate caps it, this becomes an upper bound. + +The tests set eligibility and the parent fee as inputs, so they exercise the +payment formula alone. +""" + +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +MONTH = f"{YEAR}-01" +TOLERANCE = 0.01 +AGE_LIMIT = 13 +ADDITIONAL_NEEDS_AGE_LIMIT = 19 + + +def colorado(children, monthly_fee): + """January payment for an SPM unit of one parent and these children.""" + members = ["parent", *children] + people = {"parent": {"age": {YEAR: 30}}} + for name, child in children.items(): + people[name] = { + "age": {YEAR: child["age"]}, + "is_tax_unit_dependent": {YEAR: True}, + "is_disabled": {YEAR: child["disabled"]}, + "is_under_court_supervision": {YEAR: child["court"]}, + "pre_subsidy_childcare_expenses": {YEAR: child["care"] * 12}, + "after_school_expenses": {YEAR: child["after_school"] * 12}, + } + situation = { + "people": people, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": { + "spm_unit": { + "members": members, + "co_ccap_eligible": {MONTH: True}, + "co_ccap_parent_fee": {MONTH: monthly_fee}, + } + }, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": ["parent"]}}, + "households": {"household": {"members": members, "state_name": {YEAR: "CO"}}}, + } + simulation = Simulation(situation=situation) + return float(simulation.calculate("co_ccap_subsidy", MONTH)[0]) + + +def eligible(child): + """3.132(D) and (E): under 13, or under 19 with additional care needs.""" + return child["age"] < AGE_LIMIT or ( + (child["disabled"] or child["court"]) + and child["age"] < ADDITIONAL_NEEDS_AGE_LIMIT + ) + + +charges = dict( + care=st.integers(min_value=0, max_value=2_000), + after_school=st.integers(min_value=0, max_value=800), +) +needs = st.sampled_from([(True, False), (False, True), (True, True)]) +young_children = st.fixed_dictionaries( + dict( + age=st.integers(min_value=0, max_value=AGE_LIMIT - 1), + disabled=st.booleans(), + court=st.booleans(), + **charges, + ) +) +children_with_needs = st.builds( + lambda age, need, care, after_school: dict( + age=age, + disabled=need[0], + court=need[1], + care=care, + after_school=after_school, + ), + age=st.integers(min_value=AGE_LIMIT, max_value=ADDITIONAL_NEEDS_AGE_LIMIT - 1), + need=needs, + **charges, +) +eligible_children = st.one_of(young_children, children_with_needs) +teenagers = st.fixed_dictionaries( + dict( + age=st.integers(min_value=AGE_LIMIT, max_value=ADDITIONAL_NEEDS_AGE_LIMIT - 1), + disabled=st.just(False), + court=st.just(False), + **charges, + ) +) +older_members = st.fixed_dictionaries( + dict( + age=st.integers( + min_value=ADDITIONAL_NEEDS_AGE_LIMIT, + max_value=ADDITIONAL_NEEDS_AGE_LIMIT + 3, + ), + disabled=st.booleans(), + court=st.booleans(), + **charges, + ) +) +ineligible_members = st.one_of(teenagers, older_members) + + +@settings(max_examples=15, deadline=None) +# A 3-year-old and a 15-year-old with $1,000 each and a $200 fee. +@example( + child=dict(age=3, disabled=False, court=False, care=1_000, after_school=0), + other=dict(age=15, disabled=False, court=False, care=1_000, after_school=0), + fee=200, +) +# The age boundaries: 12 and 13, and 18 and 19 with a disability. +@example( + child=dict(age=12, disabled=False, court=False, care=0, after_school=500), + other=dict(age=13, disabled=False, court=False, care=0, after_school=500), + fee=0, +) +@example( + child=dict(age=18, disabled=True, court=False, care=900, after_school=0), + other=dict(age=19, disabled=True, court=True, care=900, after_school=300), + fee=50, +) +@given(child=eligible_children, other=ineligible_members, fee=st.integers(0, 500)) +def test_a_member_outside_the_age_limits_adds_nothing(child, other, fee): + alone = colorado({"child": child}, fee) + with_other = colorado({"child": child, "other": other}, fee) + assert with_other == pytest.approx(alone, abs=TOLERANCE) + + +@settings(max_examples=20, deadline=None) +@example( + children=[ + dict(age=3, disabled=False, court=False, care=1_000, after_school=0), + dict(age=15, disabled=True, court=False, care=1_000, after_school=0), + dict(age=15, disabled=False, court=False, care=1_000, after_school=0), + ], + fee=0, +) +# A fee above the eligible child's charges leaves nothing to pay. +@example( + children=[ + dict(age=3, disabled=False, court=False, care=100, after_school=0), + dict(age=15, disabled=False, court=False, care=1_000, after_school=0), + ], + fee=200, +) +@given( + children=st.lists( + st.one_of(eligible_children, ineligible_members), min_size=1, max_size=3 + ), + fee=st.integers(min_value=0, max_value=500), +) +def test_the_payment_equals_the_eligible_childrens_charges_less_the_fee(children, fee): + named = {f"child_{i}": child for i, child in enumerate(children)} + paid = colorado(named, fee) + eligible_charges = sum( + child["care"] + child["after_school"] for child in children if eligible(child) + ) + assert paid >= -TOLERANCE + assert paid == pytest.approx(max(eligible_charges - fee, 0), abs=TOLERANCE) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_subsidy.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_subsidy.py index e1936345ec1..9e766b1e68b 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_subsidy.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_subsidy.py @@ -8,12 +8,27 @@ class co_ccap_subsidy(Variable): unit = USD definition_period = MONTH defined_for = "co_ccap_eligible" + reference = ( + # 8 CCR 1403-1 3.103(UU), PDF page 9 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=9", + # 3.111(F), PDF page 35 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=35", + # 3.132(D) and (E), PDF page 126 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=12658&fileName=8%20CCR%201403-1#page=126", + ) def formula(spm_unit, period, parameters): - expenses = add( - spm_unit, + person = spm_unit.members + # 8 CCR 1403-1 3.132(D) and (E) pay for the care of eligible children: + # children under 13, and children under 19 who can't care for + # themselves or who are under court supervision (3.103(UU), 3.111(F)). + # A member outside those limits, such as a teenager, adds nothing. + eligible_child = person("co_ccap_child_eligible", period) + charge = add( + person, period, ["pre_subsidy_childcare_expenses", "after_school_expenses"], ) + expenses = spm_unit.sum(charge * eligible_child) parent_fee = spm_unit("co_ccap_parent_fee", period) return max_(expenses - parent_fee, 0)