diff --git a/changelog.d/nh-ccap-cost-share-weeks.fixed.md b/changelog.d/nh-ccap-cost-share-weeks.fixed.md new file mode 100644 index 00000000000..bbde08a0abe --- /dev/null +++ b/changelog.d/nh-ccap-cost-share-weeks.fixed.md @@ -0,0 +1 @@ +Divide monthly income by 4.33 weeks in New Hampshire's child care cost share, as He-C 6910.18(b) directs. diff --git a/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/income_share_rate.yaml b/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/income_share_rate.yaml index 6ac34776f7f..a8d309329eb 100644 --- a/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/income_share_rate.yaml +++ b/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/income_share_rate.yaml @@ -18,7 +18,7 @@ metadata: label: New Hampshire CCAP income share rate by FPG ratio reference: - title: He-C 6910.18, Table 6910.19 - href: https://www.law.cornell.edu/regulations/new-hampshire/N-H-Admin-Code-SS-He-C-6910.18 + href: https://gc.nh.gov/rules/state_agencies/he-c6900.html - title: SR 24-08, Cost Share and Steps (January 2024) href: https://www.dhhs.nh.gov/sr_htm/html/sr_24-08_dated_01_24.htm - title: New Hampshire CCDF State Plan 2025-2027, Section 3.2.1 diff --git a/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weekly_flat_amount.yaml b/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weekly_flat_amount.yaml index 18e3834efd2..78f6679f543 100644 --- a/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weekly_flat_amount.yaml +++ b/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weekly_flat_amount.yaml @@ -22,7 +22,7 @@ metadata: label: New Hampshire CCAP weekly flat cost share amount by FPG ratio reference: - title: He-C 6910.18, Table 6910.19 - href: https://www.law.cornell.edu/regulations/new-hampshire/N-H-Admin-Code-SS-He-C-6910.18 + href: https://gc.nh.gov/rules/state_agencies/he-c6900.html - title: SR 24-08, Cost Share and Steps (January 2024) href: https://www.dhhs.nh.gov/sr_htm/html/sr_24-08_dated_01_24.htm - title: New Hampshire CCDF State Plan 2025-2027, Section 3.2.1 diff --git a/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weeks_per_month.yaml b/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weeks_per_month.yaml new file mode 100644 index 00000000000..1dd2cecd45d --- /dev/null +++ b/policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weeks_per_month.yaml @@ -0,0 +1,14 @@ +description: New Hampshire divides monthly income by this number of weeks to calculate the weekly family cost share under the Child Care Scholarship Program. + +values: + 2020-07-01: 4.33 + +metadata: + unit: week + period: month + label: New Hampshire CCAP weeks per month for the family cost share + reference: + - title: He-C 6910.18(b) + href: https://gc.nh.gov/rules/state_agencies/he-c6900.html + - title: He-C 6910.18(d) (effective July 1, 2020) + href: https://www.law.cornell.edu/regulations/new-hampshire/N-H-Admin-Code-SS-He-C-6910.18 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/integration.yaml index e7b1840d03d..0d311fbc196 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/integration.yaml @@ -100,7 +100,7 @@ weekly_hours_worked_before_lsr: 40 employment_income: 60_000 # $60,000/yr = $5,000/mo > $3,064.75 (138% FPG for 3) - # Step 3: 7% * $5,000 / 4.33 = $80.77/week + # Step 3: 7% * $5,000 / 4.33 = $80.83/week person2: age: 35 weekly_hours_worked_before_lsr: 40 @@ -129,8 +129,8 @@ nh_ccap_income_eligible: true nh_ccap_eligible: true - # Cost share: 7% * $5,000 / (52/12) = $350 / 4.3333 = $80.77/week - nh_ccap_cost_share: 80.77 + # Cost share: 7% * $5,000 / 4.33 = $350 / 4.33 = $80.83/week + nh_ccap_cost_share: 80.83 - name: Case 4, two eligible children, cost share split equally. period: 2025-01 @@ -379,3 +379,44 @@ nh_ccap_income_eligible: false nh_ccap_eligible: false nh_ccap: 0 + +- name: Case 10, positive benefit end-to-end at step 3, the cost share divides by 4.33 weeks. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 28 + weekly_hours_worked_before_lsr: 40 + employment_income: 48_000 + # $48,000/yr = $4,000/mo, above 138% of the 2024 poverty guideline for 2 ($2,350.60), + # which He-C 6910.18(e) applies until July 2025, and of the 2025 guideline ($2,432.25), + # which the model applies from January. Below 85% SMI for 2 ($7,060.37). + person2: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + nh_ccap_provider_type: LICENSED_CENTER + pre_subsidy_childcare_expenses: 18_720 + # $18,720/yr = $360/week + spm_units: + spm_unit: + members: [person1, person2] + + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NH + output: + nh_ccap_eligible: true + # Cost share: 7% * $4,000 / 4.33 = $280 / 4.33 = $64.67/week + nh_ccap_cost_share: 64.67 + # Preschool, full-time, licensed center: $280/week + # min($360/wk charge, $280 rate) = $280 + # Weekly payment: $280 - $280 / 4.33 = $215.3349 + # Monthly: $215.3349 * (52/12) = $933.12 + nh_ccap: 933.12 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.yaml index 5e2c728ad9d..772115fcc17 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.yaml @@ -5,7 +5,7 @@ # 2025 FPG family of 3: 15,650 + 5,500*2 = $26,650/year = $2,220.83/month # Step 1: <= 100% FPG ($2,220.83/mo) → $0/week # Step 2: > 100% to <= 138% FPG ($3,064.75/mo) → $5/week -# Step 3: > 138% FPG → 7% * monthly income / (52/12) +# Step 3: > 138% FPG → 7% * monthly income / 4.33 # # 2025 FPG family of 2: 15,650 + 5,500 = $21,150/year = $1,762.50/month # 2025 FPG family of 1: $15,650/year = $1,304.17/month @@ -66,7 +66,7 @@ age: 30 employment_income: 53_304 # $53,304/yr = $4,442/mo > $3,064.75 (138% FPG) - # Step 3: 7% * $4,442 / (52/12) = $71.77/week + # Step 3: 7% * $4,442 / 4.33 = $71.81/week person2: age: 5 person3: @@ -79,8 +79,8 @@ members: [person1, person2, person3] state_code: NH output: - # $4,442 * 0.07 / (52/12) = $310.94 / 4.3333 = $71.76 - nh_ccap_cost_share: 71.76 + # $4,442 * 0.07 / 4.33 = $310.94 / 4.33 = $71.81 + nh_ccap_cost_share: 71.81 - name: Case 4, income exactly at 100% FPG boundary, step 1. period: 2025-01 @@ -175,8 +175,8 @@ state_code: NH output: # $2,433.33 * 0.07 = $170.33 - # $170.33 / (52/12) = $170.33 / 4.3333 = $39.31 - nh_ccap_cost_share: 39.31 + # $170.33 / 4.33 = $39.34 + nh_ccap_cost_share: 39.34 - name: Case 8, zero income, step 1, $0 cost share. period: 2025-01 diff --git a/policyengine_us/tests/test_nh_ccap_cost_share_properties.py b/policyengine_us/tests/test_nh_ccap_cost_share_properties.py new file mode 100644 index 00000000000..4be7fd1e142 --- /dev/null +++ b/policyengine_us/tests/test_nh_ccap_cost_share_properties.py @@ -0,0 +1,110 @@ +"""Invariants of New Hampshire's child care family cost share, He-C 6910.18. + +1. Step 1: a family at or below 100% of the poverty guidelines owes nothing. +2. Step 2: a family above 100% and at or below 138% owes $5 a week. +3. Step 3: a family above 138% owes 7% of monthly income divided by 4.33 weeks, + and within the step the share rises with income. +4. For a family with one eligible child and no disability supplement, the + monthly payment equals 52/12 weeks of the lesser of the charge and the standard + rate, less the cost share, floored at zero. + +rule_cost_share restates the rule's table on its own, so the variable and the +table must agree. The YAML cases pin single families; these draw incomes and +charges across the steps. +""" + +from hypothesis import assume, example, given, settings +from hypothesis import strategies as st +import pytest + +from policyengine_us import Simulation + +YEAR = 2025 +# He-C 6910.18(e) applies each year's poverty guidelines from the following July, +# so July 2025 uses the 2025 guideline for a family of two. +MONTH = "2025-07" +FPG = 21_150 +WEEKLY_RATE = 280 # Preschool, full time, licensed center. +TOLERANCE = 0.01 + + +def rule_cost_share(annual_income): + if annual_income <= FPG: + return 0 + if annual_income * 100 <= FPG * 138: + return 5 + return annual_income / 12 * 0.07 / 4.33 + + +def outside_boundary_slivers(annual_income): + # The parameters place the steps at 1.0001 and 1.3801 of the guideline, so a + # family inside those slivers takes the lower step. + # Integer cross-products keep 138% of the guideline, $29,187, out of the sliver. + in_first = FPG * 10_000 < annual_income * 10_000 < FPG * 10_001 + in_second = FPG * 13_800 < annual_income * 10_000 < FPG * 13_801 + return not (in_first or in_second) + + +def cost_share_and_payment(annual_income, annual_charge): + members = ["parent", "child"] + parent = { + "age": {YEAR: 28}, + "weekly_hours_worked_before_lsr": {YEAR: 40}, + "employment_income": {YEAR: annual_income}, + } + child = { + "age": {YEAR: 4}, + "is_tax_unit_dependent": {YEAR: True}, + "immigration_status": {YEAR: "CITIZEN"}, + "childcare_hours_per_week": {YEAR: 40}, + "nh_ccap_provider_type": {YEAR: "LICENSED_CENTER"}, + "pre_subsidy_childcare_expenses": {YEAR: annual_charge}, + } + situation = { + "people": {"parent": parent, "child": child}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": ["parent"]}}, + "households": {"household": {"members": members, "state_name": {YEAR: "NH"}}}, + } + simulation = Simulation(situation=situation) + return [ + float(simulation.calculate(name, MONTH)[0]) + for name in ("nh_ccap_cost_share", "nh_ccap") + ] + + +@settings(max_examples=15, deadline=None) +# Zero income, each step's upper edge, the first dollars above 138%, and a charge +# below the rate that the cost share exceeds. +@example(annual_income=0, annual_charge=18_720) +@example(annual_income=FPG, annual_charge=18_720) +@example(annual_income=FPG * 138 // 100, annual_charge=18_720) +@example(annual_income=29_200, annual_charge=18_720) +@example(annual_income=48_000, annual_charge=18_720) +@example(annual_income=80_000, annual_charge=1_000) +@given( + annual_income=st.integers(min_value=0, max_value=80_000), + annual_charge=st.integers(min_value=1, max_value=40_000), +) +def test_cost_share_and_payment_follow_the_rule(annual_income, annual_charge): + assume(outside_boundary_slivers(annual_income)) + share, payment = cost_share_and_payment(annual_income, annual_charge) + expected_share = rule_cost_share(annual_income) + assert share == pytest.approx(expected_share, abs=TOLERANCE) + weekly_cap = min(annual_charge / 52, WEEKLY_RATE) + expected_payment = max(weekly_cap - expected_share, 0) * 52 / 12 + assert payment == pytest.approx(expected_payment, abs=TOLERANCE) + + +@settings(max_examples=10, deadline=None) +@given( + lower=st.integers(min_value=29_190, max_value=79_999), + gap=st.integers(min_value=1, max_value=20_000), +) +def test_step_three_share_rises_with_income(lower, gap): + higher = min(lower + gap, 80_000) + lower_share, _ = cost_share_and_payment(lower, 18_720) + higher_share, _ = cost_share_and_payment(higher, 18_720) + assert higher_share > lower_share diff --git a/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.py b/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.py index 15917dc24a6..0a619e764e2 100644 --- a/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.py +++ b/policyengine_us/variables/gov/states/nh/dhhs/ccap/nh_ccap_cost_share.py @@ -9,7 +9,7 @@ class nh_ccap_cost_share(Variable): definition_period = MONTH defined_for = StateCode.NH reference = ( - "https://www.law.cornell.edu/regulations/new-hampshire/N-H-Admin-Code-SS-He-C-6910.18", + "https://gc.nh.gov/rules/state_agencies/he-c6900.html", "https://www.dhhs.nh.gov/sr_htm/html/sr_24-08_dated_01_24.htm", ) @@ -17,10 +17,9 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nh.dhhs.ccap.cost_share countable_income = spm_unit("nh_ccap_countable_income", period) fpg = spm_unit("spm_unit_fpg", period) - weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR fpg_ratio = where(fpg > 0, countable_income / fpg, 0) income_based_share = ( - countable_income * p.income_share_rate.calc(fpg_ratio) / weeks_per_month + countable_income * p.income_share_rate.calc(fpg_ratio) / p.weeks_per_month ) return p.weekly_flat_amount.calc(fpg_ratio) + income_based_share