diff --git a/changelog.d/vt-ccfap-care-used.fixed.md b/changelog.d/vt-ccfap-care-used.fixed.md new file mode 100644 index 00000000000..7230fcb5263 --- /dev/null +++ b/changelog.d/vt-ccfap-care-used.fixed.md @@ -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. diff --git a/policyengine_us/parameters/gov/states/vt/dcf/ccfap/rates/pays_above_provider_charge.yaml b/policyengine_us/parameters/gov/states/vt/dcf/ccfap/rates/pays_above_provider_charge.yaml new file mode 100644 index 00000000000..02d0a8b3788 --- /dev/null +++ b/policyengine_us/parameters/gov/states/vt/dcf/ccfap/rates/pays_above_provider_charge.yaml @@ -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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap.yaml index 3e9e312a8ec..427470e1f1f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap.yaml @@ -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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap.yaml index c5af9a2c7a7..8d889ccf848 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap.yaml @@ -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 diff --git a/policyengine_us/tests/test_child_care_subsidy_needs_care.py b/policyengine_us/tests/test_child_care_subsidy_needs_care.py new file mode 100644 index 00000000000..f8e0d001ebd --- /dev/null +++ b/policyengine_us/tests/test_child_care_subsidy_needs_care.py @@ -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 == {} diff --git a/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap.py b/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap.py index d5f83fd5308..09a1f3f5638 100644 --- a/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap.py +++ b/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap.py @@ -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 ) diff --git a/policyengine_us/variables/gov/states/vt/dcf/ccfap/vt_ccfap.py b/policyengine_us/variables/gov/states/vt/dcf/ccfap/vt_ccfap.py index c6cacec5c1d..70c71b68269 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/ccfap/vt_ccfap.py +++ b/policyengine_us/variables/gov/states/vt/dcf/ccfap/vt_ccfap.py @@ -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)