Skip to content

Use shared childcare inputs and derive the child category in DC CCSP - #9285

Open
hua7450 wants to merge 6 commits into
mainfrom
dc-ccsp-shared-inputs-age-category
Open

Use shared childcare inputs and derive the child category in DC CCSP#9285
hua7450 wants to merge 6 commits into
mainfrom
dc-ccsp-shared-inputs-age-category

Conversation

@hua7450

@hua7450 hua7450 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

A sweep of all 48 state child care subsidy programs found DC CCSP to be the outlier on two counts. This PR fixes both.

  1. DC defined its own attending-days input. dc_ccsp_attending_days_per_month duplicated the shared childcare_attending_days_per_month that 17 other state programs read (AK, AR, AZ, FL, KY, MA, MN, MO, NE, OK, PA, TX, VA, WV, WY and others). DC now reads the shared variable as period.this_year, matching the convention in those states (intSTOCK, so no monthly division).

  2. DC's reimbursement age category was a pure input. dc_ccsp_child_category defaulted to PRESCHOOL for every child. Of the 48 programs, 45 derive the age band in a named variable and CA derives it inline — DC was the only pure input.

Following the established pattern

31 of the 52 state child care age variables derive the band with .calc() on a months-keyed single_amount bracket, and age, period.this_year * MONTHS_IN_YEAR is the dominant age source. Alabama is a direct template: al_ccsp_age_category is a clean three-member enum with thresholds at 0, 36 and 60 months — the same boundaries DC uses — and AL keeps provider type, region and quality tier in their own enums rather than fusing them into the age one.

So DC now has a clean age enum, and the fused rate-table row is derived from it:

dc_ccsp_age_category      INFANT_AND_TODDLER / PRESCHOOL / SCHOOL_AGE
                          p.months.calc(age * MONTHS_IN_YEAR)
age_category/months.yaml  0 -> 0, 36 -> 1, 60 -> 2
        |
        v
dc_ccsp_child_category    the seven-row rate-table key

Going through a three-member enum also avoids mapping onto non-contiguous indices of the seven-member category enum, which would break silently if anyone reordered it.

Age bands

The FY25 rate sheet names three bands but does not define them; the definitions come from the licensing regulations and the policy manual.

Band Boundary Source
Infant and Toddler under 36 months 5-A DCMR § 199 defines "Infant" as under 12 months; QIN infant/toddler eligibility runs "until Sept. 30 of the year when the child turns 3 years old" (policy manual p. 34)
Preschool 36 to under 60 months 5-A DCMR § 199: "Preschool or preschooler — a child thirty-six (36) to sixty (60) months of age but younger than school age"
School-Age 60 months and over Policy manual p. 5: "School-age children (ages 5 and older) may only participate in the Child Care Subsidy Program for OST care"

Which rate rows the formula produces

The rate sheet fuses the age band with the service authorized, so only three of the seven rows follow from age:

Row Produced when
INFANT_AND_TODDLER under 36 months
PRESCHOOL 36 to under 60 months
SCHOOL_AGE_BEFORE_OR_AFTER 60 months and over

The other four are reachable only by setting dc_ccsp_child_category directly, and the formula documents why:

  • INFANT_AND_TODDLER_SPECIAL_NEEDS, PRESCHOOL_AND_SCHOOL_AGE_SPECIAL_NEEDS — the special needs rate is authorized at the facility, not the child. Per the FY26 Level I Subsidy Agreement §(4), "in order to receive the special needs rate, the Provider shall complete the Special Needs Rate Request Form by facility," renewed annually. No household characteristic implies it, so it is deliberately not derived from is_disabled.
  • PRESCHOOL_BEFORE_AND_AFTER — wraparound care around a public pre-K day is not in the household data, and it carries the same rate as PRESCHOOL wherever both are published.
  • SCHOOL_AGE_BEFORE_AND_AFTER — care on both ends of the school day. Care on a single end is the more common placement.

