diff --git a/changelog.d/nd-ccap-waivers.added.md b/changelog.d/nd-ccap-waivers.added.md new file mode 100644 index 00000000000..76d625f3cdf --- /dev/null +++ b/changelog.d/nd-ccap-waivers.added.md @@ -0,0 +1 @@ +Added North Dakota CCAP Workforce Benefit, program co-payment waivers, and TANF co-payment exceptions. diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/workforce/second_caretaker_activity_exempt.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/workforce/second_caretaker_activity_exempt.yaml new file mode 100644 index 00000000000..16685643a50 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/workforce/second_caretaker_activity_exempt.yaml @@ -0,0 +1,27 @@ +description: North Dakota exempts the second caretaker in a Workforce Benefit household from the allowable-activity requirement under the Child Care Assistance Program if this is true. + +# From June 1, 2024 (ML 3819), North Dakota exempted the second caretaker of a +# Workforce Benefit household from the activity requirement, and it kept that +# rule in ML 3862 and ML 3909. From January 1, 2026 (Release 26.1), North +# Dakota requires the second caretaker's activity. North Dakota started the +# Workforce Benefit on June 1, 2024, so the earlier false value has no effect. +values: + 0000-01-01: false + 2024-06-01: true + 2026-01-01: false + +metadata: + unit: bool + period: year + label: North Dakota CCAP Workforce Benefit second caretaker activity exemption + reference: + - title: North Dakota CCAP Manual Letter 3819, Basic Eligibility Alignment 400-28-20-40 (eff. June 1, 2024) + href: https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2024/CCAP%20ML%203819%20Effective%20June.1.2024.pdf#page=2 + - title: North Dakota CCAP Manual Letter 3862, Basic Eligibility Alignment 400-28-20-40 (eff. September 1, 2024) + href: https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2024/CCAP%20ML%203862%20Effective%20September.1.2024.pdf#page=2 + - title: North Dakota CCAP Manual Letter 3909, Basic Eligibility Alignment 400-28-20-40 (eff. May 1, 2025) + href: https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2025/CCAP%20ML%203909%20Effective%20May.1.2025.pdf#page=4 + - title: "North Dakota CCAP Release 26.1, Basic Eligibility Alignment 400-28-20-40 (eff. January 1, 2026) #page 1-2" + href: https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2026/CCAP%20Release%2026.1%20Effective%20January.1.2026.pdf#page=1 + - title: North Dakota CCAP Policy Manual, Basic Eligibility Alignment 400-28-20-40 + href: https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_20_40.htm diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 0f74596fd68..e22b00d146b 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -965,7 +965,7 @@ programs: full_name: North Dakota Child Care Assistance Program variable: nd_ccap parameter_prefix: gov.states.nd.dhs.ccap - notes: Models the initial (75% of SMI) and continuing (85% of SMI) income limits, the SMI-band co-payment schedule with the 30%-of-SMI and TANF waivers, and the federal family-share ceiling. Not modeled - the CCAP Workforce Benefit income-limit and co-payment waiver for early-childhood workforce families, and the Diversion and Crossroads co-payment waivers + notes: Models the initial (75% of SMI) and continuing (85% of SMI) income limits, the SMI-band co-payment schedule with the 30%-of-SMI and TANF waivers, and the federal family-share ceiling. Also models the Workforce Benefit income-limit and co-payment waiver, Crossroads, Diversion and ARSEN co-payment waivers, and TANF and Diversion co-payment exceptions. From October 2026, when a child who receives SSI in a TANF or Diversion family attends care, the model charges one family co-payment that also covers that child's siblings in care. Not modeled - the income-requirement exemption for TANF and Diversion families, the co-payment waiver they keep after TANF or Diversion closes, Workforce Benefit eligibility for households outside North Dakota, and provider registration fees - state: NM status: complete name: New Mexico CCAP diff --git a/policyengine_us/tests/core/test_nd_ccap_waivers_invariants.py b/policyengine_us/tests/core/test_nd_ccap_waivers_invariants.py new file mode 100644 index 00000000000..d46ecb61ad6 --- /dev/null +++ b/policyengine_us/tests/core/test_nd_ccap_waivers_invariants.py @@ -0,0 +1,233 @@ +import numpy as np +import pytest +from hypothesis import example, given, settings, strategies as st + +from policyengine_us import Simulation + + +CASE_COUNT = 12 +WAIVER_INPUTS = ( + "nd_ccap_crossroads_enrolled", + "nd_ccap_tanf_diversion_enrolled", + "nd_ccap_arsen_enrolled", +) +CARETAKER_INPUTS = ( + "nd_ccap_has_disqualified_alien_caretaker", + "nd_ccap_has_ineligible_non_legally_responsible_caretaker", + "nd_ccap_has_ineligible_kinship_caretaker", +) + + +@pytest.fixture(scope="module") +def simulation(): + situation = { + entity: {} + for entity in ( + "people", + "tax_units", + "spm_units", + "families", + "marital_units", + "households", + ) + } + for index in range(CASE_COUNT): + parent = f"parent_{index}" + child = f"child_{index}" + situation["people"][parent] = {} + situation["people"][child] = {} + members = [parent, child] + for entity in ("tax_units", "spm_units", "families", "households"): + situation[entity][f"unit_{index}"] = {"members": members} + for person in members: + situation["marital_units"][person] = {"members": [person]} + return Simulation(situation=situation) + + +@settings(max_examples=8, deadline=None, derandomize=True) +@example( + period="2026-10", + income=4_500, + monthly_smi=10_000, + tanf=False, + enrolled=False, + child_ssi=True, + caretakers=(False, False, False), + tanf_eligible_child=False, +) +@example( + period="2026-10", + income=5_000, + monthly_smi=10_000, + tanf=True, + enrolled=False, + child_ssi=True, + caretakers=(True, True, True), + tanf_eligible_child=False, +) +@example( + period="2026-09", + income=50_000, + monthly_smi=10_000, + tanf=True, + enrolled=True, + child_ssi=True, + caretakers=(False, False, True), + tanf_eligible_child=True, +) +@example( + period="2024-07", + income=-1_000, + monthly_smi=10_000, + tanf=False, + enrolled=False, + child_ssi=False, + caretakers=(False, False, False), + tanf_eligible_child=False, +) +@given( + period=st.sampled_from(("2024-07", "2025-10", "2026-09", "2026-10")), + income=st.integers(min_value=-100_000, max_value=1_000_000_000), + monthly_smi=st.integers(min_value=0, max_value=25_000), + tanf=st.booleans(), + enrolled=st.booleans(), + child_ssi=st.booleans(), + caretakers=st.tuples(st.booleans(), st.booleans(), st.booleans()), + tanf_eligible_child=st.booleans(), +) +def test_nd_ccap_waivers_preserve_bounds_and_default_behavior( + simulation, + period, + income, + monthly_smi, + tanf, + enrolled, + child_ssi, + caretakers, + tanf_eligible_child, +): + year = period[:4] + simulation.drop_computed_arrays() + simulation.set_input("state_code", year, ["ND"] * CASE_COUNT) + simulation.set_input("age", year, [30, 3] * CASE_COUNT) + simulation.set_input("is_disabled", year, [False] * (2 * CASE_COUNT)) + # Every child attends care, so a child's SSI can trigger the exception. + simulation.set_input("childcare_hours_per_week", year, [0, 40] * CASE_COUNT) + simulation.set_input("hhs_smi", year, [monthly_smi * 12] * CASE_COUNT) + simulation.set_input("nd_ccap_countable_income", period, [income] * CASE_COUNT) + # Isolate the co-payment calculation from the other eligibility tests. + simulation.set_input("nd_ccap_eligible", period, [True] * CASE_COUNT) + simulation.set_input("is_tanf_enrolled", period, [tanf] * 10 + [True, True]) + simulation.set_input( + "is_nd_ccap_enrolled", + period, + [enrolled] * 4 + [False, True] + [enrolled] * 6, + ) + + workforce = np.zeros(2 * CASE_COUNT, dtype=bool) + workforce[[8, 10, 18]] = True + simulation.set_input("nd_ccap_workforce_benefit_eligible_parent", period, workforce) + for variable, waived_case in zip(WAIVER_INPUTS, (6, 7, 8)): + values = np.zeros(CASE_COUNT, dtype=bool) + values[[waived_case, 9]] = True + simulation.set_input(variable, period, values) + + for variable, caretaker in zip(CARETAKER_INPUTS, caretakers): + values = [False] * 3 + [caretaker] * 7 + [False, False] + if variable == "nd_ccap_has_ineligible_kinship_caretaker": + values[-2:] = [True, True] + simulation.set_input(variable, period, values) + simulation.set_input( + "nd_ccap_has_tanf_eligible_child", + period, + [False] * 3 + [tanf_eligible_child] * 7 + [False, True], + ) + + ssi = np.zeros(2 * CASE_COUNT) + ssi[3] = 1 + ssi[4] = 1 + for index in range(3, 10): + ssi[2 * index + 1] = int(child_ssi) + simulation.set_input("ssi", period, ssi) + + inputs = ( + "nd_ccap_workforce_benefit_eligible_parent", + *WAIVER_INPUTS, + *CARETAKER_INPUTS, + "nd_ccap_has_tanf_eligible_child", + ) + for variable in inputs: + assert simulation.tax_benefit_system.variables[variable].default_value is False + + countable_income = simulation.calculate("nd_ccap_countable_income", period) + smi = simulation.calculate("hhs_smi", period) + positive_income = np.maximum(countable_income, 0) + income_to_smi = np.divide( + positive_income, + smi, + out=np.zeros_like(positive_income), + where=smi > 0, + ) + p = simulation.tax_benefit_system.parameters(period).gov.states.nd.dhs.ccap + unwaived = np.minimum( + p.copay.rate.calc(income_to_smi) * smi, + p.copay.max_rate * positive_income, + ) + copay = simulation.calculate("nd_ccap_copay", period) + assert np.all(copay >= 0) + assert np.all(copay <= unwaived + 0.01) + np.testing.assert_allclose(copay[[4, 5, 6, 8, 9]], 0, atol=0.01) + assert np.all(copay[4:10] <= copay[3] + 0.01) + kinship_exception = caretakers[2] and not ( + period >= "2026-10" and tanf_eligible_child + ) + diversion_exception = ( + caretakers[0] + or caretakers[1] + or kinship_exception + or (period >= "2026-10" and child_ssi) + ) + expected_diversion_copay = ( + unwaived[7] + if diversion_exception and income_to_smi[7] > p.copay.waiver_smi_threshold + else 0 + ) + np.testing.assert_allclose(copay[7], expected_diversion_copay, atol=0.01) + assert copay[11] <= copay[10] + 0.01 + if period >= "2026-10": + assert copay[11] == 0 + else: + assert copay[11] == copay[10] + + # Preserve main's calculation, except that SSI can end the TANF waiver: + # a caretaker's SSI (unit 2) before October 2026, and a child's SSI + # (unit 1) from then on. + expected = np.where( + (income_to_smi[:3] <= p.copay.waiver_smi_threshold) | tanf, + 0, + unwaived[:3], + ) + ssi_exception = 1 if period >= "2026-10" else 2 + if tanf: + expected[ssi_exception] = ( + 0 + if income_to_smi[ssi_exception] <= p.copay.waiver_smi_threshold + else unwaived[ssi_exception] + ) + np.testing.assert_allclose(copay[:3], expected, atol=0.01) + + income_eligible = simulation.calculate("nd_ccap_income_eligible", period) + rate = p.income.continuing_smi_rate if enrolled else p.income.initial_smi_rate + np.testing.assert_array_equal( + income_eligible[:3], countable_income[:3] <= smi[:3] * rate + ) + assert np.all(income_eligible[4:6]) + + # Units 4, 5 and 9 have a Workforce Benefit caretaker and no other modeled + # activity. North Dakota waives the second caretaker's activity test + # through December 2025 (ML 3819) and restores it in January 2026. + activity = simulation.calculate("nd_ccap_activity_eligible", period) + if period < "2026-01": + assert np.all(activity[[4, 5, 9]]) + else: + np.testing.assert_array_equal(activity[[4, 5, 9]], activity[[0, 0, 0]]) diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_categorical_copay_waiver.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_categorical_copay_waiver.yaml new file mode 100644 index 00000000000..6fd6276c906 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_categorical_copay_waiver.yaml @@ -0,0 +1,50 @@ +# Tests for nd_ccap_categorical_copay_waiver (SPMUnit, MONTH, bool). +# North Dakota added the ARSEN waiver on April 1, 2021. + +# North Dakota starts the ARSEN categorical waiver on April 1, 2021. +- name: North Dakota withholds the ARSEN categorical waiver before April 2021 + period: 2021-03 + input: + people: + parent: + age: + 2021: 30 + child: + age: + 2021: 3 + spm_units: + family: + members: [parent, child] + is_tanf_enrolled: false + nd_ccap_arsen_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2021: ND + output: + nd_ccap_categorical_copay_waiver: false + +# North Dakota starts the ARSEN categorical waiver on April 1, 2021. +- name: North Dakota starts the ARSEN categorical waiver in April 2021 + period: 2021-04 + input: + people: + parent: + age: + 2021: 30 + child: + age: + 2021: 3 + spm_units: + family: + members: [parent, child] + is_tanf_enrolled: false + nd_ccap_arsen_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2021: ND + output: + nd_ccap_categorical_copay_waiver: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml index 6b5973c305d..8350fbe8b1f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml @@ -1,4 +1,5 @@ # Tests for nd_ccap_copay (SPMUnit, MONTH, USD). +# The original worksheet cases below check SMI bands and the TANF waiver. # The family monthly co-payment is a flat amount per SMI band from the HHS # "Individual Worksheet for Calculating Co-pay": copay = share(income/SMI) x # monthly SMI, where the band share = band rate (2% for 30-50%, 4% for 50-60%, @@ -6,7 +7,7 @@ # TANF recipients (400-28-90-10/-20). The rate structure was validated against # the worksheet for household sizes 1-8 on 2026-06-26. # -# Period is 2025-01: at that period PolicyEngine's hhs_smi equals the real +# The original worksheet cases use 2025-01, when hhs_smi equals the real # published FFY2025 SMI exactly (the uprating projection only applies after the # last published value, 2025-10-01), so these cases use real-world SMI rather # than the uprated calendar-2026 projection. @@ -380,3 +381,996 @@ output: # share 0.0474 * 8,616.44 = 408.42 nd_ccap_copay: 408.42 + +# The additional waiver cases set annual SMI directly to 120,000. +# Monthly SMI = 120,000 / 12 = 10,000. +# At monthly income 4,500, the worksheet applies the 44-46 percent SMI band: +# ordinary copay = 2 percent * 44 percent * 10,000 = 88. +# Set eligibility directly in these copay cases to isolate waiver decisions. + +# Monthly SMI = 120,000 / 12 = 10,000; income / SMI = 0.45. +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the worksheet copay when new inputs retain their defaults + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: false + nd_ccap_copay: 88 + +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88; North Dakota waives it. +- name: North Dakota waives the copay for a Workforce Benefit family + period: 2024-07 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2024: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + child: + age: + 2024: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2024: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2024: ND + output: + nd_ccap_workforce_benefit_eligible: true + nd_ccap_copay: 0 + +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88; North Dakota waives it. +- name: North Dakota waives the copay for a family receiving Crossroads + period: 2024-07 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2024: 30 + ssi: 0 + child: + age: + 2024: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2024: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_crossroads_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2024: ND + output: + nd_ccap_copay: 0 + +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88; North Dakota waives it. +- name: North Dakota waives the copay for a family receiving Diversion + period: 2024-07 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2024: 30 + ssi: 0 + child: + age: + 2024: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2024: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_tanf_diversion_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2024: ND + output: + nd_ccap_copay: 0 + +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88; North Dakota waives it. +- name: North Dakota waives the copay for a family receiving ARSEN + period: 2024-07 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2024: 30 + ssi: 0 + child: + age: + 2024: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2024: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_arsen_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2024: ND + output: + nd_ccap_copay: 0 + +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88; North Dakota waives it. +- name: North Dakota retains the TANF copay waiver without an exception + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# The child receives 100 in monthly SSI. +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota retains the TANF waiver for a child receiving SSI before October 2026 + period: 2026-09 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# The child attends care 40 hours a week and receives 100 in monthly SSI. +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the TANF copay for a child receiving SSI from October 2026 + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# The parent receives 100 in monthly SSI; the child receives zero. +# North Dakota waives the ordinary copay of 88. +- name: North Dakota retains the TANF copay waiver when only an adult receives SSI + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 100 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the TANF copay for a disqualified alien caretaker + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_disqualified_alien_caretaker: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the TANF copay for an ineligible non-legally responsible caretaker + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_ineligible_non_legally_responsible_caretaker: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the TANF copay for an ineligible kinship caretaker with non-TANF children + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_ineligible_kinship_caretaker: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# A TANF-eligible non-kinship child preserves the waiver of the ordinary copay of 88. +- name: North Dakota preserves the kinship waiver when a non-kinship child qualifies for TANF + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_ineligible_kinship_caretaker: true + nd_ccap_has_tanf_eligible_child: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# The TANF-eligible-child exception applies to kinship caretakers. +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges an alien caretaker despite a TANF-eligible child + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_disqualified_alien_caretaker: true + nd_ccap_has_tanf_eligible_child: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# Monthly SMI = 120,000 / 12 = 10,000; 3,000 / 10,000 = 0.30. +# North Dakota waives the copay at the 30 percent boundary. +- name: North Dakota waives the copay at 30 percent of SMI despite TANF exceptions + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 3000 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_disqualified_alien_caretaker: true + nd_ccap_has_ineligible_non_legally_responsible_caretaker: true + nd_ccap_has_ineligible_kinship_caretaker: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# Crossroads, ARSEN and Workforce families retain independent waivers. +# North Dakota charges the Diversion family for a disqualified alien caretaker. +# The Diversion copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota applies TANF exceptions to Diversion and retains independent program waivers + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + crossroads_parent: + age: + 2026: 30 + ssi: 0 + crossroads_child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + diversion_parent: + age: + 2026: 30 + ssi: 0 + diversion_child: + age: + 2026: 3 + ssi: 0 + arsen_parent: + age: + 2026: 30 + ssi: 0 + arsen_child: + age: + 2026: 3 + ssi: 0 + workforce_parent: + age: + 2026: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + workforce_child: + age: + 2026: 3 + ssi: 0 + spm_units: + crossroads: + members: [crossroads_parent, crossroads_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_crossroads_enrolled: true + diversion: + members: [diversion_parent, diversion_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_tanf_diversion_enrolled: true + nd_ccap_has_disqualified_alien_caretaker: true + arsen: + members: [arsen_parent, arsen_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_arsen_enrolled: true + nd_ccap_has_ineligible_non_legally_responsible_caretaker: true + workforce: + members: [workforce_parent, workforce_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_ineligible_kinship_caretaker: true + tax_units: + crossroads: + members: [crossroads_parent, crossroads_child] + diversion: + members: [diversion_parent, diversion_child] + arsen: + members: [arsen_parent, arsen_child] + workforce: + members: [workforce_parent, workforce_child] + households: + crossroads: + members: [crossroads_parent, crossroads_child] + state_code: + 2026: ND + diversion: + members: [diversion_parent, diversion_child] + state_code: + 2026: ND + arsen: + members: [arsen_parent, arsen_child] + state_code: + 2026: ND + workforce: + members: [workforce_parent, workforce_child] + state_code: + 2026: ND + output: + nd_ccap_copay: [0, 88, 0, 0] + +# Each ordinary copay = 0.02 * 0.44 * 10,000 = 88. +# North Dakota waives only the first and third families; the second has a child receiving SSI. +- name: North Dakota keeps Workforce and child SSI decisions within each family + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + workforce_parent: + age: + 2026: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + workforce_child: + age: + 2026: 3 + ssi: 0 + tanf_ssi_parent: + age: + 2026: 30 + ssi: 0 + tanf_ssi_child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + tanf_plain_parent: + age: + 2026: 30 + ssi: 0 + tanf_plain_child: + age: + 2026: 3 + ssi: 0 + spm_units: + workforce: + members: [workforce_parent, workforce_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + tanf_ssi: + members: [tanf_ssi_parent, tanf_ssi_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + tanf_plain: + members: [tanf_plain_parent, tanf_plain_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + tax_units: + workforce: + members: [workforce_parent, workforce_child] + tanf_ssi: + members: [tanf_ssi_parent, tanf_ssi_child] + tanf_plain: + members: [tanf_plain_parent, tanf_plain_child] + households: + workforce: + members: [workforce_parent, workforce_child] + state_code: + 2026: ND + tanf_ssi: + members: [tanf_ssi_parent, tanf_ssi_child] + state_code: + 2026: ND + tanf_plain: + members: [tanf_plain_parent, tanf_plain_child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: [true, false, false] + nd_ccap_copay: [0, 88, 0] + +# The disabled child qualifies under the existing CCAP child age limit and receives 100 in SSI. +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the TANF copay for an eligible disabled child aged 18 receiving SSI + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 18 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + is_disabled: + 2026: true + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# Before October 2026, a TANF-eligible child does not preserve the kinship waiver. +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the kinship caretaker before the October 2026 TANF-child exception + period: 2026-09 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + nd_ccap_has_ineligible_kinship_caretaker: true + nd_ccap_has_tanf_eligible_child: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# The child receives 100 in monthly SSI at age 17 and does not have a disability. +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the TANF copay for a child aged 17 receiving SSI + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 17 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + is_disabled: + 2026: false + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# The family receives Diversion and no TANF; the child receives 100 in monthly SSI. +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88. +# North Dakota retains the Diversion waiver before October 1, 2026. +- name: North Dakota retains the Diversion waiver for a child receiving SSI before October 2026 + period: 2026-09 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_tanf_diversion_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# The family receives Diversion and no TANF; the child receives 100 in monthly SSI. +# The ordinary copay = 0.02 * 0.44 * 10,000 = 88. +# North Dakota applies the child SSI exception from October 1, 2026. +- name: North Dakota charges a Diversion family for a child receiving SSI from October 2026 + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 100 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_tanf_diversion_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# Monthly SMI = 120,000 / 12 = 10,000; 4,500 / 10,000 = 0.45, the 44-46% band. +# Until October 2026, North Dakota charges the co-pay for care that follows +# from the activity of a TANF caretaker who receives SSI (400-28-90-20). +# Copay = min(0.02 * 0.44 * 10,000, 0.07 * 4,500) = min(88, 315) = 88. +- name: North Dakota charges the TANF copay when a caretaker receives SSI before October 2026 + period: 2026-09 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 100 + child: + age: + 2026: 3 + ssi: 0 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# The Diversion family loses its waiver to the same caretaker-SSI exception. +# Copay = 0.02 * 0.44 * 10,000 = 88. +- name: North Dakota charges the Diversion copay when a caretaker receives SSI before October 2026 + period: 2026-09 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 100 + child: + age: + 2026: 3 + ssi: 0 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + nd_ccap_eligible: true + nd_ccap_tanf_diversion_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 88 + +# The 10-year-old receives SSI and attends no child care; the 3-year-old attends +# care and receives no SSI. North Dakota charges the co-pay for "child care for +# an SSI child" (400-28-45-05), so the TANF waiver covers the 88 worksheet copay. +- name: North Dakota keeps the TANF waiver when the SSI child attends no child care + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + ssi_child: + age: + 2026: 10 + ssi: 100 + child: + age: + 2026: 3 + ssi: 0 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, ssi_child, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, ssi_child, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 + +# The model applies North Dakota's under-19 incapacity age limit to annual age, +# so a disabled 19-year-old who receives SSI and attends care does not trigger +# the child-SSI exception. The TANF waiver covers the 88 worksheet copay. +- name: North Dakota keeps the TANF waiver for a disabled 19-year-old receiving SSI + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 45 + ssi: 0 + disabled_adult_child: + age: + 2026: 19 + is_disabled: + 2026: true + ssi: 100 + childcare_hours_per_week: + 2026: 20 + child: + age: + 2026: 3 + ssi: 0 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, disabled_adult_child, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: true + nd_ccap_eligible: true + households: + household: + members: [parent, disabled_adult_child, child] + state_code: + 2026: ND + output: + nd_ccap_copay: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_copay_exception.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_copay_exception.yaml new file mode 100644 index 00000000000..105e5ae01ee --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_copay_exception.yaml @@ -0,0 +1,112 @@ +# Tests for nd_ccap_tanf_copay_exception (SPMUnit, MONTH, bool). +# From October 2011 through September 2026, North Dakota charged the co-pay +# when child care followed from the activity of a TANF caretaker "In receipt +# of SSI" (400-28-90-20, ML 3278 and ML 3605 versions). North Dakota repealed +# that exception on October 1, 2026 (Release 26.3) and moved the SSI test to +# the child. + +- name: North Dakota applies the caretaker-SSI exception from October 2011 + period: 2020-01 + input: + people: + parent: + age: + 2020: 30 + ssi: 100 + child: + age: + 2020: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: + 2020: ND + output: + nd_ccap_tanf_copay_exception: true + +- name: North Dakota applies the caretaker-SSI exception through September 2026 + period: 2026-09 + input: + people: + parent: + age: + 2026: 30 + ssi: 100 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_tanf_copay_exception: true + +- name: North Dakota ends the caretaker-SSI exception in October 2026 + period: 2026-10 + input: + people: + parent: + age: + 2026: 30 + ssi: 100 + child: + age: + 2026: 3 + ssi: 0 + childcare_hours_per_week: + 2026: 40 + spm_units: + family: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_tanf_copay_exception: false + +# The 18-year-old receives SSI but is neither the tax unit head nor spouse, +# so the model does not treat them as a caretaker. +- name: North Dakota applies the caretaker-SSI exception to caretakers alone + period: 2025-01 + input: + people: + parent: + age: + 2025: 40 + is_tax_unit_head_or_spouse: + 2025: true + ssi: 0 + adult_child: + age: + 2025: 18 + is_tax_unit_head_or_spouse: + 2025: false + ssi: 100 + child: + age: + 2025: 3 + is_tax_unit_head_or_spouse: + 2025: false + ssi: 0 + spm_units: + family: + members: [parent, adult_child, child] + households: + household: + members: [parent, adult_child, child] + state_code: + 2025: ND + output: + nd_ccap_tanf_copay_exception: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml index fec407a9059..69d85376e31 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml @@ -132,3 +132,111 @@ state_code: ND output: nd_ccap_activity_eligible: true + +# ML 3819 Example 1: one caretaker works 30 hours a week at a North Dakota +# licensed program and qualifies for the Workforce Benefit; the other has no +# allowable activity. North Dakota approved CCAP from June 2024 through +# December 2025. +- name: North Dakota exempts the second caretaker of a Workforce Benefit household in 2025 + period: 2025-01 + input: + people: + person1: + age: 32 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + nd_ccap_workforce_benefit_eligible_parent: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_activity_eligible: true + +# North Dakota requires the second caretaker's activity from January 2026 +# (Release 26.1). +- name: North Dakota requires the second caretaker's activity in a Workforce Benefit household from 2026 + period: 2026-01 + input: + people: + person1: + age: 32 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + nd_ccap_workforce_benefit_eligible_parent: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_activity_eligible: false + +# North Dakota started the Workforce Benefit on June 1, 2024. +- name: North Dakota requires the second caretaker's activity before the Workforce Benefit starts + period: 2024-05 + input: + people: + person1: + age: 32 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + nd_ccap_workforce_benefit_eligible_parent: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_activity_eligible: false + +# Without a Workforce Benefit caretaker, North Dakota still requires the +# second caretaker's activity in 2025. +- name: North Dakota requires the second caretaker's activity outside the Workforce Benefit in 2025 + period: 2025-01 + input: + people: + person1: + age: 32 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_activity_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml index 0e17b2d81b1..ede593746a0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml @@ -129,3 +129,63 @@ output: # defined_for = StateCode.ND zeroes eligibility outside North Dakota. nd_ccap_eligible: false + +# ML 3819 Example 1 with the income and activity tests computed. North Dakota +# excludes all income of a Workforce Benefit family and, from June 2024 through +# December 2025, required no activity of its second caretaker. +- name: North Dakota approves a Workforce Benefit household with an inactive second caretaker in 2025 + period: 2025-01 + input: + people: + person1: + age: 32 + employment_income: 240_000 + is_tax_unit_head_or_spouse: true + nd_ccap_workforce_benefit_eligible_parent: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + is_ccdf_asset_eligible: true + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_income_eligible: true + nd_ccap_activity_eligible: true + nd_ccap_eligible: true + +# North Dakota denies the same household from January 2026 (Release 26.1). +- name: North Dakota denies a Workforce Benefit household with an inactive second caretaker from 2026 + period: 2026-01 + input: + people: + person1: + age: 32 + employment_income: 240_000 + is_tax_unit_head_or_spouse: true + nd_ccap_workforce_benefit_eligible_parent: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + is_ccdf_asset_eligible: true + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_income_eligible: true + nd_ccap_activity_eligible: false + nd_ccap_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml index 640bd12232a..dada9b1cc37 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml @@ -1,9 +1,10 @@ # Tests for nd_ccap_income_eligible (SPMUnit, MONTH, bool). +# The original threshold cases below isolate the two-tier income comparison. # Initial applicants (is_nd_ccap_enrolled = false) are tested against 75% of # the monthly state median income; enrolled recipients (is_nd_ccap_enrolled = # true) are tested against 85% (400-28-25-15). Countable income is set # directly to isolate the two-tier comparison. -# 2-person ND monthly SMI for 2026 = $7,361.62/mo: +# The original threshold cases use 2026-01 and two-person SMI of $7,361.62/mo: # 75% = $5,521.22/mo, 85% = $6,257.38/mo. # nd_ccap_countable_income is a MONTH variable, so values are monthly. @@ -146,3 +147,209 @@ state_code: ND output: nd_ccap_income_eligible: false + +# The additional Workforce Benefit cases set annual SMI to 120,000. +# Monthly SMI = 10,000; application threshold = 0.75 * 10,000 = 7,500; +# continuing threshold = 0.85 * 10,000 = 8,500. +# Qualifying Workforce families receive an income exclusion and copay waiver. + +# Monthly SMI = 120,000 / 12 = 10,000; earnings = 240,000 / 12 = 20,000. +# Earnings exceed 75 percent SMI (7,500); North Dakota waives the income limit and copay. +- name: North Dakota waives the income limit for a Workforce Benefit applicant + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + employment_income: + 2026: 240000 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + is_ccdf_asset_eligible: + 2026: true + meets_ccdf_activity_test: + 2026: true + is_tanf_enrolled: false + is_nd_ccap_enrolled: false + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: true + nd_ccap_countable_income: 20000 + nd_ccap_income_eligible: true + nd_ccap_eligible: true + nd_ccap_copay: 0 + +# Monthly SMI = 120,000 / 12 = 10,000; income 20,000 exceeds 7,500. +- name: North Dakota applies the income limit to an ordinary applicant + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 20000 + is_tanf_enrolled: false + is_nd_ccap_enrolled: false + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: false + nd_ccap_income_eligible: false + +# Monthly SMI = 120,000 / 12 = 10,000; earnings = 240,000 / 12 = 20,000. +# Earnings exceed 85 percent SMI (8,500); North Dakota waives the income limit and copay. +- name: North Dakota waives the income limit for an enrolled Workforce Benefit family + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + employment_income: + 2026: 240000 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + is_ccdf_asset_eligible: + 2026: true + meets_ccdf_activity_test: + 2026: true + is_tanf_enrolled: false + is_nd_ccap_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: true + nd_ccap_countable_income: 20000 + nd_ccap_income_eligible: true + nd_ccap_eligible: true + nd_ccap_copay: 0 + +# Monthly SMI = 120,000 / 12 = 10,000; income 20,000 exceeds 8,500. +- name: North Dakota applies the income limit to an ordinary enrolled family + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 30 + ssi: 0 + child: + age: + 2026: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 20000 + is_tanf_enrolled: false + is_nd_ccap_enrolled: true + households: + household: + members: [parent, child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: false + nd_ccap_income_eligible: false + +# Monthly SMI = 120,000 / 12 = 10,000; income 20,000 exceeds 75 percent SMI (7,500). +# North Dakota waives the income limit for the Workforce Benefit family. +- name: North Dakota keeps the Workforce income waiver within the qualifying family + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + workforce_parent: + age: + 2026: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + workforce_child: + age: + 2026: 3 + ssi: 0 + ordinary_parent: + age: + 2026: 30 + ssi: 0 + ordinary_child: + age: + 2026: 3 + ssi: 0 + spm_units: + workforce: + members: [workforce_parent, workforce_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 20000 + is_tanf_enrolled: false + is_nd_ccap_enrolled: false + ordinary: + members: [ordinary_parent, ordinary_child] + hhs_smi: + 2026: 120000 + nd_ccap_countable_income: 20000 + is_tanf_enrolled: false + is_nd_ccap_enrolled: false + tax_units: + workforce: + members: [workforce_parent, workforce_child] + ordinary: + members: [ordinary_parent, ordinary_child] + households: + workforce: + members: [workforce_parent, workforce_child] + state_code: + 2026: ND + ordinary: + members: [ordinary_parent, ordinary_child] + state_code: + 2026: ND + output: + nd_ccap_workforce_benefit_eligible: [true, false] + nd_ccap_income_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible.yaml new file mode 100644 index 00000000000..a83c33aef3c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible.yaml @@ -0,0 +1,90 @@ +# Tests for nd_ccap_workforce_benefit_eligible (SPMUnit, MONTH, bool). +# North Dakota started the Workforce Benefit on June 1, 2024. + +# A qualifying parent receives the Workforce Benefit from June 1, 2024. +- name: North Dakota withholds the Workforce Benefit before June 2024 + period: 2024-05 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2024: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + child: + age: + 2024: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2024: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + households: + household: + members: [parent, child] + state_code: + 2024: ND + output: + nd_ccap_workforce_benefit_eligible: false + +# A qualifying parent receives the Workforce Benefit from June 1, 2024. +- name: North Dakota starts the Workforce Benefit in June 2024 + period: 2024-06 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2024: 30 + ssi: 0 + nd_ccap_workforce_benefit_eligible_parent: true + child: + age: + 2024: 3 + ssi: 0 + spm_units: + family: + members: [parent, child] + hhs_smi: + 2024: 120000 + nd_ccap_countable_income: 4500 + is_tanf_enrolled: false + households: + household: + members: [parent, child] + state_code: + 2024: ND + output: + nd_ccap_workforce_benefit_eligible: true + +# North Dakota requires a caretaker in qualifying child care employment +# (400-28-20-40). A 16-year-old sibling's job leaves the family ineligible. +- name: North Dakota withholds the Workforce Benefit when only a non-caretaker qualifies + period: 2025-01 + input: + people: + parent: + age: + 2025: 30 + nd_ccap_workforce_benefit_eligible_parent: false + teen: + age: + 2025: 16 + nd_ccap_workforce_benefit_eligible_parent: true + child: + age: + 2025: 3 + spm_units: + family: + members: [parent, teen, child] + households: + household: + members: [parent, teen, child] + state_code: + 2025: ND + output: + nd_ccap_workforce_benefit_eligible: false diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_arsen_enrolled.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_arsen_enrolled.py new file mode 100644 index 00000000000..4f6fb276a6f --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_arsen_enrolled.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class nd_ccap_arsen_enrolled(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = "North Dakota CCAP family participates in ARSEN" + documentation = ( + "Records whether this family participates in the Alternative Response " + "for Substance Exposed Newborns program in the month. " + "North Dakota waives its co-payment from April 1, 2021, under CCAP " + "manual 400-28-90-20 and 400-28-55-30." + ) + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_55_30.htm", + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_categorical_copay_waiver.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_categorical_copay_waiver.py new file mode 100644 index 00000000000..7c8fb5007a0 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_categorical_copay_waiver.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class nd_ccap_categorical_copay_waiver(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + label = "North Dakota CCAP family qualifies for a program co-payment waiver" + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_45_05.htm", + ) + + def formula_2011_10_01(spm_unit, period, parameters): + crossroads = spm_unit("nd_ccap_crossroads_enrolled", period) + diversion = spm_unit("nd_ccap_tanf_diversion_enrolled", period) + exception = spm_unit("nd_ccap_tanf_copay_exception", period) + return crossroads | (diversion & ~exception) + + def formula_2021_04_01(spm_unit, period, parameters): + crossroads = spm_unit("nd_ccap_crossroads_enrolled", period) + diversion = spm_unit("nd_ccap_tanf_diversion_enrolled", period) + arsen = spm_unit("nd_ccap_arsen_enrolled", period) + exception = spm_unit("nd_ccap_tanf_copay_exception", period) + return crossroads | (diversion & ~exception) | arsen diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py index ecf4ca3cf01..a440328ecb6 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py @@ -8,7 +8,10 @@ class nd_ccap_copay(Variable): label = "North Dakota CCAP family co-payment" definition_period = MONTH defined_for = "nd_ccap_eligible" - reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_20_40.htm", + ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nd.dhs.ccap.copay @@ -28,10 +31,20 @@ def formula(spm_unit, period, parameters): # never exceeds max_rate of countable income. This does not bind under # the current schedule (the band rate tops out at 6%). copay = min_(copay, p.max_rate * countable_income) - # The co-payment is waived for families at or below 30% of the state - # median income and for TANF recipients (400-28-90-20). Diversion and - # Crossroads recipients are also waived but are not tracked at the - # moment. + # North Dakota waives co-payments for low-income families, qualifying + # TANF families, Crossroads, Diversion, ARSEN and Workforce Benefit + # families. TANF exceptions also apply to Diversion, while Crossroads, + # ARSEN and Workforce Benefit families keep their independent waivers + # (400-28-90-20 and 400-28-20-40). is_tanf_enrolled = spm_unit("is_tanf_enrolled", period) - waived = (income_to_smi <= p.waiver_smi_threshold) | is_tanf_enrolled + tanf_exception = spm_unit("nd_ccap_tanf_copay_exception", period) + tanf_waiver = is_tanf_enrolled & ~tanf_exception + program_waiver = spm_unit("nd_ccap_categorical_copay_waiver", period) + workforce = spm_unit("nd_ccap_workforce_benefit_eligible", period) + waived = ( + (income_to_smi <= p.waiver_smi_threshold) + | tanf_waiver + | program_waiver + | workforce + ) return where(waived, 0, copay) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_crossroads_enrolled.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_crossroads_enrolled.py new file mode 100644 index 00000000000..6ad18c46ee2 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_crossroads_enrolled.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class nd_ccap_crossroads_enrolled(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = "North Dakota CCAP family receives Crossroads services" + documentation = ( + "Records whether this family receives Crossroads Program services in the " + "month under CCAP manual 400-28-90-20. " + "North Dakota waived these families' co-payments by October 1, 2011." + ) + reference = "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_disqualified_alien_caretaker.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_disqualified_alien_caretaker.py new file mode 100644 index 00000000000..90b4df747d9 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_disqualified_alien_caretaker.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class nd_ccap_has_disqualified_alien_caretaker(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = "North Dakota CCAP family has a disqualified alien TANF caretaker" + documentation = ( + "Records whether a disqualified alien caretaker needs child care for " + "an activity in this TANF or Diversion family. " + "North Dakota applies the co-payment under CCAP manual 400-28-90-20." + ) + reference = "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_ineligible_kinship_caretaker.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_ineligible_kinship_caretaker.py new file mode 100644 index 00000000000..6f934a43a28 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_ineligible_kinship_caretaker.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class nd_ccap_has_ineligible_kinship_caretaker(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = "North Dakota CCAP family has an ineligible kinship caretaker with non-kinship children" + documentation = ( + "Records whether an ineligible Kinship Care caretaker needs child care " + "for non-kinship or non-TANF children in this family. " + "North Dakota applies the co-payment under CCAP manual 400-28-90-20, " + "except when some children qualify for TANF." + ) + reference = "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_ineligible_non_legally_responsible_caretaker.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_ineligible_non_legally_responsible_caretaker.py new file mode 100644 index 00000000000..0aa0011407b --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_ineligible_non_legally_responsible_caretaker.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class nd_ccap_has_ineligible_non_legally_responsible_caretaker(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = ( + "North Dakota CCAP family has an ineligible non-legally responsible caretaker" + ) + documentation = ( + "Records whether an ineligible non-legally responsible caretaker needs " + "child care for an activity in this TANF or Diversion family. " + "North Dakota applies the co-payment under CCAP manual 400-28-90-20." + ) + reference = "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_tanf_eligible_child.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_tanf_eligible_child.py new file mode 100644 index 00000000000..bc28390d2ae --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_has_tanf_eligible_child.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class nd_ccap_has_tanf_eligible_child(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = "North Dakota CCAP family has a TANF-eligible child" + documentation = ( + "Records whether this family includes a child who qualifies for TANF. " + "North Dakota preserves the ineligible Kinship Care caretaker's " + "co-payment waiver in this situation under CCAP manual 400-28-90-20." + ) + reference = "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_copay_exception.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_copay_exception.py new file mode 100644 index 00000000000..e6f782609ed --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_copay_exception.py @@ -0,0 +1,68 @@ +from policyengine_us.model_api import * + + +class nd_ccap_tanf_copay_exception(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + label = "North Dakota CCAP family has an exception to the TANF or Diversion co-payment waiver" + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_45_05.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/Archive%20Documents/2011%20-%203278%2C%203295%2C%203327/400_28_90_20%20ML3295.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/Archive%20Documents/2023/ML%203732/400_28_90_20.htm", + # PDF pages 3-4 + "https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2026/CCAP%20Release%2026.3%20Effective%20October.1.2026.pdf#page=3", + ) + + def formula_2011_10_01(spm_unit, period, parameters): + # Until October 2026, North Dakota charged the co-pay when child care + # resulted from the activity of a TANF caretaker who received SSI + # (400-28-90-20 and the repealed 400-28-45-15). + person = spm_unit.members + caretaker = person("is_tax_unit_head_or_spouse", period.this_year) + caretaker_ssi = spm_unit.any(caretaker & (person("ssi", period) > 0)) + alien = spm_unit("nd_ccap_has_disqualified_alien_caretaker", period) + non_legally_responsible = spm_unit( + "nd_ccap_has_ineligible_non_legally_responsible_caretaker", period + ) + return caretaker_ssi | alien | non_legally_responsible + + def formula_2021_04_01(spm_unit, period, parameters): + person = spm_unit.members + caretaker = person("is_tax_unit_head_or_spouse", period.this_year) + caretaker_ssi = spm_unit.any(caretaker & (person("ssi", period) > 0)) + alien = spm_unit("nd_ccap_has_disqualified_alien_caretaker", period) + non_legally_responsible = spm_unit( + "nd_ccap_has_ineligible_non_legally_responsible_caretaker", period + ) + kinship = spm_unit("nd_ccap_has_ineligible_kinship_caretaker", period) + return caretaker_ssi | alien | non_legally_responsible | kinship + + def formula_2026_10_01(spm_unit, period, parameters): + person = spm_unit.members + p = parameters(period).gov.states.nd.dhs.ccap.eligibility + # Include children under 18 and North Dakota's older disabled children, + # without imposing the separate CCAP immigration test on SSI receipt. + child = person("is_child", period.this_year) | ( + person("is_disabled", period.this_year) + & (person("age", period.this_year) < p.disabled_child_age_limit) + ) + # North Dakota charges the co-pay for "child care for an SSI child" + # (400-28-45-05), so the waiver holds when the SSI child attends no + # care. When an SSI child attends care, the model charges the whole + # family co-payment, including for a sibling whose care stays waived. + in_care = person("nd_ccap_child_in_care", period) + child_receives_ssi = spm_unit.any(child & in_care & (person("ssi", period) > 0)) + alien = spm_unit("nd_ccap_has_disqualified_alien_caretaker", period) + non_legally_responsible = spm_unit( + "nd_ccap_has_ineligible_non_legally_responsible_caretaker", period + ) + kinship = spm_unit("nd_ccap_has_ineligible_kinship_caretaker", period) + tanf_child = spm_unit("nd_ccap_has_tanf_eligible_child", period) + return ( + child_receives_ssi + | alien + | non_legally_responsible + | (kinship & ~tanf_child) + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_diversion_enrolled.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_diversion_enrolled.py new file mode 100644 index 00000000000..3280fb370e4 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_tanf_diversion_enrolled.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class nd_ccap_tanf_diversion_enrolled(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + default_value = False + label = "North Dakota CCAP family receives TANF Diversion" + documentation = ( + "Records whether this family receives TANF Diversion in the month under " + "CCAP manual 400-28-90-20. " + "North Dakota waived these families' co-payments by October 1, 2011, " + "subject to the TANF-related exceptions in 400-28-45-05." + ) + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_90_20.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_45_05.htm", + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py index 080c50a68d8..16d25e64ab2 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py @@ -7,7 +7,11 @@ class nd_ccap_activity_eligible(Variable): label = "North Dakota CCAP activity eligible" definition_period = MONTH defined_for = StateCode.ND - reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/40028.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_20_40.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2024/CCAP%20ML%203819%20Effective%20June.1.2024.pdf#page=2", + ) def formula(spm_unit, period, parameters): # All caretakers (head and spouse) must be in an allowable activity. @@ -24,4 +28,12 @@ def formula(spm_unit, period, parameters): # individually modeled (job search, education or training programs, # SNAP E&T, or a temporary leave from work or school; 400-28-55-05). meets_ccdf = spm_unit("meets_ccdf_activity_test", period.this_year) - return modeled_eligible | meets_ccdf + # From June 2024 through December 2025, North Dakota approved a + # Workforce Benefit household whose second caretaker had no allowable + # activity (400-28-20-40, ML 3819 Example 1). The qualifying caretaker's + # own employment meets the activity test. North Dakota requires the + # second caretaker's activity from January 2026 (Release 26.1). + p = parameters(period).gov.states.nd.dhs.ccap.workforce + workforce = spm_unit("nd_ccap_workforce_benefit_eligible", period) + workforce_exempt = p.second_caretaker_activity_exempt & workforce + return modeled_eligible | meets_ccdf | workforce_exempt diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py index a2235d409be..103f67f2d09 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py @@ -7,7 +7,10 @@ class nd_ccap_income_eligible(Variable): label = "North Dakota CCAP income eligible" definition_period = MONTH defined_for = StateCode.ND - reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_25_15.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_20_40.htm", + ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nd.dhs.ccap.income @@ -22,8 +25,8 @@ def formula(spm_unit, period, parameters): initial_limit = monthly_smi * p.initial_smi_rate continuing_limit = monthly_smi * p.continuing_smi_rate income_limit = where(enrolled, continuing_limit, initial_limit) - # The CCAP Workforce Benefit waives the income limit (and copay) for - # eligible early-childhood workforce families. We don't track which - # families qualify for the Workforce Benefit at the moment, so this - # waiver is not modeled. - return countable_income <= income_limit + # North Dakota excludes all income while a caretaker qualifies for the + # Workforce Benefit, including at review (400-28-20-40). After that + # qualification ends, enrolled families face the ordinary 85% limit. + workforce = spm_unit("nd_ccap_workforce_benefit_eligible", period) + return (countable_income <= income_limit) | workforce diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible.py new file mode 100644 index 00000000000..9ce5008f342 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class nd_ccap_workforce_benefit_eligible(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + label = "North Dakota CCAP family meets the Workforce Benefit employment test" + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_20_40.htm" + + def formula_2024_06_01(spm_unit, period, parameters): + # North Dakota requires at least one caretaker in qualifying child care + # employment (400-28-20-40); the model treats the tax unit head and + # spouse as the caretakers, as the activity test does. + person = spm_unit.members + caretaker = person("is_tax_unit_head_or_spouse", period.this_year) + qualifies = person("nd_ccap_workforce_benefit_eligible_parent", period) + return spm_unit.any(caretaker & qualifies) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible_parent.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible_parent.py new file mode 100644 index 00000000000..fda6a39838e --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_workforce_benefit_eligible_parent.py @@ -0,0 +1,37 @@ +from policyengine_us.model_api import * + + +class nd_ccap_workforce_benefit_eligible_parent(Variable): + value_type = bool + entity = Person + definition_period = MONTH + default_value = False + label = "North Dakota CCAP caretaker meets the Workforce Benefit employment test" + documentation = ( + "Records whether this caretaker meets the Workforce Benefit employment " + "conditions in CCAP manual 400-28-20-40 for the month. " + "From June 1, 2024, an employee must work at least 25 hours weekly in a " + "North Dakota licensed, tribally licensed or registered child care program. " + "The employer verifies employment on SFN 354. " + "Approved-relative, self-declared and out-of-state providers do not qualify. " + "Owners qualify only when their children attend another licensed program. " + "Employees could not care for their own children before September 1, 2024. " + "From May 1, 2025, the provider must operate at least 25 hours weekly, " + "and maternity leave can excuse reduced employee hours. " + "From April 1, 2026, employees and providers average 25 hours weekly. " + "Employer-confirmed vacation, temporary closure and temporary sick leave " + "can excuse reduced hours for up to one month. " + "Maternity and extended sick leave require an anticipated return date. " + "A job change or questionable hours at review require a new SFN 354. " + "Keep this input true through the month employment or the license ends, " + "or hours fall below the minimum without an allowed exception. " + "Ordinary income testing and co-payments resume the following month. " + "Other family eligibility conditions still apply." + ) + reference = ( + "https://www.nd.gov/dhs/policymanuals/40028/Content/400_28_20_40.htm", + "https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2024/CCAP%20ML%203819%20Effective%20June.1.2024.pdf", + "https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2024/CCAP%20ML%203862%20Effective%20September.1.2024.pdf", + "https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2025/CCAP%20ML%203909%20Effective%20May.1.2025.pdf", + "https://www.nd.gov/dhs/policymanuals/40028/Content/ML/2026/CCAP%20Release%2026.2%20Effective%20April.1.2026.pdf", + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py index f56dbe8f398..92679c3de3b 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py @@ -18,18 +18,7 @@ def formula(spm_unit, period, parameters): # co-payment (400-28-100-30). base_subsidy = spm_unit("nd_ccap_base_subsidy", period) person = spm_unit.members - # Reported care days or hours establish participation independently - # of the missing-hours pricing fallback. - weekly_hours = person("childcare_hours_per_week", period.this_year) - daily_hours = person("childcare_hours_per_day", period.this_year) - monthly_days = person("childcare_attending_days_per_month", period.this_year) - weekly_days = person("childcare_days_per_week", period.this_year) - in_care = ( - (weekly_hours > 0) - | (daily_hours > 0) - | (monthly_days > 0) - | (weekly_days > 0) - ) + in_care = person("nd_ccap_child_in_care", period) qris_step_bonus = spm_unit.sum( person("nd_ccap_qris_step_bonus", period) * in_care ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap_child_in_care.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap_child_in_care.py new file mode 100644 index 00000000000..69202c9a390 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap_child_in_care.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class nd_ccap_child_in_care(Variable): + value_type = bool + entity = Person + label = "North Dakota CCAP person attends child care" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + # Reported care days or hours establish participation independently + # of the missing-hours pricing fallback. + year = period.this_year + return ( + (person("childcare_hours_per_week", year) > 0) + | (person("childcare_hours_per_day", year) > 0) + | (person("childcare_attending_days_per_month", year) > 0) + | (person("childcare_days_per_week", year) > 0) + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py index d3082cf417c..3a02a714f9c 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py @@ -24,18 +24,7 @@ def formula(spm_unit, period, parameters): # payment outside the monthly care subsidy, and we don't track whether a # provider charges a registration fee or its amount at the moment. person = spm_unit.members - # Reported care days or hours establish participation independently - # of the missing-hours pricing fallback. - weekly_hours = person("childcare_hours_per_week", period.this_year) - daily_hours = person("childcare_hours_per_day", period.this_year) - monthly_days = person("childcare_attending_days_per_month", period.this_year) - weekly_days = person("childcare_days_per_week", period.this_year) - in_care = ( - (weekly_hours > 0) - | (daily_hours > 0) - | (monthly_days > 0) - | (weekly_days > 0) - ) + in_care = person("nd_ccap_child_in_care", period) maximum_monthly_rate = spm_unit.sum( person("nd_ccap_state_max_rate", period) * in_care )