diff --git a/changelog.d/9405.fixed.md b/changelog.d/9405.fixed.md new file mode 100644 index 00000000000..7645f26bfb4 --- /dev/null +++ b/changelog.d/9405.fixed.md @@ -0,0 +1 @@ +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/parameters/gov/household/household_state_benefits.yaml b/policyengine_us/parameters/gov/household/household_state_benefits.yaml index 3dc79dda9c9..bcc51449cd4 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 + - household_child_care_subsidies # Delaware benefits - de_ssp - - de_child_care_subsidies # Indiana benefits - in_ssp # Connecticut benefits @@ -14,9 +15,7 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies # Montana benefits - - mt_child_care_subsidies - mt_ssp # Massachusetts benefits - ma_state_supplement @@ -29,9 +28,7 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -43,33 +40,22 @@ 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_oss - - nv_child_care_subsidies # Oklahoma benefits - - ok_child_care_subsidies - ok_ssp # Oregon benefits - or_ssp - # South Carolina benefits - - sc_child_care_subsidies # New Hampshire benefits - nh_ssp # New Jersey benefits @@ -95,7 +81,6 @@ values: # Vermont benefits - vt_aabd # Virginia benefits - - va_child_care_subsidies - va_ag # Wisconsin benefits - wi_ssp @@ -119,18 +104,15 @@ values: - nj_property_tax_relief # Louisiana benefits - la_oss - # Washington benefits - - wa_child_care_subsidies # New York benefits - ny_ssp - - ny_child_care_subsidies 2024-01-01: + # Child care subsidies administered by state agencies + - household_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 @@ -139,9 +121,7 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies # Montana benefits - - mt_child_care_subsidies - mt_ssp # Massachusetts benefits - ma_state_supplement @@ -154,9 +134,7 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -168,32 +146,21 @@ 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_oss - - nv_child_care_subsidies # New Hampshire benefits - nh_ssp # Oklahoma benefits - - ok_child_care_subsidies - ok_ssp # Oregon benefits - or_ssp @@ -218,19 +185,13 @@ values: # Vermont benefits - vt_aabd # Virginia benefits - - va_child_care_subsidies - va_ag - # West Virginia benefits - - wv_child_care_subsidies # Wisconsin benefits - wi_ssp # Wyoming benefits - wy_ssp # North Carolina benefits - - nc_child_care_subsidies - nc_sa - # South Carolina benefits - - sc_child_care_subsidies # Massachusetts benefits - ma_eaedc # DC benefits @@ -249,22 +210,15 @@ values: - nj_property_tax_relief # 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_ssp - - ny_child_care_subsidies 2025-01-01: + # Child care subsidies administered by state agencies + - household_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 @@ -273,9 +227,7 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies # Montana benefits - - mt_child_care_subsidies - mt_ssp # Massachusetts benefits - ma_state_supplement @@ -288,9 +240,7 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -302,32 +252,21 @@ 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_oss - - nv_child_care_subsidies # New Hampshire benefits - nh_ssp # Oklahoma benefits - - ok_child_care_subsidies - ok_ssp # Oregon benefits - or_ssp @@ -352,24 +291,17 @@ values: # Vermont benefits - vt_aabd # Virginia benefits - - va_child_care_subsidies - va_ag - # West Virginia benefits - - wv_child_care_subsidies # Wisconsin benefits - wi_ssp # Wyoming benefits - wy_ssp # North Carolina benefits - - nc_child_care_subsidies - nc_sa - # South Carolina benefits - - sc_child_care_subsidies # Massachusetts benefits - ma_eaedc # DC benefits - dc_ossp - - dc_child_care_subsidies # Iowa benefits - ia_ssa # Kansas benefits @@ -384,22 +316,15 @@ values: - nj_property_tax_relief # 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_ssp - - ny_child_care_subsidies 2026-01-01: + # Child care subsidies administered by state agencies + - household_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 @@ -408,9 +333,7 @@ values: - me_ssp # Missouri benefits - mo_ssp - - mo_child_care_subsidies # Montana benefits - - mt_child_care_subsidies - mt_ssp # Massachusetts benefits - ma_state_supplement @@ -423,9 +346,7 @@ values: # Colorado benefits - co_state_supplement - co_oap - - co_child_care_subsidies # California benefits - - ca_child_care_subsidies - ca_cvrp - ca_care - ca_fera @@ -437,32 +358,21 @@ 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_oss - - nv_child_care_subsidies # New Hampshire benefits - nh_ssp # Oklahoma benefits - - ok_child_care_subsidies - ok_ssp # Oregon benefits - or_ssp @@ -487,24 +397,17 @@ values: # Vermont benefits - vt_aabd # Virginia benefits - - va_child_care_subsidies - va_ag - # West Virginia benefits - - wv_child_care_subsidies # Wisconsin benefits - wi_ssp # Wyoming benefits - wy_ssp # North Carolina benefits - - nc_child_care_subsidies - nc_sa - # South Carolina benefits - - sc_child_care_subsidies # Massachusetts benefits - ma_eaedc # DC benefits - dc_ossp - - dc_child_care_subsidies # Iowa benefits - ia_ssa # Kansas benefits @@ -517,17 +420,10 @@ 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_ssp - - ny_child_care_subsidies metadata: unit: list 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 0c899ce92dc..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 @@ -189,3 +189,266 @@ 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 + +# 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: + 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: + 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 + +# 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 + 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 + +# 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_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index 22287ba0703..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. @@ -88,23 +91,59 @@ 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). 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 + 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}" 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..e2742f0a3e6 --- /dev/null +++ b/policyengine_us/tests/test_child_care_subsidies_household_benefits.py @@ -0,0 +1,151 @@ +"""Invariants of child care subsidies in household benefits. + +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, 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 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. +""" + +from pathlib import Path + +import pytest +import yaml +from hypothesis import example, 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("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 = { + "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=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=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) + + +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 - after_school}, + } + child = {"age": {year: 3}, "after_school_expenses": {year: after_school}} + situation = { + "people": {"parent": {"age": {year: 30}}, "child": child}, + "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) +# 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, 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, after_school_share +): + programs = programs_in(year) + program = programs[index % len(programs)] + state = program[:2].upper() + 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, after_school + ) + 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..5af36c291b0 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py @@ -0,0 +1,29 @@ +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 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 = ( + # 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) + # 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)