There is no plain school-age row on the sheet, because school-age children may participate only for out-of-school-time care — every school-age placement is before or after school care by definition.

Known limitation: SCHOOL_AGE_BEFORE_OR_AFTER is priced only in the two traditional columns, so a school-age child whose dc_ccsp_schedule_type is set to an extended day or nontraditional column reaches no published rate and needs SCHOOL_AGE_BEFORE_AND_AFTER set directly. That case is pinned in a test. dc_ccsp_schedule_type defaults to FULL_TIME_TRADITIONAL, where both school-age rows carry the same amount.

Impact

Under the old PRESCHOOL default, a child development center full-time traditional day was priced at $63.00 regardless of age — 36% below the $98.65 infant rate and 41% above the $36.74 school-age rate. Microsimulation output is unchanged, because childcare_attending_days_per_month is 0 for everyone in the Populace microdata, as with every other attending-days state.

Files

variables/gov/states/dc/dhs/ccsp/payment/dc_ccsp_age_category.py                new
variables/gov/states/dc/dhs/ccsp/payment/dc_ccsp_child_category.py             formula added
variables/gov/states/dc/dhs/ccsp/payment/dc_ccsp_maximum_subsidy_amount.py
variables/gov/states/dc/dhs/ccsp/copay/dc_ccsp_first_child_copay.py
variables/gov/states/dc/dhs/ccsp/copay/dc_ccsp_second_child_copay.py
variables/gov/states/dc/dhs/ccsp/payment/dc_ccsp_attending_days_per_month.py   deleted
parameters/gov/states/dc/dhs/ccsp/age_category/months.yaml                     new

Not addressed here

Left for separate PRs so this one stays reviewable:

  • dc_ccsp_maximum_subsidy_amount is gated only on StateCode.DC, and dc_ccsp sums it over every SPM unit member — an adult or over-age child with nonzero attending days inflates the subsidy. The copay side correctly gates on the two youngest eligible children.
  • reimbursement_rates.yaml encodes only the Developing/Progressing quality tier; OSSE publishes five designations (Developing, Progressing, Quality, High-Quality, Preliminary), and there is no facility-quality dimension in the breakdown.
  • The second-child copay amount at 2.31 FPG is $0.60, between $1.90 (2.21) and $2.59 (2.41) — likely a transcription slip from the sliding fee scale.
  • CHILD_HOME_AND_EXPANDED_HOME PRESCHOOL_BEFORE_AND_AFTER full-time traditional is encoded as $53.03; the FY25 rate sheet says $53.04.

Test plan

  • New dc_ccsp_age_category cases covering a four-child family and both band boundaries at exact months (36 and 60)
  • dc_ccsp_child_category cases assert the daily rate each band reaches, including the school-age part-time rate ($18.37/day) and the unpriced nontraditional cell
  • policyengine-core test .../states/dc — 332 passed
  • gov/hhs/ccdf, household_state_benefits.yaml and tests/policy/baseline/partners — 699 passed
  • make format clean
  • CI passes

DC CCSP was the only state child care program that defined its own
attending-days input rather than using the shared
childcare_attending_days_per_month that 17 other states read, and the only
one whose reimbursement age category was a pure input instead of a formula
over age.

- Replace dc_ccsp_attending_days_per_month with the shared
  childcare_attending_days_per_month, read as period.this_year to match the
  convention in the other states.
- Derive dc_ccsp_child_category from age: infants and toddlers under 36
  months, preschoolers from 36 to under 60 months, school-age at 60 months
  and over, per 5-A DCMR 199 and the CCSP policy manual. The two special
  needs categories and the school-age Before or After part-time rate turn on
  the service authorized rather than the child's age, so the variable stays
  overridable.

Previously every DC child was priced at the preschool rate by default, which
understated an infant's daily rate at a child development center by 36
percent ($63.00 against $98.65) and overstated a school-ager's by 71 percent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (66cefde) to head (0de5ea4).
⚠️ Report is 15 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9285   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         6    +5     
  Lines           22       115   +93     
