From caec9d8a8078e60bff5903bd68352d7e953719fd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 09:12:22 -0500 Subject: [PATCH 1/2] Divide monthly income by 4.33 weeks in New Hampshire's child care cost share He-C 6910.18(b) calculates the weekly cost share as monthly gross income times the step's percent, "divided by 4.33 weeks". The variable divided by 52/12, which understates a step 3 family's share by 0.077%. A parameter now holds the rule's divisor, and the cost share references point to the current rule. Co-Authored-By: Claude Opus 5.5 --- changelog.d/nh-ccap-cost-share-weeks.fixed.md | 1 + .../ccap/cost_share/income_share_rate.yaml | 2 +- .../ccap/cost_share/weekly_flat_amount.yaml | 2 +- .../dhhs/ccap/cost_share/weeks_per_month.yaml | 14 +++ .../gov/states/nh/dhhs/ccap/integration.yaml | 45 +++++++- .../nh/dhhs/ccap/nh_ccap_cost_share.yaml | 12 +- .../test_nh_ccap_cost_share_properties.py | 107 ++++++++++++++++++ .../states/nh/dhhs/ccap/nh_ccap_cost_share.py | 5 +- 8 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 changelog.d/nh-ccap-cost-share-weeks.fixed.md create mode 100644 policyengine_us/parameters/gov/states/nh/dhhs/ccap/cost_share/weeks_per_month.yaml create mode 100644 policyengine_us/tests/test_nh_ccap_cost_share_properties.py 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..ab415d78189 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,42 @@ 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 > $2,432.25 (138% FPG for 2) and < $7,060.37 (85% SMI for 2) + 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..e78a2d3f8eb --- /dev/null +++ b/policyengine_us/tests/test_nh_ccap_cost_share_properties.py @@ -0,0 +1,107 @@ +"""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. 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 +MONTH = "2025-01" +# 2025 poverty guideline for a family of two. +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. + in_first = FPG < annual_income < FPG * 1.0001 + in_second = FPG * 1.38 < annual_income < FPG * 1.3801 + 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 From 7134a3cf98bcd1f8286a419bf8130c87fc7f6768 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 11:25:31 -0500 Subject: [PATCH 2/2] Test New Hampshire's cost share in July and compare boundaries in integers Under He-C 6910.18(e), New Hampshire applies each year's poverty guidelines from the following July, so the property test now draws families for July 2025, when the 2025 guideline applies under the rule and in the model. The boundary check compares integer cross-products, so 138% of the guideline counts as step 2. Invariant 4 now names the family it covers: one eligible child and no disability supplement. Case 10 stays in January 2025, since the YAML runner doesn't carry year-long inputs into a mid-year month. Its income exceeds 138% of both the 2024 and 2025 guidelines, and its comment gives both. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/nh/dhhs/ccap/integration.yaml | 4 +++- .../tests/test_nh_ccap_cost_share_properties.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) 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 ab415d78189..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 @@ -389,7 +389,9 @@ age: 28 weekly_hours_worked_before_lsr: 40 employment_income: 48_000 - # $48,000/yr = $4,000/mo > $2,432.25 (138% FPG for 2) and < $7,060.37 (85% SMI for 2) + # $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 diff --git a/policyengine_us/tests/test_nh_ccap_cost_share_properties.py b/policyengine_us/tests/test_nh_ccap_cost_share_properties.py index e78a2d3f8eb..4be7fd1e142 100644 --- a/policyengine_us/tests/test_nh_ccap_cost_share_properties.py +++ b/policyengine_us/tests/test_nh_ccap_cost_share_properties.py @@ -4,8 +4,9 @@ 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. The monthly payment equals 52/12 weeks of the lesser of the charge and the - standard rate, less the cost share, floored at zero. +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 @@ -19,8 +20,9 @@ from policyengine_us import Simulation YEAR = 2025 -MONTH = "2025-01" -# 2025 poverty guideline for a family of two. +# 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 @@ -37,8 +39,9 @@ def rule_cost_share(annual_income): 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. - in_first = FPG < annual_income < FPG * 1.0001 - in_second = FPG * 1.38 < annual_income < FPG * 1.3801 + # 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)