Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/vt-ccfap-care-used.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Vermont no longer pays a child care subsidy for a child with no care hours and no bill, and caps it at any bill before December 17, 2023. New Hampshire no longer pays its disability supplement for a child with no charge.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description: Vermont pays the state rate above a provider's charge under the Child Care Financial Assistance Program, if this is true.

metadata:
unit: bool
period: year
label: Vermont CCFAP pays state rate above provider charge
reference:
- title: CCFAP Understanding Payments, question 3
href: https://outside.vermont.gov/dept/DCF/Shared%20Documents/CDD/CCFAP/CCFAP-Understanding-Payments.pdf#page=1

# Before this date Vermont paid the lesser of the state rate and the provider's rate.
values:
2009-02-09: false
2023-12-17: true
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,59 @@
# Monthly = 288.46 * 52/12 = $1,250
# Benefit capped at actual expenses, NOT inflated by negative income
nh_ccap: 1_250

- name: Case 12, disabled child with no charge draws no supplement.
period: 2025-01
absolute_error_margin: 0.01
input:
people:
person1:
age: 30
person2:
age: 5
is_disabled: true
nh_ccap_eligible_child: true
nh_ccap_payment_rate: 280
nh_ccap_disability_supplement: 100
pre_subsidy_childcare_expenses: 0
spm_units:
spm_unit:
members: [person1, person2]
nh_ccap_eligible: true
nh_ccap_cost_share: 0
households:
household:
members: [person1, person2]
state_code: NH
output:
nh_ccap: 0

- name: Case 13, supplement stays outside the cap when the cost share covers the charge.
period: 2025-01
absolute_error_margin: 0.01
input:
people:
person1:
age: 30
person2:
age: 5
is_disabled: true
nh_ccap_eligible_child: true
nh_ccap_payment_rate: 280
nh_ccap_disability_supplement: 100
pre_subsidy_childcare_expenses: 5_200
# $5,200/yr = $100/week
spm_units:
spm_unit:
members: [person1, person2]
nh_ccap_eligible: true
nh_ccap_cost_share: 150
households:
household:
members: [person1, person2]
state_code: NH
output:
# base_payment = max(min($100, $280) - $150, 0) = $0
# Weekly: $0 + $100 disability supplement = $100
# Monthly: $100 * (52/12) = $433.33
nh_ccap: 433.33
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,180 @@
# Monthly rate = $439 * 52 / 12 = $1,902.33
# Benefit = max(1,902.33 - 0, 0) = $1,902.33
vt_ccfap: 1_902.33

- name: A child with no care hours and no bill draws no payment.
absolute_error_margin: 0.01
period: 2025-01
input:
people:
person1:
age: 30
employment_income: 52_875
person2:
age: 3
is_tax_unit_dependent: true
spm_units:
spm_unit:
members: [person1, person2]
meets_ccdf_activity_test: true
households:
household:
members: [person1, person2]
state_code: VT
output:
vt_ccfap_eligible: true
vt_ccfap: 0

- name: A bill below the state rate still draws the state rate.
absolute_error_margin: 0.01
period: 2025-01
input:
people:
person1:
age: 30
employment_income: 52_875
person2:
age: 3
is_tax_unit_dependent: true
vt_ccfap_provider_type: LICENSED_CENTER
spm_units:
spm_unit:
members: [person1, person2]
meets_ccdf_activity_test: true
spm_unit_pre_subsidy_childcare_expenses: 2_000
households:
household:
members: [person1, person2]
state_code: VT
output:
# No hours reads as part time, so $240 a week for a preschooler at a licensed
# center, less a $100 weekly family share: $140 * 52 / 12 = $606.67.
vt_ccfap: 606.67

- name: Only the child in care draws the rate.
absolute_error_margin: 0.01
period: 2025-01
input:
people:
person1:
age: 30
employment_income: 52_875
person2:
age: 3
is_tax_unit_dependent: true
vt_ccfap_provider_type: LICENSED_CENTER
childcare_hours_per_week: 40
person3:
age: 4
is_tax_unit_dependent: true
vt_ccfap_provider_type: LICENSED_CENTER
spm_units:
spm_unit:
members: [person1, person2, person3]
meets_ccdf_activity_test: true
vt_ccfap_family_share: 0
households:
household:
members: [person1, person2, person3]
state_code: VT
output:
# $439 a week for one full-time preschooler: $439 * 52 / 12 = $1,902.33.
vt_ccfap: 1_902.33

- name: Before December 17, 2023, the family's bill caps the state rate.
absolute_error_margin: 0.01
period: 2023-01
input:
people:
person1:
age: 30
person2:
age: 3
vt_ccfap_eligible_child: true
vt_ccfap_state_rate: 325
pre_subsidy_childcare_expenses: 10_000
spm_units:
spm_unit:
members: [person1, person2]
vt_ccfap_eligible: true
vt_ccfap_family_share: 0
households:
household:
members: [person1, person2]
state_code: VT
output:
# $10,000 / 52 = $192.31 a week, below the $325 state rate: $10,000 / 12.
vt_ccfap: 833.33

- name: Before December 17, 2023, care hours with no bill leave the charge unknown.
absolute_error_margin: 0.01
period: 2023-01
input:
people:
person1:
age: 30
person2:
age: 3
vt_ccfap_eligible_child: true
vt_ccfap_state_rate: 325
childcare_hours_per_week: 40
spm_units:
spm_unit:
members: [person1, person2]
vt_ccfap_eligible: true
vt_ccfap_family_share: 0
households:
household:
members: [person1, person2]
state_code: VT
output:
# Nothing caps the rate: $325 * 52 / 12 = $1,408.33.
vt_ccfap: 1_408.33

- name: Before December 17, 2023, a child with no hours and no bill draws nothing.
absolute_error_margin: 0.01
period: 2023-01
input:
people:
person1:
age: 30
person2:
age: 3
vt_ccfap_eligible_child: true
vt_ccfap_state_rate: 325
spm_units:
spm_unit:
members: [person1, person2]
vt_ccfap_eligible: true
vt_ccfap_family_share: 0
households:
household:
members: [person1, person2]
state_code: VT
output:
vt_ccfap: 0

- name: From December 17, 2023, the state rate stands above the bill.
absolute_error_margin: 0.01
period: 2024-01
input:
people:
person1:
age: 30
person2:
age: 3
vt_ccfap_eligible_child: true
vt_ccfap_state_rate: 439
pre_subsidy_childcare_expenses: 10_000
spm_units:
spm_unit:
members: [person1, person2]
vt_ccfap_eligible: true
vt_ccfap_family_share: 0
households:
household:
members: [person1, person2]
state_code: VT
output:
# $439 * 52 / 12 = $1,902.33.
vt_ccfap: 1_902.33
75 changes: 75 additions & 0 deletions policyengine_us/tests/test_child_care_subsidy_needs_care.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""A child care subsidy pays for care.

Invariant, in every state and for any family: children with no child care bill
and no care hours draw no subsidy, so child_care_subsidies is 0. Vermont and New
Hampshire broke this until they gated their payments on care.

This sweeps all 51 programs at once. YAML can pin one family in one state, but it
can't assert one rule across every state and a generated family, and one
simulation holds all 51 households, so each example builds one model.
"""

from hypothesis import given, settings
from hypothesis import strategies as st

from policyengine_us import Simulation

YEAR = 2026
STATES = (
"AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO "
"MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY"
).split()


def situation(earnings, child_ages, disabled):
people, tax_units, spm_units, families, marital_units, households = (
{} for _ in range(6)
)
for state in STATES:
parent = f"{state}_parent"
children = [f"{state}_child_{i}" for i in range(len(child_ages))]
members = [parent, *children]
people[parent] = {
"age": {YEAR: 30},
"employment_income": {YEAR: earnings},
"weekly_hours_worked_before_lsr": {YEAR: 40},
"weeks_worked": {YEAR: 52},
}
for child, age in zip(children, child_ages):
people[child] = {
"age": {YEAR: age},
"is_disabled": {YEAR: disabled},
"is_tax_unit_dependent": {YEAR: True},
}
tax_units[state] = {"members": members}
families[state] = {"members": members}
marital_units[state] = {"members": [parent]}
spm_units[state] = {
"members": members,
"spm_unit_pre_subsidy_childcare_expenses": {YEAR: 0},
"meets_ccdf_activity_test": {YEAR: True},
}
households[state] = {"members": members, "state_name": {YEAR: state}}
return {
"people": people,
"tax_units": tax_units,
"families": families,
"marital_units": marital_units,
"spm_units": spm_units,
"households": households,
}


@settings(max_examples=8, deadline=None)
@given(
earnings=st.integers(0, 60_000),
child_ages=st.lists(st.integers(0, 12), min_size=1, max_size=2),
disabled=st.booleans(),
)
def test_no_care_draws_no_subsidy(earnings, child_ages, disabled):
simulation = Simulation(situation=situation(earnings, child_ages, disabled))
subsidies = simulation.calculate("child_care_subsidies", YEAR)
paying = {
state: float(amount) for state, amount in zip(STATES, subsidies) if amount != 0
}
assert paying == {}
7 changes: 5 additions & 2 deletions policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ def formula(spm_unit, period, parameters):
per_child_cost_share[mask] = family_cost_share[mask] / n_eligible_children[mask]
per_child_cost_share_broadcast = spm_unit.project(per_child_cost_share)

# He-C 6910.17(g): disability supplement is additional, outside the min
# He-C 6910.17(g): disability supplement adds to the standard rates, outside
# the min. Those rates pay for care, so a child with no charge draws none.
base_payment = max_(capped_rate - per_child_cost_share_broadcast, 0)
per_child_weekly_payment = base_payment + disability_supplement
per_child_weekly_payment = base_payment + disability_supplement * (
annual_expense > 0
)
per_child_monthly_payment = (
per_child_weekly_payment * weeks_per_month * is_eligible_child
)
Expand Down
27 changes: 22 additions & 5 deletions policyengine_us/variables/gov/states/vt/dcf/ccfap/vt_ccfap.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,37 @@ class vt_ccfap(Variable):
defined_for = "vt_ccfap_eligible"
label = "Vermont Child Care Financial Assistance Program benefit"
reference = (
"https://outside.vermont.gov/dept/DCF/Shared%20Documents/CDD/CCFAP/CCFAP-Regulations.pdf#page=13",
"https://outside.vermont.gov/dept/DCF/Shared%20Documents/CDD/CCFAP/CCFAP-Regulations.pdf#page=17",
"https://legislature.vermont.gov/statutes/section/33/035/03512",
)

def formula(spm_unit, period, parameters):
# Models the post-2023-12-16 regime: payment is based on the state
# rate regardless of the provider's charges. Prior to 2023-12-17,
# Vermont paid the lesser of the state rate and the provider's rate.
# From 2023-12-17 payment is based on the state rate regardless of the
# provider's charges. Before then, Vermont paid the lesser of the state
# rate and the provider's rate, and the family's bill stands in for that.
# With no bill the charge is unknown, so nothing caps the state rate. The
# model reads parameters at the start of each month, so all of December
# 2023 falls under the earlier rule.
# Source: CCFAP Understanding Payments (Q3 and "Jane Smith" example)
# https://outside.vermont.gov/dept/DCF/Shared%20Documents/CDD/CCFAP/CCFAP-Understanding-Payments.pdf
# CCFAP Regulations V pays from the first day of eligibility and actual
# attendance, so a child who attends no provider draws nothing. Care hours
# or a bill stand in for attendance.
p = parameters(period).gov.states.vt.dcf.ccfap.rates
person = spm_unit.members
eligible_child = person("vt_ccfap_eligible_child", period)
weekly_rate = person("vt_ccfap_state_rate", period)
total_weekly_rate = spm_unit.sum(weekly_rate * eligible_child)
annual_bill = person("pre_subsidy_childcare_expenses", period.this_year)
in_care = (person("childcare_hours_per_week", period.this_year) > 0) | (
annual_bill > 0
)
state_rate = person("vt_ccfap_state_rate", period)
weekly_rate = where(
p.pays_above_provider_charge | (annual_bill == 0),
state_rate,
min_(state_rate, annual_bill / WEEKS_IN_YEAR),
)
total_weekly_rate = spm_unit.sum(weekly_rate * eligible_child * in_care)
monthly_rate = total_weekly_rate * WEEKS_IN_YEAR / MONTHS_IN_YEAR
family_share = spm_unit("vt_ccfap_family_share", period)
return max_(monthly_rate - family_share, 0)
Loading