=========================================
+ Hits            22       115   +93     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

hua7450 and others added 2 commits August 14, 2026 12:35
Replaces the ad-hoc select over two scalar month thresholds with the pattern
the rest of the repo uses. 31 of the 52 state child care age variables derive
their band with .calc() on a months-keyed single_amount bracket, and Alabama
is a direct template for DC: a clean three-member age enum with thresholds at
0, 36 and 60 months, the same boundaries DC uses, with the other rate
dimensions kept in their own enums rather than fused into the age one.

- Add dc_ccsp_age_category (INFANT_AND_TODDLER / PRESCHOOL / SCHOOL_AGE) and
  age_category/months.yaml, mirroring al_ccsp_age_category.
- Derive dc_ccsp_child_category, the fused rate-table key, from that age
  category instead of from raw month comparisons. This also removes the
  fragile mapping onto non-contiguous enum indices that a bracket over the
  seven-member category enum would have needed.

Rewrites the tests around real households rather than the mechanics. The
previous child-category file included a case that fed in a category and
asserted the same value back, which tested the input override rather than any
DC rule. Every case now starts from age and asserts the daily rate the band
reaches, including a disabled infant holding the ordinary $98.65 infant rate
rather than the $118.38 special needs rate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Care on a single end of the school day is the more common out-of-school-time
placement, and the rate sheet publishes no plain school-age row because
school-age children may participate only for OST care.

Also documents which rows the formula never produces from household inputs.
The two special needs rows are authorized at the facility rather than by the
child: under the Level I subsidy agreement a provider files a Special Needs
Rate Request Form per facility and renews it annually, so no household
characteristic implies the rate and it is deliberately not derived from
is_disabled. Preschool Before and After and School-Age Before and After turn
on wraparound care that the household data does not record.

Note that School-Age Before or After is priced only in the two traditional
columns, so a school-age child on an extended day or nontraditional schedule
reaches no published rate and needs School-Age Before and After set directly.
That case is pinned in a test. The schedule type defaults to full-time
traditional, where both school-age rows carry the same amount, so
microsimulation output is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hua7450 and others added 3 commits August 14, 2026 13:53
…r caregiver test

dc_ccsp sums dc_ccsp_maximum_subsidy_amount over every SPM unit member while
the variable was gated only on StateCode.DC, so an adult or an over-age child
with authorized days of care added to the subsidy. It is now gated on
dc_ccsp_eligible_child, matching the copay side, which already ranks only
eligible children.

dc_ccsp_qualified_need_eligible counted any household member's SSDI or SSI,
including a child's own benefits. Policy manual 2.4.2.6 conditions the elder
caregiver classification on an individual with responsibility for the
day-to-day care of the child who is "age 62 or older or receive[s] Social
Security disability benefits or Supplemental Security Income payments", so
both prongs now sit inside the head-or-spouse mask, as the age prong already
did.

Corrects the child development home Preschool Before and After full-time
traditional rate from $53.03 to $53.04, matching the FY25 rate sheet.

Two other suspected defects were checked against the sources and are not
defects, so nothing changed:

- The non-monotonic second-child copay amounts are printed that way in the
  OSSE sliding fee scale, including $0.60 at 231-240% of poverty between
  $1.90 and $2.59. Added a note so they are not "corrected" later.
- assessed_property_value belongs in the asset sources: policy manual 2.5
  counts assets "including cash, money in bank accounts, investments, and
  real estate".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both cases in dc_ccsp_maximum_subsidy_amount.yaml set no age, so the person
defaulted to 40. Once the payment was gated on dc_ccsp_eligible_child that
person was an adult, over the age limit and the head of the tax unit rather
than a dependent, so the rate lookup returned zero.

Each case now includes the parent the child lives with and states the child's
age, matching what the case names already described. The asserted daily rates
are unchanged at $98.65 and $39.78. Adds a case for a child over the age limit
who has authorized days of care, so the new gate cannot regress silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant