PolicyEngine / PolicyEngine/policyengine-us

SC and AK CCAP pool per-child provider rate caps before the lesser-of comparison

Open
#9,269 0 comments 0 reactions 1 assignee Claimed by @hua7450 View on GitHub
Dominant language
Python
Stars
162
Forks
212
Avg merge
3d 3h
Merged PRs (30d)
97

Description

## Summary

`sc_ccap` and `ak_ccap` sum every child's provider rate cap into an SPM-unit total before applying a single lesser-of comparison against household childcare expenses. Both programs set rates **per child**, so the comparison must run per child and then sum. As written, one child's unused rate headroom covers another child's above-rate charges, overpaying the benefit.

Found while reviewing #9040, which had the same defect in `tn_ccap` (fixed there by summing the per-child minima, following the `mt_ccap` pattern).

## The defect

Both states compute a genuine per-child cap in a `Person`-level variable:

- `sc_ccap_maximum_weekly_benefit` (Person) — rate varies by age group, provider type, quality level, geography, time category, plus special-needs and foster surcharges
- `ak_ccap_benefit_per_child` (Person) — authorized rate per child plus a special-needs supplement

Then the top-level variable pools them:

```python
# sc_ccap.py
maximum_weekly_benefit = add(spm_unit, period, ["sc_ccap_maximum_weekly_benefit"])
maximum_monthly_benefit = maximum_weekly_benefit * (WEEKS_IN_YEAR / MONTHS_IN_YEAR)
pre_subsidy_childcare_expenses = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period)
capped_expenses = min_(pre_subsidy_childcare_expenses, maximum_monthly_benefit)
return max_(capped_expenses - copay, 0)

# ak_ccap.py
total_per_child = add(spm_unit, period, ["ak_ccap_benefit_per_child"])
pre_subsidy_childcare_expenses = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period)
capped = min_(pre_subsidy_childcare_expenses, total_per_child)
return max_(0, capped - copay)
```

`min_(E1 + E2, C1 + C2)` is not `min_(E1, C1) + min_(E2, C2)` whenever one child is under its cap and the other is over it.

## Reproduction

Both cases use one infant and one school-age child at the same provider, so only the age-driven rate differs.

**SC** — urban center, quality C, 2025-01, \$19,200/yr expenses:

| | |
|---|---|
| Per-child monthly caps | \$923.00 (infant), \$715.00 (school-age) |
| Per-child monthly expenses | \$800.00 each |
| Pooled (current) | **\$1,600.00** |
| Per-child (correct) | **\$1,515.00** |
| Overpayment | **\$85.00/mo** |

**AK** — Anchorage licensed center, FT_MONTH, 2022-01, \$24,000/yr expenses:

| | |
|---|---|
| Per-child monthly caps | \$1,135.00 (infant), \$884.00 (school-age) |
| Per-child monthly expenses | \$1,000.00 each |
| Copay | \$80.00 |
| Pooled (current) | **\$1,920.00** |
| Per-child (correct) | **\$1,804.00** |
| Overpayment | **\$116.00/mo** |

The error is zero for single-child households and zero when every child is either under or over their own cap. It peaks at `(C_high - C_low) / 2` when the expense split straddles the two caps, so it grows with the age-driven rate spread.

## Suggested fix

Follow `mt_ccap`, which already does this correctly:

```python
person = spm_unit.members
cap = person("_..._per_child", period)
child_expenses = person("pre_subsidy_childcare_expenses", period)
capped_expenses = spm_unit.sum(min_(child_expenses, cap))
return max_(capped_expenses - copay, 0)
```

Ineligible children fall out on their own where the per-child rate variable carries a `defined_for` that zeroes their cap.

**Caveat.** `pre_subsidy_childcare_expenses` (Person) splits the SPM-unit total **evenly** across children, so per-child capping still cannot capture genuinely uneven per-child provider charges. It does correct the case where the *caps* differ by age category, which is the common one and what drives the reproductions above. `mt_ccap` carries the same limitation.

## Possibly affected beyond SC and AK

A grep for the same shape (`min_` against `spm_unit_pre_subsidy_childcare_expenses` alongside an `add(spm_unit, ...)` of a per-child rate) returns 17 further candidates. **These are pattern matches only — not verified, and some will be correct** (e.g. programs whose rate genuinely has no per-child dimension). Listed for triage, not as confirmed defects:

`ms_ccpp`, `ma_ccfa`, `mn_ccap`, `in_ccdf`, `ct_c4k`, `ri_ccap`, `tx_ccs`, `pa_ccw`, `nd_ccap_base_subsidy`, `nm_ccap`, `nj_ccap`, `dc_ccsp`, `hi_ccap`, `de_poc`, `az_ccap`, `id_iccp`, `wa_wccc`

Each needs its own check: does the state's rate schedule vary by child (age, provider, quality)? If yes, the lesser-of belongs per child.

## Test gap

Existing SC and AK tests pass because their multi-child cases do not straddle the caps. Any fix should add a case where one child sits under their cap and another sits over it.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.