From 57bb6c30da7cb018898dcf631b208578fb815be4 Mon Sep 17 00:00:00 2001 From: Rajveer Kapoor Date: Wed, 16 Sep 2026 19:14:25 +0530 Subject: [PATCH 01/10] Count all child care subsidies in household benefits --- .../household/household_state_benefits.yaml | 151 +----------------- 1 file changed, 8 insertions(+), 143 deletions(-) diff --git a/policyengine_us/parameters/gov/household/household_state_benefits.yaml b/policyengine_us/parameters/gov/household/household_state_benefits.yaml index 080a049c39f..e6d24a61128 100644 --- a/policyengine_us/parameters/gov/household/household_state_benefits.yaml +++ b/policyengine_us/parameters/gov/household/household_state_benefits.yaml @@ -1,9 +1,10 @@ description: The government counts these sources as benefits paid by state agencies. values: 2023-01-01: + # Child care subsidies administered by state agencies + - child_care_subsidies # Delaware benefits - de_ssp - - de_child_care_subsidies # Indiana benefits - in_ssp # Connecticut benefits @@ -14,9 +15,6 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies - # Montana benefits - - mt_child_care_subsidies # Massachusetts benefits - ma_state_supplement # Maryland benefits @@ -28,9 +26,6 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies - # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -41,40 +36,24 @@ values: - ca_state_supplement # Alabama benefits - al_ssp - - al_child_care_subsidies # Alaska benefits - ak_ssp - # Arizona benefits - - az_child_care_subsidies # Florida benefits - fl_oss # Idaho benefits - id_aabd - - id_child_care_subsidies # Kentucky benefits - ky_ssp - # Maryland benefits - - md_child_care_subsidies # Nebraska benefits - ne_aabd - - ne_child_care_subsidies - # Nevada benefits - - nv_child_care_subsidies - # Oklahoma benefits - - ok_child_care_subsidies - # South Carolina benefits - - sc_child_care_subsidies # New Mexico benefits - nm_ssi_state_supplement # South Carolina benefits - sc_ssi_state_supplement # Texas benefits - tx_ssi_state_supplement - # Virginia benefits - - va_child_care_subsidies # Massachusetts benefits - ma_eaedc - - ma_tafdc # DC benefits - dc_ossp # Kansas benefits @@ -87,17 +66,13 @@ values: - wa_rca # Louisiana benefits - la_oss - # Washington benefits - - wa_child_care_subsidies - # New York benefits - - ny_child_care_subsidies 2024-01-01: + # Child care subsidies administered by state agencies + - child_care_subsidies # Delaware benefits - de_ssp - - de_child_care_subsidies # Indiana benefits - in_ssp - - in_child_care_subsidies # Connecticut benefits - ct_ssp # Georgia benefits @@ -106,9 +81,6 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies - # Montana benefits - - mt_child_care_subsidies # Massachusetts benefits - ma_state_supplement # Maryland benefits @@ -120,9 +92,6 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies - # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -133,46 +102,24 @@ values: - ca_state_supplement # Alabama benefits - al_ssp - - al_child_care_subsidies # Alaska benefits - ak_ssp - # Arizona benefits - - az_child_care_subsidies # Florida benefits - fl_oss # Idaho benefits - id_aabd - - id_child_care_subsidies # Kentucky benefits - ky_ssp - # Georgia benefits - - ga_child_care_subsidies - # Maryland benefits - - md_child_care_subsidies # Nebraska benefits - ne_aabd - - ne_child_care_subsidies - # Nevada benefits - - nv_child_care_subsidies - # Oklahoma benefits - - ok_child_care_subsidies # New Mexico benefits - nm_ssi_state_supplement # South Carolina benefits - sc_ssi_state_supplement # Texas benefits - tx_ssi_state_supplement - # Virginia benefits - - va_child_care_subsidies - # West Virginia benefits - - wv_child_care_subsidies - # North Carolina benefits - - nc_child_care_subsidies - # South Carolina benefits - - sc_child_care_subsidies # Massachusetts benefits - ma_eaedc - - ma_tafdc # DC benefits - dc_ossp # Kansas benefits @@ -185,21 +132,13 @@ values: - wa_rca # Louisiana benefits - la_oss - # Arkansas benefits - - ar_child_care_subsidies - # Ohio benefits - - oh_child_care_subsidies - # Washington child care subsidies - - wa_child_care_subsidies - # New York benefits - - ny_child_care_subsidies 2025-01-01: + # Child care subsidies administered by state agencies + - child_care_subsidies # Delaware benefits - de_ssp - - de_child_care_subsidies # Indiana benefits - in_ssp - - in_child_care_subsidies # Connecticut benefits - ct_ssp # Georgia benefits @@ -208,9 +147,6 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies - # Montana benefits - - mt_child_care_subsidies # Massachusetts benefits - ma_state_supplement # Maryland benefits @@ -222,9 +158,6 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies - # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -235,49 +168,26 @@ values: - ca_state_supplement # Alabama benefits - al_ssp - - al_child_care_subsidies # Alaska benefits - ak_ssp - # Arizona benefits - - az_child_care_subsidies # Florida benefits - fl_oss # Idaho benefits - id_aabd - - id_child_care_subsidies # Kentucky benefits - ky_ssp - # Georgia benefits - - ga_child_care_subsidies - # Maryland benefits - - md_child_care_subsidies # Nebraska benefits - ne_aabd - - ne_child_care_subsidies - # Nevada benefits - - nv_child_care_subsidies - # Oklahoma benefits - - ok_child_care_subsidies # New Mexico benefits - nm_ssi_state_supplement # South Carolina benefits - sc_ssi_state_supplement # Texas benefits - tx_ssi_state_supplement - # Virginia benefits - - va_child_care_subsidies - # West Virginia benefits - - wv_child_care_subsidies - # North Carolina benefits - - nc_child_care_subsidies - # South Carolina benefits - - sc_child_care_subsidies # Massachusetts benefits - ma_eaedc - - ma_tafdc # DC benefits - dc_ossp - - dc_child_care_subsidies # Kansas benefits - ks_sspp # Hawaii benefits @@ -288,21 +198,13 @@ values: - wa_rca # Louisiana benefits - la_oss - # Arkansas benefits - - ar_child_care_subsidies - # Ohio benefits - - oh_child_care_subsidies - # Washington child care subsidies - - wa_child_care_subsidies - # New York benefits - - ny_child_care_subsidies 2026-01-01: + # Child care subsidies administered by state agencies + - child_care_subsidies # Delaware benefits - de_ssp - - de_child_care_subsidies # Indiana benefits - in_ssp - - in_child_care_subsidies # Connecticut benefits - ct_ssp # Georgia benefits @@ -311,9 +213,6 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies - # Montana benefits - - mt_child_care_subsidies # Massachusetts benefits - ma_state_supplement # Maryland benefits @@ -325,9 +224,6 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies - # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -338,49 +234,26 @@ values: - ca_state_supplement # Alabama benefits - al_ssp - - al_child_care_subsidies # Alaska benefits - ak_ssp - # Arizona benefits - - az_child_care_subsidies # Florida benefits - fl_oss # Idaho benefits - id_aabd - - id_child_care_subsidies # Kentucky benefits - ky_ssp - # Georgia benefits - - ga_child_care_subsidies - # Maryland benefits - - md_child_care_subsidies # Nebraska benefits - ne_aabd - - ne_child_care_subsidies - # Nevada benefits - - nv_child_care_subsidies - # Oklahoma benefits - - ok_child_care_subsidies # New Mexico benefits - nm_ssi_state_supplement # South Carolina benefits - sc_ssi_state_supplement # Texas benefits - tx_ssi_state_supplement - # Virginia benefits - - va_child_care_subsidies - # West Virginia benefits - - wv_child_care_subsidies - # North Carolina benefits - - nc_child_care_subsidies - # South Carolina benefits - - sc_child_care_subsidies # Massachusetts benefits - ma_eaedc - - ma_tafdc # DC benefits - dc_ossp - - dc_child_care_subsidies # Kansas benefits - ks_sspp # Hawaii benefits @@ -391,16 +264,8 @@ values: - wa_rca # Louisiana benefits - la_oss - # Arkansas benefits - - ar_child_care_subsidies - # Ohio benefits - - oh_child_care_subsidies - # Washington child care subsidies - - wa_child_care_subsidies # New Jersey benefits - nj_property_tax_relief - # New York benefits - - ny_child_care_subsidies metadata: unit: list period: year From 85c9734f38ef9ab46b680fa638a417fe27dbb03a Mon Sep 17 00:00:00 2001 From: Rajveer Kapoor Date: Wed, 16 Sep 2026 19:15:09 +0530 Subject: [PATCH 02/10] Add household child care subsidy regressions Added test cases for Massachusetts TAFDC and EAEDC benefits, as well as Alaska and North Carolina child care subsidies. Updated household state benefits calculations to ensure accurate representation of state programs. --- .../household/household_state_benefits.yaml | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index 8472559cb69..5ac212d52c2 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -53,3 +53,180 @@ state_code: DC output: household_state_benefits: 500 + +# MA TAFDC is one of the state TANF programs summed into `tanf`, and `tanf` is +# already part of `household_benefits`. Listing it here as well counted the +# grant twice in household net income. +- name: Massachusetts TAFDC reaches household benefits once, through TANF + period: 2026 + input: + age: 30 + state_code: MA + ma_tafdc: 6_000 + ma_eaedc: 0 + ma_state_supplement: 0 + social_security: 0 + ssi: 0 + snap: 0 + wic: 0 + free_school_meals: 0 + reduced_price_school_meals: 0 + acp: 0 + ebb: 0 + high_efficiency_electric_home_rebate: 0 + residential_efficiency_electrification_rebate: 0 + unemployment_compensation: 0 + child_support_received: 0 + workers_compensation: 0 + educational_assistance: 0 + financial_assistance: 0 + survivor_benefits: 0 + household_head_start_benefits: 0 + commodity_supplemental_food_program: 0 + basic_income: 0 + trump_dividend: 0 + housing_assistance: 0 + household_health_benefits: 0 + output: + tanf: 6_000 + household_state_benefits: 0 + household_benefits: 6_000 + +# EAEDC is not a TANF program, so it stays in household state benefits. +- name: Massachusetts EAEDC still flows into household state benefits + period: 2026 + input: + age: 30 + state_code: MA + ma_eaedc: 3_000 + ma_tafdc: 0 + ma_state_supplement: 0 + output: + household_state_benefits: 3_000 + +# Regression for the household in the report: a Massachusetts married couple +# with three children and 26,000 of earnings draws a 9,880 TAFDC grant in 2026. +# The grant must reach household benefits once, through `tanf`. SNAP, WIC and +# school meals are pinned to zero so that `household_benefits` isolates TAFDC. +- name: Massachusetts TAFDC of 9,880 reaches household benefits once in 2026 + period: 2026 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 26_000 + wic: 0 + spouse: + age: 30 + wic: 0 + child_1: + age: 1 + wic: 0 + child_2: + age: 4 + wic: 0 + child_3: + age: 9 + wic: 0 + marital_units: + parents: + members: [parent, spouse] + spm_units: + spm_unit: + members: [parent, spouse, child_1, child_2, child_3] + snap: 0 + free_school_meals: 0 + reduced_price_school_meals: 0 + households: + household: + members: [parent, spouse, child_1, child_2, child_3] + state_code: MA + output: + ma_tafdc: 9_880 + tanf: 9_880 + household_state_benefits: 0 + household_benefits: 9_880 + +# `tanf` is defined_for takes_up_tanf_if_eligible, so turning take-up off drops +# the grant out of household benefits. While TAFDC was also listed in +# `household_state_benefits`, that path ignored take-up and kept paying it. +- name: Massachusetts TAFDC leaves household benefits when TANF take-up is off + period: 2026 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 26_000 + wic: 0 + spouse: + age: 30 + wic: 0 + child_1: + age: 1 + wic: 0 + child_2: + age: 4 + wic: 0 + child_3: + age: 9 + wic: 0 + marital_units: + parents: + members: [parent, spouse] + spm_units: + spm_unit: + members: [parent, spouse, child_1, child_2, child_3] + takes_up_tanf_if_eligible: false + snap: 0 + free_school_meals: 0 + reduced_price_school_meals: 0 + households: + household: + members: [parent, spouse, child_1, child_2, child_3] + state_code: MA + output: + ma_tafdc: 9_880 + tanf: 0 + household_state_benefits: 0 + household_benefits: 0 + +# Alaska's CCAP was implemented but absent from household state benefits. +# The federal CCDF aggregate now connects every implemented state program. +- name: Alaska CCAP reaches household state benefits through the CCDF aggregate + period: 2025 + input: + people: + parent: + age: 30 + spm_units: + spm_unit: + members: [parent] + ak_child_care_subsidies: 4_800 + households: + household: + members: [parent] + state_code: AK + output: + child_care_subsidies: 4_800 + household_state_benefits: 4_800 + +# North Carolina was already listed directly. This protects against retaining +# a direct state entry alongside the new aggregate and counting SCCA twice. +- name: NC SCCA reaches household state benefits exactly once + period: 2025 + input: + people: + parent: + age: 30 + spm_units: + spm_unit: + members: [parent] + nc_child_care_subsidies: 3_600 + households: + household: + members: [parent] + state_code: NC + county_str: ALAMANCE_COUNTY_NC + output: + child_care_subsidies: 3_600 + household_state_benefits: 3_600 From 9a4101d485d4828f473d0242710981ad75a61881 Mon Sep 17 00:00:00 2001 From: Rajveer Kapoor Date: Wed, 16 Sep 2026 19:15:38 +0530 Subject: [PATCH 03/10] Add changelog for child care subsidy aggregation --- changelog.d/9405.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/9405.fixed.md diff --git a/changelog.d/9405.fixed.md b/changelog.d/9405.fixed.md new file mode 100644 index 00000000000..caa0d1d4e29 --- /dev/null +++ b/changelog.d/9405.fixed.md @@ -0,0 +1 @@ +Count every implemented state child care subsidy in household benefits. From d8fcf12a3256ff3e2a80c837399b181d9d838325 Mon Sep 17 00:00:00 2001 From: Rajveer Kapoor Date: Mon, 21 Sep 2026 14:52:07 +0530 Subject: [PATCH 04/10] Teach the aggregate list guard to see through state-gated aggregates test_members_are_state_gated failed on household_state_benefits because child_care_subsidies is itself an aggregate with no defined_for gate. An aggregate whose every summed member is state-gated cannot leak across states, so the guard now recurses through a member's adds list (parameter path or literal list, depth-capped at 3) and accepts it only when every addend is gated. Requested in review of #9503. --- .../tests/test_aggregate_list_integrity.py | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/policyengine_us/tests/test_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index e5694e6d7dd..c510e7161a6 100644 --- a/policyengine_us/tests/test_aggregate_list_integrity.py +++ b/policyengine_us/tests/test_aggregate_list_integrity.py @@ -82,23 +82,56 @@ def test_members_are_defined_variables(list_path, variables): ) +def has_state_gate(variables, member): + """True when a StateCode (or locality) gate appears in the defined_for chain.""" + gate = variables[member].defined_for + for _ in range(10): + if gate is None: + return False + if re.fullmatch(r"[A-Z]{2}", str(gate)): + return True # StateCode gate found + if gate in GEOGRAPHIC_GATES: + return True + gate = variables[gate].defined_for if gate in variables else None + return False + + +def aggregate_members(variables, member): + """The variables an aggregate member sums, or None if it is not an aggregate. + + `adds` is either a list of variable names or a dotted parameter path + (e.g. gov.hhs.ccdf.child_care_subsidy_programs). + """ + adds = getattr(variables[member], "adds", None) + if not adds: + return None + if isinstance(adds, str): + list_path = adds.replace(".", "/") + ".yaml" + if not (PARAMETERS / list_path).exists(): + return None + return members_of(list_path) + return set(adds) + + +def is_state_gated(variables, member, depth=0): + if has_state_gate(variables, member): + return True + # An aggregate whose every summed member is state-gated cannot leak one + # state's program into other states: each addend is zero outside its gate. + if depth < 3: + members = aggregate_members(variables, member) + if members is not None: + return all(is_state_gated(variables, m, depth + 1) for m in members) + return False + + @pytest.mark.parametrize("list_path", AGGREGATE_LISTS) def test_members_are_state_gated(list_path, variables): - ungated = [] - for member in sorted(members_of(list_path)): - gate = variables[member].defined_for - for _ in range(10): - if gate is None: - break - if re.fullmatch(r"[A-Z]{2}", str(gate)): - break # StateCode gate found - if gate in GEOGRAPHIC_GATES: - break - gate = variables[gate].defined_for if gate in variables else None - else: - gate = None - if gate is None: - ungated.append(member) + ungated = [ + member + for member in sorted(members_of(list_path)) + if not is_state_gated(variables, member) + ] assert not ungated, ( f"{list_path} members without a StateCode gate in their " f"defined_for chain (dc_ctc-style leak risk): {ungated}" From 4b0d12ffcf2bf201db3deb5880034b419ccec188 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 17:34:23 -0500 Subject: [PATCH 05/10] Pin Vermont's computed subsidy and state the household invariants Vermont pays the state rate whatever the provider charges, so a computed case now records that the payment above the bill reaches household benefits. The property tests state that no subsidy counts twice and that a dollar of subsidy adds a dollar to household benefits and net income. Co-Authored-By: Claude Opus 5.5 --- .../household/household_state_benefits.yaml | 33 +++++++++ ...child_care_subsidies_household_benefits.py | 73 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 policyengine_us/tests/test_child_care_subsidies_household_benefits.py diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index 5ac212d52c2..e0d354cc1bf 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -230,3 +230,36 @@ output: child_care_subsidies: 3_600 household_state_benefits: 3_600 + +# Vermont pays the state rate whatever the provider charges, from December 2023, +# so the subsidy can exceed the family's bill. The whole payment counts as a +# benefit here, while childcare_expenses stops at zero. +- name: Vermont's computed subsidy reaches household state benefits above the bill + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 52_875 + child: + age: 3 + is_tax_unit_dependent: true + vt_ccfap_provider_type: LICENSED_CENTER + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 10_000 + households: + household: + members: [parent, child] + state_code: VT + output: + # $439 weekly state rate less the $100 weekly family share, as in + # vt_ccfap.yaml Case 1: ($439 - $100) * 52 = $17,628. + child_care_subsidies: 17_628 + household_state_benefits: 17_628 + childcare_expenses: 0 diff --git a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py new file mode 100644 index 00000000000..3a6c3513049 --- /dev/null +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -0,0 +1,73 @@ +"""Invariants of child care subsidies in household benefits. + +household_state_benefits counts the child_care_subsidies aggregate, so +gov.hhs.ccdf.child_care_subsidy_programs is the one list of state programs. + +1. No state's own subsidy appears beside the aggregate in any year, so no + subsidy counts twice. +2. A dollar of subsidy adds a dollar to household benefits and to household net + income, in every state that has a program that year. +""" + +from pathlib import Path + +import pytest +import yaml +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +PARAMETERS = Path(__file__).parent.parent / "parameters" +STATE_BENEFITS = PARAMETERS / "gov/household/household_state_benefits.yaml" +PROGRAMS = PARAMETERS / "gov/hhs/ccdf/child_care_subsidy_programs.yaml" +TOLERANCE = 0.02 + + +def blocks(path): + return yaml.load(path.read_text(), Loader=yaml.BaseLoader)["values"] + + +def programs_in(year): + dated = [item for item in blocks(PROGRAMS).items() if item[0] <= f"{year}-12-31"] + return sorted(max(dated)[1]) + + +@pytest.mark.parametrize("instant", sorted(blocks(STATE_BENEFITS))) +def test_each_block_counts_only_the_aggregate(instant): + members = blocks(STATE_BENEFITS)[instant] + assert members.count("child_care_subsidies") == 1 + assert set(members) & set(programs_in(instant[:4])) == set() + + +def net_income_and_benefits(state, year, program, amount): + members = ["you"] + situation = { + "people": {"you": {"age": {year: 40}, "employment_income": {year: 500_000}}}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members, program: {year: amount}}}, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": members}}, + "households": {"household": {"members": members, "state_name": {year: state}}}, + } + simulation = Simulation(situation=situation) + return [ + float(simulation.calculate(name, year).sum()) + for name in ("household_net_income", "household_benefits") + ] + + +@settings(max_examples=30, deadline=None) +@given( + year=st.sampled_from([2023, 2024, 2025, 2026]), + index=st.integers(min_value=0, max_value=50), + amount=st.integers(min_value=1, max_value=30_000), +) +def test_a_dollar_of_subsidy_adds_a_dollar(year, index, amount): + programs = programs_in(year) + program = programs[index % len(programs)] + state = program[:2].upper() + without = net_income_and_benefits(state, year, program, 0) + with_subsidy = net_income_and_benefits(state, year, program, amount) + for before, after in zip(without, with_subsidy): + assert after - before == pytest.approx(amount, abs=TOLERANCE) From a2c0f50846e3550a0dd65c272681ae65ace0a2a1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 02:39:52 -0500 Subject: [PATCH 06/10] Stop pinning Vermont's child care expenses at zero Vermont's family still owes its share, so the zero that childcare_expenses returns isn't a result worth pinning. State why the household test is Python and halve its examples. Co-Authored-By: Claude Opus 5.5 --- .../household/income/household/household_state_benefits.yaml | 3 +-- .../tests/test_child_care_subsidies_household_benefits.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index e0d354cc1bf..29b2ef65ed4 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -233,7 +233,7 @@ # Vermont pays the state rate whatever the provider charges, from December 2023, # so the subsidy can exceed the family's bill. The whole payment counts as a -# benefit here, while childcare_expenses stops at zero. +# benefit here. - name: Vermont's computed subsidy reaches household state benefits above the bill period: 2025 absolute_error_margin: 0.01 @@ -262,4 +262,3 @@ # vt_ccfap.yaml Case 1: ($439 - $100) * 52 = $17,628. child_care_subsidies: 17_628 household_state_benefits: 17_628 - childcare_expenses: 0 diff --git a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py index 3a6c3513049..dcebffa4fc3 100644 --- a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -7,6 +7,9 @@ subsidy counts twice. 2. A dollar of subsidy adds a dollar to household benefits and to household net income, in every state that has a program that year. + +The YAML cases pin single families. These read every dated block of the list and +draw from every program and year, which YAML can't express. """ from pathlib import Path @@ -57,7 +60,7 @@ def net_income_and_benefits(state, year, program, amount): ] -@settings(max_examples=30, deadline=None) +@settings(max_examples=15, deadline=None) @given( year=st.sampled_from([2023, 2024, 2025, 2026]), index=st.integers(min_value=0, max_value=50), From 083d5a906baf44ddff75d531b49f843444bf5257 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 03:17:29 -0500 Subject: [PATCH 07/10] Say what the household subsidy property test samples Co-Authored-By: Claude Opus 5.5 --- .../household/income/household/household_state_benefits.yaml | 3 ++- .../tests/test_child_care_subsidies_household_benefits.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index 29b2ef65ed4..cf89b502001 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -233,7 +233,8 @@ # Vermont pays the state rate whatever the provider charges, from December 2023, # so the subsidy can exceed the family's bill. The whole payment counts as a -# benefit here. +# benefit here. childcare_expenses can't show the family's own share in this +# case, because it subtracts the subsidy from the bill and stops at zero. - name: Vermont's computed subsidy reaches household state benefits above the bill period: 2025 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py index dcebffa4fc3..e1aeebfc384 100644 --- a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -6,10 +6,10 @@ 1. No state's own subsidy appears beside the aggregate in any year, so no subsidy counts twice. 2. A dollar of subsidy adds a dollar to household benefits and to household net - income, in every state that has a program that year. + income. Each run samples programs and years. The YAML cases pin single families. These read every dated block of the list and -draw from every program and year, which YAML can't express. +generate programs, years and amounts, which YAML can't express. """ from pathlib import Path From 94534943141314d7f000b2bac774952aa0e27b10 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 19:10:32 -0500 Subject: [PATCH 08/10] Count child care subsidies in household benefits up to the family's bill Vermont pays its state rate to the provider whatever the provider charges, so its subsidy can exceed the family's bill, and the amount above the bill never reaches the family. household_child_care_subsidies caps child_care_subsidies at the pre-subsidy bill (whole subsidy with no bill recorded), and every household_state_benefits block counts it in place of the uncapped aggregate. child_care_subsidies stays the full payment. Tests: Vermont cases at bills below, at and above the payment, with no bill and with the bill set on the child; a Connecticut case; a Hypothesis property of the cap; and a check that the 2025+ CCDF blocks list every state program. Co-Authored-By: Claude Opus 5.5 --- changelog.d/9405.fixed.md | 2 +- .../household/household_state_benefits.yaml | 8 +- .../household/household_state_benefits.yaml | 151 +++++++++++++++++- .../tests/test_aggregate_list_integrity.py | 8 +- ...child_care_subsidies_household_benefits.py | 75 ++++++++- .../ccdf/household_child_care_subsidies.py | 23 +++ 6 files changed, 247 insertions(+), 20 deletions(-) create mode 100644 policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py diff --git a/changelog.d/9405.fixed.md b/changelog.d/9405.fixed.md index caa0d1d4e29..0028b86b0a3 100644 --- a/changelog.d/9405.fixed.md +++ b/changelog.d/9405.fixed.md @@ -1 +1 @@ -Count every implemented state child care subsidy in household benefits. +Count every state child care subsidy in the CCDF program list in household benefits, up to the family's bill. diff --git a/policyengine_us/parameters/gov/household/household_state_benefits.yaml b/policyengine_us/parameters/gov/household/household_state_benefits.yaml index b73855d9359..bcc51449cd4 100644 --- a/policyengine_us/parameters/gov/household/household_state_benefits.yaml +++ b/policyengine_us/parameters/gov/household/household_state_benefits.yaml @@ -2,7 +2,7 @@ description: The government counts these sources as benefits paid by state agenc values: 2023-01-01: # Child care subsidies administered by state agencies - - child_care_subsidies + - household_child_care_subsidies # Delaware benefits - de_ssp # Indiana benefits @@ -108,7 +108,7 @@ values: - ny_ssp 2024-01-01: # Child care subsidies administered by state agencies - - child_care_subsidies + - household_child_care_subsidies # Delaware benefits - de_ssp # Indiana benefits @@ -214,7 +214,7 @@ values: - ny_ssp 2025-01-01: # Child care subsidies administered by state agencies - - child_care_subsidies + - household_child_care_subsidies # Delaware benefits - de_ssp # Indiana benefits @@ -320,7 +320,7 @@ values: - ny_ssp 2026-01-01: # Child care subsidies administered by state agencies - - child_care_subsidies + - household_child_care_subsidies # Delaware benefits - de_ssp # Indiana benefits diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index cf89b502001..63624dc45d5 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -231,11 +231,14 @@ child_care_subsidies: 3_600 household_state_benefits: 3_600 -# Vermont pays the state rate whatever the provider charges, from December 2023, -# so the subsidy can exceed the family's bill. The whole payment counts as a -# benefit here. childcare_expenses can't show the family's own share in this -# case, because it subtracts the subsidy from the bill and stops at zero. -- name: Vermont's computed subsidy reaches household state benefits above the bill +# Vermont pays the state rate to the provider whatever the provider charges, +# from December 2023, so the payment can exceed the family's bill. Household +# benefits count it up to the bill. childcare_expenses can't show the family's +# own share in this case, because it subtracts the subsidy from the bill and +# stops at zero. +# In each case the subsidy is the $439 weekly state rate less the $100 weekly +# family share, as in vt_ccfap.yaml Case 1: ($439 - $100) * 52 = $17,628. +- name: Vermont's subsidy above a 10,000 bill counts up to the bill period: 2025 absolute_error_margin: 0.01 input: @@ -259,7 +262,141 @@ members: [parent, child] state_code: VT output: - # $439 weekly state rate less the $100 weekly family share, as in - # vt_ccfap.yaml Case 1: ($439 - $100) * 52 = $17,628. child_care_subsidies: 17_628 + household_child_care_subsidies: 10_000 + household_state_benefits: 10_000 + +- name: Vermont's subsidy above a 1,000 bill counts up to the bill + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 52_875 + child: + age: 3 + is_tax_unit_dependent: true + vt_ccfap_provider_type: LICENSED_CENTER + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 1_000 + households: + household: + members: [parent, child] + state_code: VT + output: + child_care_subsidies: 17_628 + household_child_care_subsidies: 1_000 + household_state_benefits: 1_000 + +- name: Vermont's subsidy below a 20,000 bill counts in full + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 52_875 + child: + age: 3 + is_tax_unit_dependent: true + vt_ccfap_provider_type: LICENSED_CENTER + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 20_000 + households: + household: + members: [parent, child] + state_code: VT + output: + child_care_subsidies: 17_628 + household_child_care_subsidies: 17_628 + household_state_benefits: 17_628 + +- name: Vermont's subsidy counts in full with no bill recorded + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 52_875 + child: + age: 3 + is_tax_unit_dependent: true + vt_ccfap_provider_type: LICENSED_CENTER + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + households: + household: + members: [parent, child] + state_code: VT + output: + child_care_subsidies: 17_628 + household_child_care_subsidies: 17_628 household_state_benefits: 17_628 + +# The bill reads the person-level input the way childcare_expenses does, so a +# bill set on the child caps the subsidy just as the SPM unit input does. +- name: Vermont's subsidy counts up to a bill set on the child + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 52_875 + child: + age: 3 + is_tax_unit_dependent: true + vt_ccfap_provider_type: LICENSED_CENTER + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + pre_subsidy_childcare_expenses: 10_000 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + households: + household: + members: [parent, child] + state_code: VT + output: + child_care_subsidies: 17_628 + household_child_care_subsidies: 10_000 + household_state_benefits: 10_000 + +# A subsidy below the bill counts in full in any state. +- name: Connecticut's subsidy below the bill counts in full + period: 2025 + input: + people: + parent: + age: 30 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + ct_child_care_subsidies: 5_000 + spm_unit_pre_subsidy_childcare_expenses: 12_000 + households: + household: + members: [parent, child] + state_code: CT + output: + child_care_subsidies: 5_000 + household_child_care_subsidies: 5_000 diff --git a/policyengine_us/tests/test_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index dcdd9d83009..bc6a1a2763e 100644 --- a/policyengine_us/tests/test_aggregate_list_integrity.py +++ b/policyengine_us/tests/test_aggregate_list_integrity.py @@ -38,6 +38,9 @@ # defined_for (locality booleans). GEOGRAPHIC_GATES = {"in_la", "in_nyc", "in_riv"} +# Variables that cap an aggregate and add nothing of their own. +CAPPED_AGGREGATES = {"household_child_care_subsidies": {"child_care_subsidies"}} + # Members intentionally removed in a year block, keyed by (list, block date). # Add an entry here when a credit genuinely ends; do not silently drop # members when copying a block forward. @@ -106,8 +109,11 @@ def aggregate_members(variables, member): """The variables an aggregate member sums, or None if it is not an aggregate. `adds` is either a list of variable names or a dotted parameter path - (e.g. gov.hhs.ccdf.child_care_subsidy_programs). + (e.g. gov.hhs.ccdf.child_care_subsidy_programs). A variable in + CAPPED_AGGREGATES caps the aggregates it names, so those are its members. """ + if member in CAPPED_AGGREGATES: + return CAPPED_AGGREGATES[member] adds = getattr(variables[member], "adds", None) if not adds: return None diff --git a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py index e1aeebfc384..67c20491f2a 100644 --- a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -1,12 +1,15 @@ """Invariants of child care subsidies in household benefits. -household_state_benefits counts the child_care_subsidies aggregate, so -gov.hhs.ccdf.child_care_subsidy_programs is the one list of state programs. +household_state_benefits counts household_child_care_subsidies, which caps the +child_care_subsidies aggregate, so gov.hhs.ccdf.child_care_subsidy_programs is +the one list of state programs. -1. No state's own subsidy appears beside the aggregate in any year, so no - subsidy counts twice. -2. A dollar of subsidy adds a dollar to household benefits and to household net - income. Each run samples programs and years. +1. No state's own subsidy, and not the uncapped aggregate, appears beside + household_child_care_subsidies in any year, so no subsidy counts twice. +2. With no bill recorded, a dollar of subsidy adds a dollar to household + benefits and to household net income. Each run samples programs and years. +3. Household benefits count the subsidy up to the family's pre-subsidy bill. A + payment above the bill goes to the provider, not the family. The YAML cases pin single families. These read every dated block of the list and generate programs, years and amounts, which YAML can't express. @@ -39,10 +42,24 @@ def programs_in(year): @pytest.mark.parametrize("instant", sorted(blocks(STATE_BENEFITS))) def test_each_block_counts_only_the_aggregate(instant): members = blocks(STATE_BENEFITS)[instant] - assert members.count("child_care_subsidies") == 1 + assert members.count("household_child_care_subsidies") == 1 + assert "child_care_subsidies" not in members assert set(members) & set(programs_in(instant[:4])) == set() +def test_latest_blocks_list_every_state_program(): + defined = { + path.stem + for path in (PARAMETERS.parent / "variables/gov/states").rglob( + "*_child_care_subsidies.py" + ) + } + assert len(defined) == 51 + for start, programs in blocks(PROGRAMS).items(): + if start >= "2025": + assert set(programs) == defined, start + + def net_income_and_benefits(state, year, program, amount): members = ["you"] situation = { @@ -74,3 +91,47 @@ def test_a_dollar_of_subsidy_adds_a_dollar(year, index, amount): with_subsidy = net_income_and_benefits(state, year, program, amount) for before, after in zip(without, with_subsidy): assert after - before == pytest.approx(amount, abs=TOLERANCE) + + +def counted_subsidy(state, year, program, amount, bill): + members = ["parent", "child"] + spm_unit = { + "members": members, + program: {year: amount}, + "spm_unit_pre_subsidy_childcare_expenses": {year: bill}, + } + situation = { + "people": {"parent": {"age": {year: 30}}, "child": {"age": {year: 3}}}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": spm_unit}, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": ["parent"]}}, + "households": {"household": {"members": members, "state_name": {year: state}}}, + } + simulation = Simulation(situation=situation) + return float(simulation.calculate("household_child_care_subsidies", year)[0]) + + +@settings(max_examples=15, deadline=None) +@given( + year=st.sampled_from([2023, 2024, 2025, 2026]), + index=st.integers(min_value=0, max_value=50), + amount=st.integers(min_value=0, max_value=30_000), + bill=st.integers(min_value=0, max_value=30_000), + extra=st.integers(min_value=1, max_value=30_000), +) +def test_household_benefits_count_the_subsidy_up_to_the_bill( + year, index, amount, bill, extra +): + programs = programs_in(year) + program = programs[index % len(programs)] + state = program[:2].upper() + counted = counted_subsidy(state, year, program, amount, bill) + assert counted <= amount + TOLERANCE + if bill > 0: + assert counted <= bill + TOLERANCE + if bill == 0 or amount <= bill: + assert counted == pytest.approx(amount, abs=TOLERANCE) + if bill > 0: + bigger = counted_subsidy(state, year, program, amount, bill + extra) + assert bigger >= counted - TOLERANCE diff --git a/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py b/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py new file mode 100644 index 00000000000..91237b99f22 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class household_child_care_subsidies(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + label = "Child care subsidies counted in household benefits" + documentation = ( + "Child care subsidies up to the family's pre-subsidy bill. A payment " + "above the bill goes to the provider, not the family. With no bill " + "recorded, the whole subsidy counts." + ) + unit = USD + reference = ( + # PDF pages 1-2 + "https://outside.vermont.gov/dept/DCF/Shared%20Documents/CDD/CCFAP/CCFAP-Understanding-Payments.pdf#page=1", + ) + + def formula(spm_unit, period, parameters): + subsidies = spm_unit("child_care_subsidies", period) + bill = add(spm_unit, period, ["pre_subsidy_childcare_expenses"]) + return where(bill > 0, min_(subsidies, bill), subsidies) From 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 23:20:37 -0500 Subject: [PATCH 09/10] Count after-school costs in the child care subsidy cap's bill Colorado's CCAP subsidy pays for pre-subsidy child care and after-school expenses (co_ccap_subsidy), so a cap at the child care bill alone cut part of it and lowered household_net_income below main for families with after-school costs. Add a Colorado YAML case and pin the cap property's zero, equal and one-dollar-over boundaries. Co-Authored-By: Claude Opus 5.5 --- changelog.d/9405.fixed.md | 2 +- .../household/household_state_benefits.yaml | 22 +++++++++++++++++++ ...child_care_subsidies_household_benefits.py | 12 +++++++--- .../ccdf/household_child_care_subsidies.py | 14 ++++++++---- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/changelog.d/9405.fixed.md b/changelog.d/9405.fixed.md index 0028b86b0a3..7645f26bfb4 100644 --- a/changelog.d/9405.fixed.md +++ b/changelog.d/9405.fixed.md @@ -1 +1 @@ -Count every state child care subsidy in the CCDF program list in household benefits, up to the family's bill. +Count every state child care subsidy in the CCDF program list in household benefits, up to the family's child care and after-school bill. diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index 63624dc45d5..f0d6d9190d4 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -400,3 +400,25 @@ output: child_care_subsidies: 5_000 household_child_care_subsidies: 5_000 + +# Colorado's subsidy pays for after-school care too, so the cap counts that bill. +- name: Colorado's subsidy for child care and after-school care counts in full + period: 2025 + input: + people: + parent: + age: 30 + child: + age: 3 + after_school_expenses: 6_000 + spm_units: + spm_unit: + members: [parent, child] + co_child_care_subsidies: 12_000 + spm_unit_pre_subsidy_childcare_expenses: 6_000 + households: + household: + members: [parent, child] + state_code: CO + output: + household_child_care_subsidies: 12_000 diff --git a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py index 67c20491f2a..4a02353e4a8 100644 --- a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -8,8 +8,9 @@ household_child_care_subsidies in any year, so no subsidy counts twice. 2. With no bill recorded, a dollar of subsidy adds a dollar to household benefits and to household net income. Each run samples programs and years. -3. Household benefits count the subsidy up to the family's pre-subsidy bill. A - payment above the bill goes to the provider, not the family. +3. Household benefits count the subsidy up to the family's pre-subsidy bill for + child care and after-school care. A payment above the bill goes to the + provider, not the family. The YAML cases pin single families. These read every dated block of the list and generate programs, years and amounts, which YAML can't express. @@ -19,7 +20,7 @@ import pytest import yaml -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis import strategies as st from policyengine_us import Simulation @@ -113,6 +114,11 @@ def counted_subsidy(state, year, program, amount, bill): @settings(max_examples=15, deadline=None) +# Pin the boundaries the random draws may miss: no bill, a subsidy equal to +# the bill, and a subsidy a dollar above it. +@example(year=2025, index=0, amount=5_000, bill=0, extra=1) +@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1) +@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1) @given( year=st.sampled_from([2023, 2024, 2025, 2026]), index=st.integers(min_value=0, max_value=50), diff --git a/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py b/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py index 91237b99f22..5af36c291b0 100644 --- a/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py +++ b/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py @@ -7,9 +7,10 @@ class household_child_care_subsidies(Variable): definition_period = YEAR label = "Child care subsidies counted in household benefits" documentation = ( - "Child care subsidies up to the family's pre-subsidy bill. A payment " - "above the bill goes to the provider, not the family. With no bill " - "recorded, the whole subsidy counts." + "Child care subsidies up to the family's pre-subsidy bill for child " + "care and after-school care. A payment above the bill goes to the " + "provider, not the family. With no bill recorded, the whole subsidy " + "counts." ) unit = USD reference = ( @@ -19,5 +20,10 @@ class household_child_care_subsidies(Variable): def formula(spm_unit, period, parameters): subsidies = spm_unit("child_care_subsidies", period) - bill = add(spm_unit, period, ["pre_subsidy_childcare_expenses"]) + # Colorado's subsidy pays for after-school care too (co_ccap_subsidy). + bill = add( + spm_unit, + period, + ["pre_subsidy_childcare_expenses", "after_school_expenses"], + ) return where(bill > 0, min_(subsidies, bill), subsidies) From bbcd0b527c96e7e95a38eb5b11f0033e1d88ffb4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 3 Nov 2026 11:04:42 -0500 Subject: [PATCH 10/10] Test after-school bills in the child care subsidy cap The cap property draws the after-school share of the bill, and two examples set it to half and to all. A Vermont case checks that, with no child care bill, the model counts the payment up to the after-school bill. Co-Authored-By: Claude Opus 5.5 --- .../household/household_state_benefits.yaml | 30 +++++++++++++++++++ ...child_care_subsidies_household_benefits.py | 26 ++++++++++------ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml index f0d6d9190d4..e38c3bf903c 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_state_benefits.yaml @@ -379,6 +379,36 @@ household_child_care_subsidies: 10_000 household_state_benefits: 10_000 +# The model adds after-school costs to the bill in every state. With no child +# care bill, the model counts Vermont's payment up to the after-school bill. +- name: Vermont's subsidy counts up to an after-school bill with no child care bill + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 52_875 + child: + age: 3 + is_tax_unit_dependent: true + vt_ccfap_provider_type: LICENSED_CENTER + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + after_school_expenses: 1_000 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + households: + household: + members: [parent, child] + state_code: VT + output: + child_care_subsidies: 17_628 + household_child_care_subsidies: 1_000 + household_state_benefits: 1_000 + # A subsidy below the bill counts in full in any state. - name: Connecticut's subsidy below the bill counts in full period: 2025 diff --git a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py index 4a02353e4a8..e2742f0a3e6 100644 --- a/policyengine_us/tests/test_child_care_subsidies_household_benefits.py +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -94,15 +94,16 @@ def test_a_dollar_of_subsidy_adds_a_dollar(year, index, amount): assert after - before == pytest.approx(amount, abs=TOLERANCE) -def counted_subsidy(state, year, program, amount, bill): +def counted_subsidy(state, year, program, amount, bill, after_school=0): members = ["parent", "child"] spm_unit = { "members": members, program: {year: amount}, - "spm_unit_pre_subsidy_childcare_expenses": {year: bill}, + "spm_unit_pre_subsidy_childcare_expenses": {year: bill - after_school}, } + child = {"age": {year: 3}, "after_school_expenses": {year: after_school}} situation = { - "people": {"parent": {"age": {year: 30}}, "child": {"age": {year: 3}}}, + "people": {"parent": {"age": {year: 30}}, "child": child}, "tax_units": {"tax_unit": {"members": members}}, "spm_units": {"spm_unit": spm_unit}, "families": {"family": {"members": members}}, @@ -116,28 +117,35 @@ def counted_subsidy(state, year, program, amount, bill): @settings(max_examples=15, deadline=None) # Pin the boundaries the random draws may miss: no bill, a subsidy equal to # the bill, and a subsidy a dollar above it. -@example(year=2025, index=0, amount=5_000, bill=0, extra=1) -@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1) -@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1) +@example(year=2025, index=0, amount=5_000, bill=0, extra=1, after_school_share=0) +@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1, after_school_share=0) +@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1, after_school_share=0) +# After-school care makes up half the bill, and then all of it. +@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1, after_school_share=50) +@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1, after_school_share=100) @given( year=st.sampled_from([2023, 2024, 2025, 2026]), index=st.integers(min_value=0, max_value=50), amount=st.integers(min_value=0, max_value=30_000), bill=st.integers(min_value=0, max_value=30_000), extra=st.integers(min_value=1, max_value=30_000), + after_school_share=st.integers(min_value=0, max_value=100), ) def test_household_benefits_count_the_subsidy_up_to_the_bill( - year, index, amount, bill, extra + year, index, amount, bill, extra, after_school_share ): programs = programs_in(year) program = programs[index % len(programs)] state = program[:2].upper() - counted = counted_subsidy(state, year, program, amount, bill) + after_school = bill * after_school_share // 100 + counted = counted_subsidy(state, year, program, amount, bill, after_school) assert counted <= amount + TOLERANCE if bill > 0: assert counted <= bill + TOLERANCE if bill == 0 or amount <= bill: assert counted == pytest.approx(amount, abs=TOLERANCE) if bill > 0: - bigger = counted_subsidy(state, year, program, amount, bill + extra) + bigger = counted_subsidy( + state, year, program, amount, bill + extra, after_school + ) assert bigger >= counted - TOLERANCE