diff --git a/changelog.d/mt-ccap.added.md b/changelog.d/mt-ccap.added.md new file mode 100644 index 00000000000..773fd29da50 --- /dev/null +++ b/changelog.d/mt-ccap.added.md @@ -0,0 +1 @@ +Add Montana Child Care Assistance Program (Best Beginnings Child Care Scholarship). diff --git a/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml b/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml index 748e5f5bffc..c396617d2f3 100644 --- a/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml +++ b/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml @@ -17,6 +17,7 @@ values: - ky_child_care_subsidies # Kentucky Child Care Assistance Program - ma_child_care_subsidies # Massachusetts Child Care Financial Assistance - me_child_care_subsidies # Maine Child Care Affordability Program + - mt_child_care_subsidies # Montana Best Beginnings Child Care Scholarship - ne_child_care_subsidies # Nebraska Child Care Subsidy - vt_child_care_subsidies # Vermont Child Care Financial Assistance Program - nh_child_care_subsidies # New Hampshire Child Care Scholarship Program diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/age_group/months.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/age_group/months.yaml new file mode 100644 index 00000000000..7e453098f69 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/age_group/months.yaml @@ -0,0 +1,28 @@ +description: Montana sets these child age group boundaries in months under the Best Beginnings Child Care Scholarship program. +# The published rate table lists three age groups (Infant/Toddler, Preschool, School +# Age) but states no month cutoffs. These boundaries are a documented assumption: +# 0-35 months = Infant/Toddler, 36-59 months = Preschool, 60+ months = School Age. +# Amounts are enum indices: 0 = INFANT_TODDLER, 1 = PRESCHOOL, 2 = SCHOOL_AGE. +metadata: + type: single_amount + threshold_unit: month + amount_unit: /1 + period: year + label: Montana CCAP child age group by months + reference: + - title: Best Beginnings Scholarship Half-Time & Full-Time Monthly Rates + href: https://dphhs.mt.gov/assets/ecfsd/childcare/documentsandresources/BBSProviderRatesMonthly.pdf + +brackets: + - threshold: + 2025-07-01: 0 + amount: + 2025-07-01: 0 + - threshold: + 2025-07-01: 36 + amount: + 2025-07-01: 1 + - threshold: + 2025-07-01: 60 + amount: + 2025-07-01: 2 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/copayment/copay_rate.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/copayment/copay_rate.yaml new file mode 100644 index 00000000000..b01f10369df --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/copayment/copay_rate.yaml @@ -0,0 +1,75 @@ +description: Montana sets this share of gross monthly income as the copayment under the Best Beginnings Child Care Scholarship program. +# The copayment share varies by the family's federal poverty guideline ratio. +# Sliding fee scale rows are in 5-point % FPG steps; each rate tier applies to income +# above the prior step's top up to and including the named step (e.g., the 1.0% tier +# covers income above 100% FPG through 110% FPG). Thresholds use the "+ 0.0001" shift +# so a family exactly at a step's top stays in the lower tier. The top tier (7.0%) +# runs through 200% FPG (graduated eligibility); above 200% the household is not +# eligible (gated separately by the income limit). +metadata: + type: single_amount + threshold_unit: /1 + amount_unit: /1 + period: year + label: Montana CCAP copayment rate by FPG ratio + reference: + - title: Best Beginnings Child Care Scholarship Sliding Fee Scale (effective 07-01-2025) + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/SlidingFeeScale07012023.pdf + +brackets: + - threshold: + 2025-07-01: 0 + amount: + 2025-07-01: 0.005 # Entry through 100% FPG + - threshold: + 2025-07-01: 1.0001 # Above 100% through 110% FPG + amount: + 2025-07-01: 0.01 + - threshold: + 2025-07-01: 1.1001 # Above 110% through 120% FPG + amount: + 2025-07-01: 0.015 + - threshold: + 2025-07-01: 1.2001 # Above 120% through 130% FPG + amount: + 2025-07-01: 0.02 + - threshold: + 2025-07-01: 1.3001 # Above 130% through 140% FPG + amount: + 2025-07-01: 0.025 + - threshold: + 2025-07-01: 1.4001 # Above 140% through 145% FPG + amount: + 2025-07-01: 0.03 + - threshold: + 2025-07-01: 1.4501 # Above 145% through 150% FPG + amount: + 2025-07-01: 0.035 + - threshold: + 2025-07-01: 1.5001 # Above 150% through 155% FPG + amount: + 2025-07-01: 0.04 + - threshold: + 2025-07-01: 1.5501 # Above 155% through 160% FPG + amount: + 2025-07-01: 0.045 + - threshold: + 2025-07-01: 1.6001 # Above 160% through 165% FPG + amount: + 2025-07-01: 0.05 + - threshold: + 2025-07-01: 1.6501 # Above 165% through 170% FPG + amount: + 2025-07-01: 0.055 + - threshold: + 2025-07-01: 1.7001 # Above 170% through 175% FPG + amount: + 2025-07-01: 0.06 + - threshold: + 2025-07-01: 1.7501 # Above 175% through 180% FPG + amount: + 2025-07-01: 0.065 + - threshold: + 2025-07-01: 1.8001 # Above 180% through 200% FPG + amount: + 2025-07-01: 0.07 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/copayment/minimum.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/copayment/minimum.yaml new file mode 100644 index 00000000000..e4048b2f252 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/copayment/minimum.yaml @@ -0,0 +1,11 @@ +description: Montana sets this minimum monthly copayment under the Best Beginnings Child Care Scholarship program. +values: + 2025-07-01: 10 + +metadata: + unit: currency-USD + period: month + label: Montana CCAP minimum monthly copayment + reference: + - title: Best Beginnings Child Care Scholarship Sliding Fee Scale (effective 07-01-2025) + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/SlidingFeeScale07012023.pdf diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/activity_hours/single_parent.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/activity_hours/single_parent.yaml new file mode 100644 index 00000000000..bab652d8c0e --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/activity_hours/single_parent.yaml @@ -0,0 +1,13 @@ +description: Montana requires single-parent families to work at least this many hours per month under the Best Beginnings Child Care Scholarship program. +values: + 2016-11-01: 60 + +metadata: + unit: hour + period: month + label: Montana CCAP single-parent monthly activity hours + reference: + - title: Child Care 2-3 Non-TANF Activity Requirements, Minimum Hourly Work Requirement + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/CC23NonTANFActivity070718.pdf#page=1 + - title: Mont. Admin. R. 37.80.201 + href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/activity_hours/two_parent.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/activity_hours/two_parent.yaml new file mode 100644 index 00000000000..a90201b917a --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/activity_hours/two_parent.yaml @@ -0,0 +1,13 @@ +description: Montana requires two-parent families to work at least this many hours per month under the Best Beginnings Child Care Scholarship program. +values: + 2016-11-01: 120 + +metadata: + unit: hour + period: month + label: Montana CCAP two-parent monthly activity hours + reference: + - title: Child Care 2-3 Non-TANF Activity Requirements, Minimum Hourly Work Requirement + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/CC23NonTANFActivity070718.pdf#page=1 + - title: Mont. Admin. R. 37.80.201 + href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/child_age_limit.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/child_age_limit.yaml new file mode 100644 index 00000000000..e10308356b2 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/child_age_limit.yaml @@ -0,0 +1,15 @@ +description: Montana limits eligibility to children under this age under the Best Beginnings Child Care Scholarship program. +# The manual references child eligibility in Policy Section 1-6 (not available); this +# is the CCDF default under-13 age limit (45 CFR 98.20). +values: + 2016-11-01: 13 + +metadata: + unit: year + period: year + label: Montana CCAP child age limit + reference: + - title: Mont. Admin. R. 37.80.201 + href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201 + - title: 45 CFR 98.20(a)(1) + href: https://www.ecfr.gov/current/title-45/section-98.20#p-98.20(a)(1) diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/special_needs_child_age_limit.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/special_needs_child_age_limit.yaml new file mode 100644 index 00000000000..c745f0e7a15 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/eligibility/special_needs_child_age_limit.yaml @@ -0,0 +1,15 @@ +description: Montana limits eligibility to children with special needs under this age under the Best Beginnings Child Care Scholarship program. +# The manual references child eligibility in Policy Section 1-6 (not available); this +# is the CCDF special-needs age limit (45 CFR 98.20, younger than 19). +values: + 2016-11-01: 19 + +metadata: + unit: year + period: year + label: Montana CCAP special needs child age limit + reference: + - title: Mont. Admin. R. 37.80.201 + href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201 + - title: 45 CFR 98.20(a)(1) + href: https://www.ecfr.gov/current/title-45/section-98.20#p-98.20(a)(1) diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/countable_income/sources.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/countable_income/sources.yaml new file mode 100644 index 00000000000..8251095a3fc --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/countable_income/sources.yaml @@ -0,0 +1,36 @@ +description: Montana uses these income sources as countable income under the Best Beginnings Child Care Scholarship program. +values: + 2016-11-01: + # Earned income (CC 2-6: Commissions/Salary/Wages, AmeriCorps VISTA, College + # Work Study, Internship, Jury Duty, Military Allotment, WIA-as-Work, Tips, + # Bonuses, Training Allowance, dependent child not attending school) + - employment_income + - self_employment_income # Self-employment, sale of blood, FFN/RCP provider, farm + - sstb_self_employment_income + - farm_operations_income + # Unearned income + - interest_income # Interest, dividend, royalty, annuity, trust fund, capital investment returns + - dividend_income + - rental_income # Rental/lease income, contract for deed + - social_security # SS and SSDI counted; SSI excluded (social_security omits SSI) + - pension_income # Government pension, annuity, railroad retirement, veterans' pensions + - retirement_distributions + - unemployment_compensation + - workers_compensation + - child_support_received # Child support retained, "in excess", arrearage + - alimony_income + - disability_benefits + - capital_gains # Counted as a lump sum + - veterans_benefits # Veterans' pensions and benefits (educational VA payments excluded) + # TANF cash (child-only/tribal) is excluded here to break the childcare<->TANF + # circular dependency (mt_tanf's dependent care deduction reads childcare_expenses, + # which nets out child_care_subsidies). TANF cash-assistance families are handled + # via the is_tanf_enrolled categorical pathway and the flat $10 copay instead. + +metadata: + unit: list + period: year + label: Montana CCAP countable income sources + reference: + - title: Child Care 2-6 Income Evaluation Table (pp. 1-14) + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/CC26IncomeTable070718.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/fpg_limit.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/fpg_limit.yaml new file mode 100644 index 00000000000..42c6355dad7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/fpg_limit.yaml @@ -0,0 +1,17 @@ +description: Montana limits gross income to this share of the federal poverty guideline under the Best Beginnings Child Care Scholarship program. +# Operational sliding fee scale value, not the 150% in the ARM 37.80.202 body text. +# The scale runs standard eligibility through 185% FPG and graduated eligibility +# (non-TANF over-income families) through 200% FPG; 200% is the outer eligibility +# ceiling. Graduated eligibility was added effective 11-01-2016. +values: + 2016-11-01: 2 + +metadata: + unit: /1 + period: year + label: Montana CCAP income limit share of federal poverty guideline + reference: + - title: Best Beginnings Child Care Scholarship Sliding Fee Scale (effective 07-01-2025) + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/SlidingFeeScale07012023.pdf + - title: Mont. Admin. R. 37.80.202 + href: https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.202 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/income_limit_smi_rate.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/income_limit_smi_rate.yaml new file mode 100644 index 00000000000..49090dff403 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/income/income_limit_smi_rate.yaml @@ -0,0 +1,12 @@ +description: Montana limits gross income to this share of state median income under the Best Beginnings Child Care Scholarship program. +# Households over 85% SMI are not eligible; this ceiling overlays the FPG brackets. +values: + 2016-11-01: 0.85 + +metadata: + unit: /1 + period: year + label: Montana CCAP income limit share of state median income + reference: + - title: Best Beginnings Child Care Scholarship Sliding Fee Scale (effective 07-01-2025) + href: https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/SlidingFeeScale07012023.pdf diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ccap/rates/provider_rates.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/rates/provider_rates.yaml new file mode 100644 index 00000000000..356f4ff3f57 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ccap/rates/provider_rates.yaml @@ -0,0 +1,112 @@ +description: Montana provides these maximum monthly reimbursement rates under the Best Beginnings Child Care Scholarship program. +# Rates vary by provider type, age group, and care time. +metadata: + period: month + unit: currency-USD + label: Montana CCAP maximum monthly provider rates + breakdown: + - mt_ccap_provider_type + - mt_ccap_child_age_category + - mt_ccap_care_time + reference: + - title: Best Beginnings Scholarship Half-Time & Full-Time Monthly Rates + href: https://dphhs.mt.gov/assets/ecfsd/childcare/documentsandresources/BBSProviderRatesMonthly.pdf + +CENTER: + INFANT_TODDLER: + HALF_TIME: + 2025-07-01: 638 + FULL_TIME: + 2025-07-01: 1_276 + PRESCHOOL: + HALF_TIME: + 2025-07-01: 605 + FULL_TIME: + 2025-07-01: 1_210 + SCHOOL_AGE: + HALF_TIME: + 2025-07-01: 550 + FULL_TIME: + 2025-07-01: 1_100 +GROUP_HOME: + INFANT_TODDLER: + HALF_TIME: + 2025-07-01: 528 + FULL_TIME: + 2025-07-01: 1_056 + PRESCHOOL: + HALF_TIME: + 2025-07-01: 495 + FULL_TIME: + 2025-07-01: 990 + SCHOOL_AGE: + HALF_TIME: + 2025-07-01: 495 + FULL_TIME: + 2025-07-01: 990 +FAMILY_HOME: + INFANT_TODDLER: + HALF_TIME: + 2025-07-01: 495 + FULL_TIME: + 2025-07-01: 990 + PRESCHOOL: + HALF_TIME: + 2025-07-01: 440 + FULL_TIME: + 2025-07-01: 880 + SCHOOL_AGE: + HALF_TIME: + 2025-07-01: 440 + FULL_TIME: + 2025-07-01: 880 +FFN: + INFANT_TODDLER: + HALF_TIME: + 2025-07-01: 423.5 + FULL_TIME: + 2025-07-01: 841.5 + PRESCHOOL: + HALF_TIME: + 2025-07-01: 374 + FULL_TIME: + 2025-07-01: 748 + SCHOOL_AGE: + HALF_TIME: + 2025-07-01: 374 + FULL_TIME: + 2025-07-01: 748 +RELATIVE_EXEMPT: + INFANT_TODDLER: + HALF_TIME: + 2025-07-01: 374 + FULL_TIME: + 2025-07-01: 742.5 + PRESCHOOL: + HALF_TIME: + 2025-07-01: 330 + FULL_TIME: + 2025-07-01: 660 + SCHOOL_AGE: + HALF_TIME: + 2025-07-01: 330 + FULL_TIME: + 2025-07-01: 660 +SCHOOL_AGE_PROVIDER: + # School Age Provider does not serve infants/toddlers; these cells are NA on the + # published rate table and are encoded as 0 (not payable). + INFANT_TODDLER: + HALF_TIME: + 2025-07-01: 0 + FULL_TIME: + 2025-07-01: 0 + PRESCHOOL: + HALF_TIME: + 2025-07-01: 286 + FULL_TIME: + 2025-07-01: 572 + SCHOOL_AGE: + HALF_TIME: + 2025-07-01: 286 + FULL_TIME: + 2025-07-01: 572 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 83ef1f1f1a0..d7ae92f98bb 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -554,6 +554,12 @@ programs: name: Maine CCAP full_name: Maine Child Care Affordability Program variable: me_ccap + - state: MT + status: complete + name: Montana BBCCS + full_name: Montana Best Beginnings Child Care Scholarship + variable: mt_child_care_subsidies + parameter_prefix: gov.states.mt.dphhs.ccap - state: IA status: complete name: Iowa CCA diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/integration.yaml new file mode 100644 index 00000000000..d58575f1b2a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/integration.yaml @@ -0,0 +1,339 @@ +# Integration tests for Montana Best Beginnings Child Care Scholarship (CCAP). +# End-to-end scenarios exercising age classification, the 3-D provider rate lookup, +# the sliding-scale copay, the TANF flat-$10 copay, the income test (200% FPG / +# 85% SMI), the activity requirement, and the final benefit + annual aggregator. +# 2026 (contiguous) FPG: size 2 = 21,640; size 3 = 27,320 (annual). +# Provider rates effective 07-01-2025 (BBS Provider Reimbursement Monthly Rates). + +- name: Case 1, single parent + preschooler at an FFN provider, full-time. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 28 + employment_income: 26_000 # size 2, ratio 26,000/21,640 = 1.2015 -> 2.0% + weekly_hours_worked: 40 # ~173 hrs/month >> 60 + person2: + age: 4 # 48 months -> PRESCHOOL + mt_ccap_provider_type: FFN + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 12_000 # 1,000/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_child_age_category: [SCHOOL_AGE, PRESCHOOL] + # person1 (adult, 336 months) lands in the top SCHOOL_AGE bracket; person2 is preschool + mt_ccap_eligible_child: [false, true] + mt_ccap_income_eligible: true # below 200% FPG and 85% SMI + mt_ccap_activity_eligible: true + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 748] # FFN preschool full-time = 748 + mt_ccap_countable_income: 2_166.67 # 26,000 / 12 + mt_ccap_copay: 43.33 # 2,166.67 * 2.0% + # charge 1,000 > max rate 748 -> capped at 748; benefit = 748 - 43.33 + mt_ccap: 704.67 + +- name: Case 2, two-parent household + infant at a group home, full-time, 150% FPG. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 33 + employment_income: 40_980 # size 3, ratio 40,980/27,320 = 1.50 -> 3.5% + weekly_hours_worked: 20 # ~86.7 hrs/month + person2: + age: 31 + weekly_hours_worked: 20 # combined ~173 hrs/month > 120 + person3: + age: 1 # 12 months -> INFANT_TODDLER + mt_ccap_provider_type: GROUP_HOME + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo for the child + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_child_age_category: [SCHOOL_AGE, SCHOOL_AGE, INFANT_TODDLER] + mt_ccap_eligible_child: [false, false, true] + mt_ccap_income_eligible: true + mt_ccap_activity_eligible: true # combined ~173 hrs/month >= 120 + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 0, 1_056] # group home infant/toddler full-time = 1,056 + mt_ccap_countable_income: 3_415 # 40,980 / 12 + mt_ccap_copay: 119.53 # 3,415 * 3.5% + # charge 2,000 > max rate 1,056 -> capped at 1,056; benefit = 1,056 - 119.53 + mt_ccap: 936.48 + +- name: Case 3, single parent + two children at a family home, 100% FPG. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 27_320 # size 3, ratio 1.00 -> 0.5% + weekly_hours_worked: 40 + person2: + age: 4 # PRESCHOOL + mt_ccap_provider_type: FAMILY_HOME + mt_ccap_care_time: FULL_TIME + person3: + age: 8 # SCHOOL_AGE + mt_ccap_provider_type: FAMILY_HOME + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 12,000/child -> 1,000/mo each + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_child_age_category: [SCHOOL_AGE, PRESCHOOL, SCHOOL_AGE] + mt_ccap_eligible_child: [false, true, true] + mt_ccap_income_eligible: true + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 880, 880] # family home preschool / school age full-time = 880 + mt_ccap_countable_income: 2_276.67 # 27,320 / 12 + mt_ccap_copay: 11.38 # 2,276.67 * 0.5% + # each child: min(1,000, 880) = 880; sum 1,760; benefit = 1,760 - 11.38 + mt_ccap: 1_748.62 + +- name: Case 4, TANF over-income family at a relative-care-exempt provider. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 29 + employment_income: 50_000 # above 185% FPG for size 2, but TANF-enrolled + weekly_hours_worked: 40 + person2: + age: 4 # PRESCHOOL + mt_ccap_provider_type: RELATIVE_EXEMPT + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: true + spm_unit_pre_subsidy_childcare_expenses: 18_000 # 1,500/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + mt_ccap_income_eligible: true # TANF-enrolled -> categorically income-eligible + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 660] # RCE preschool full-time = 660 + mt_ccap_copay: 10 # flat $10 for TANF recipients + # charge 1,500 > max rate 660 -> capped at 660; benefit = 660 - 10 + mt_ccap: 650 + +- name: Case 5, over-income non-TANF family is ineligible (no benefit). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 33 + employment_income: 60_000 # size 3, ratio 2.196 -> above 200% FPG (54,640) + weekly_hours_worked: 40 + person2: + age: 31 + weekly_hours_worked: 20 + person3: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_eligible_child: [false, false, true] + mt_ccap_income_eligible: false # above 200% FPG, below 85% SMI -> FPG limit binds + mt_ccap_eligible: false + mt_ccap: 0 + mt_child_care_subsidies: 0 + +- name: Case 6, half-time center care for an infant. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 27 + employment_income: 28_000 # size 2, ratio 1.2939 -> 2.0% + weekly_hours_worked: 40 + person2: + age: 1 # INFANT_TODDLER + mt_ccap_provider_type: CENTER + mt_ccap_care_time: HALF_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 12_000 # 1,000/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_child_age_category: [SCHOOL_AGE, INFANT_TODDLER] + mt_ccap_eligible_child: [false, true] + mt_ccap_income_eligible: true + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 638] # center infant/toddler half-time = 638 + mt_ccap_countable_income: 2_333.33 # 28,000 / 12 + mt_ccap_copay: 46.67 # 2,333.33 * 2.0% + # charge 1,000 > max rate 638 -> capped at 638; benefit = 638 - 46.67 + mt_ccap: 591.33 + +- name: Case 7, single parent + school-age child at a school-age provider; annual aggregator. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + employment_income: 24_000 # size 2, ratio 1.1091 -> 1.5% + weekly_hours_worked: 40 + person2: + age: 8 # 96 months -> SCHOOL_AGE + mt_ccap_provider_type: SCHOOL_AGE_PROVIDER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 9_000 # 750/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + # max rate (school age provider, school age, full-time) = 572 + # copay: GMI 2,000 * 1.5% = 30; charge 750 > 572 -> capped at 572 + # monthly benefit = 572 - 30 = 542; annual aggregator = 12 * 542 = 6,504 + mt_child_care_subsidies: 6_504 + +# --- Edge case integration scenarios: 185% standard ceiling, benefit floor at 0, +# and the highest single max rate (infant at a center, full-time). --- +- name: Case 8, family at the 185% FPG standard ceiling, center preschooler, full-time. + period: 2026-01 + absolute_error_margin: 0.1 + # ratio 40,034/21,640 = 1.85 -> top 7.0% copay bracket; still income-eligible (185% < 200% ceiling and below 85% SMI). + input: + people: + person1: + age: 31 + employment_income: 40_034 # size 2, exactly 185% of FPG (21,640 * 1.85) + weekly_hours_worked: 40 + person2: + age: 4 # PRESCHOOL + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 # 1,500/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + mt_ccap_income_eligible: true # 185% FPG < 200% ceiling, below 85% SMI + mt_ccap_activity_eligible: true + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 1_210] # center preschool full-time + mt_ccap_countable_income: 3_336.17 # 40,034 / 12 + mt_ccap_copay: 233.53 # 3,336.17 * 7.0% + # charge 1,500 > max rate 1,210 -> capped at 1,210; benefit = 1,210 - 233.53 + mt_ccap: 976.47 + +- name: Case 9, eligible family whose copay exceeds the small reimbursement (benefit floors at 0). + period: 2026-01 + absolute_error_margin: 0.1 + # high-income eligible family with a tiny provider charge; copay 233.53 > reimbursement 200 -> benefit floored at 0 even though the family is eligible. + input: + people: + person1: + age: 31 + employment_income: 40_034 # 185% FPG, 7.0% copay bracket + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 2_400 # 200/mo charge, below the copay + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 1_210] + mt_ccap_copay: 233.53 # 3,336.17 * 7.0% + # reimbursement = min(200, 1,210) = 200; max(200 - 233.53, 0) = 0 + mt_ccap: 0 + +- name: Case 10, infant at a center, full-time (highest single max rate). + period: 2026-01 + absolute_error_margin: 0.1 + # center infant/toddler full-time = 1,276, the highest cell on the rate table; low income -> $10-floor copay. + input: + people: + person1: + age: 26 + employment_income: 18_000 # size 2, ~83% FPG -> 0.5% bracket, copay at the $10 floor + weekly_hours_worked: 40 + person2: + age: 0 # newborn, INFANT_TODDLER + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo > 1,276 max rate + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_child_age_category: [SCHOOL_AGE, INFANT_TODDLER] + mt_ccap_eligible_child: [false, true] + mt_ccap_income_eligible: true + mt_ccap_eligible: true + mt_ccap_max_rate: [0, 1_276] # center infant/toddler full-time + mt_ccap_countable_income: 1_500 # 18,000 / 12 + mt_ccap_copay: 10 # 1,500 * 0.5% = 7.50 -> floored to the $10 minimum + # charge 2,000 > max rate 1,276 -> capped at 1,276; benefit = 1,276 - 10 + mt_ccap: 1_266 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap.yaml new file mode 100644 index 00000000000..9bb598df337 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap.yaml @@ -0,0 +1,254 @@ +# Unit tests for mt_ccap (SPMUnit, MONTH) benefit amount. +# Benefit = max(sum over eligible children of min(per-child charge, state max rate) +# - household copay, 0). +# Per-child charge comes from pre_subsidy_childcare_expenses, which prorates +# spm_unit_pre_subsidy_childcare_expenses (annual) evenly across children. +# 2026 (contiguous) FPG: size 2 = 21,640 annual; size 3 = 27,320 annual. +- name: Case 1, one child at a center, charge exceeds the max rate (capped at max rate). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # size 2, ~111% FPG -> 1.5% copay bracket + weekly_hours_worked: 40 + person2: + age: 4 # preschool + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 # 1,500/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + # max rate (center preschool full-time) = 1,210; charge 1,500 -> capped at 1,210 + # copay: GMI 2,000 * 1.5% = 30 + # benefit = 1,210 - 30 = 1,180 + mt_ccap: 1_180 + +- name: Case 2, one child, charge below the max rate (benefit capped at the charge). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 9_000 # 750/mo for the child + households: + household: + members: [person1, person2] + state_code: MT + output: + # charge 750 < max rate 1,210 -> min = 750 + # copay 30; benefit = 750 - 30 = 720 + mt_ccap: 720 + +- name: Case 3, two children at a center, both charges exceed the max rate. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 30_000 # size 3, ~110% FPG -> 1.0% copay bracket + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + person3: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 36_000 # 1,500/mo each child + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # each child capped at 1,210 -> sum 2,420 + # copay: GMI 2,500 * 1.0% = 25 + # benefit = 2,420 - 25 = 2,395 + mt_ccap: 2_395 + +- name: Case 4, TANF recipient pays the flat $10 copay. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: true + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # max rate 1,210; flat TANF copay 10 + # benefit = 1,210 - 10 = 1,200 + mt_ccap: 1_200 + +- name: Case 5, ineligible household (over income) receives no benefit. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 70_000 # above 200% FPG for size 2 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap: 0 + +- name: Case 6, no childcare expenses yields no benefit (zero charge minus copay floors at 0). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 0 + households: + household: + members: [person1, person2] + state_code: MT + output: + # min(0, 1,210) = 0; 0 - copay floored at 0 + mt_ccap: 0 + +# --- Edge cases: charge exactly at the max rate, half-time reimbursement, and the +# benefit floor at 0 when the copay exceeds the reimbursement. --- +- name: Case 7, provider charge exactly equal to the max rate (reimbursement is the max rate). + period: 2026-01 + absolute_error_margin: 0.1 + # charge == max rate; min(1,210, 1,210) = 1,210; copay 30 -> benefit 1,180. + input: + people: + person1: + age: 30 + employment_income: 24_000 # size 2, ratio 1.109 -> 1.5% copay bracket + weekly_hours_worked: 40 + person2: + age: 4 # preschool + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 14_520 # 1,210/mo, exactly the max rate + households: + household: + members: [person1, person2] + state_code: MT + output: + # min(1,210, 1,210) = 1,210; copay 2,000 * 1.5% = 30; benefit = 1,210 - 30 + mt_ccap: 1_180 + +- name: Case 8, half-time care reimburses at the half-time max rate. + period: 2026-01 + absolute_error_margin: 0.1 + # HALF_TIME direct input; center infant/toddler half-time max rate = 638. + input: + people: + person1: + age: 30 + employment_income: 24_000 # ratio 1.109 -> 1.5% + weekly_hours_worked: 40 + person2: + age: 1 # infant/toddler + mt_ccap_provider_type: CENTER + mt_ccap_care_time: HALF_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 12_000 # 1,000/mo > 638 half-time rate + households: + household: + members: [person1, person2] + state_code: MT + output: + # min(1,000, 638) = 638; copay 30; benefit = 638 - 30 + mt_ccap: 608 + +- name: Case 9, copay exceeds the reimbursement so the benefit floors at 0. + period: 2026-01 + absolute_error_margin: 0.1 + # high-income eligible family with a small provider charge; copay 205.83 > reimbursement 200 -> benefit floored at 0. + input: + people: + person1: + age: 30 + employment_income: 38_000 # size 2, ratio 1.756 -> 6.5% copay bracket + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 2_400 # 200/mo charge, below the copay + households: + household: + members: [person1, person2] + state_code: MT + output: + # reimbursement = min(200, 1,210) = 200 + # copay = 3,166.67 * 6.5% = 205.83 > 200 -> max(200 - 205.83, 0) = 0 + mt_ccap: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_activity_eligible.yaml new file mode 100644 index 00000000000..729942cd1cf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_activity_eligible.yaml @@ -0,0 +1,275 @@ +# Unit tests for mt_ccap_activity_eligible (SPMUnit, MONTH). +# Activity requirement: two-parent households 120 hrs/month total; single-parent +# 60 hrs/month, OR full-time student (no work requirement), OR part-time student +# plus 40 hrs/month work. weekly_hours_worked (Person, weekly) drives the work test; +# monthly hours = weekly hours * (52 / 12) ~= 4.333. +# person2 / person3 are children (age < 13). +- name: Case 1, single working parent well above 60 hrs/month is activity-eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 40 # ~173 hrs/month >> 60 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_activity_eligible: true + +- name: Case 2, single non-working non-student parent is not activity-eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 5 # ~21.7 hrs/month < 60 + is_full_time_student: false + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_activity_eligible: false + +- name: Case 3, single full-time student parent has no work requirement. + period: 2026-01 + input: + people: + person1: + age: 22 + weekly_hours_worked: 0 + is_full_time_student: true + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_activity_eligible: true + +- name: Case 4, two working parents above 120 hrs/month total are activity-eligible. + period: 2026-01 + input: + people: + person1: + age: 32 + weekly_hours_worked: 20 # ~86.7 hrs/month + person2: + age: 30 + weekly_hours_worked: 20 # ~86.7 hrs/month, combined ~173 > 120 + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_activity_eligible: true + +- name: Case 5, two parents with neither working and neither a student fail the activity test. + period: 2026-01 + input: + people: + person1: + age: 32 + weekly_hours_worked: 0 + is_full_time_student: false + person2: + age: 30 + weekly_hours_worked: 0 + is_full_time_student: false + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_activity_eligible: false + +# --- Edge cases: activity-hour boundaries (monthly hours = weekly * 52/12 = 4.3333). +# Single-parent threshold 60 hrs/mo; two-parent threshold 120 hrs/mo; both ">=". --- +- name: Case 6, single parent just above 60 hrs/month is activity-eligible. + period: 2026-01 + # 14 hrs/week * 4.3333 = 60.67 hrs/mo >= 60. + input: + people: + person1: + age: 30 + weekly_hours_worked: 14 # 60.67 hrs/mo + is_full_time_student: false + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_activity_eligible: true + +- name: Case 7, single parent just below 60 hrs/month is not activity-eligible. + period: 2026-01 + # 13 hrs/week * 4.3333 = 56.33 hrs/mo < 60. + input: + people: + person1: + age: 30 + weekly_hours_worked: 13 # 56.33 hrs/mo + is_full_time_student: false + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_activity_eligible: false + +- name: Case 8, two parents just above 120 hrs/month combined are activity-eligible. + period: 2026-01 + # each 14 hrs/week -> 28 combined * 4.3333 = 121.33 hrs/mo >= 120. + input: + people: + person1: + age: 32 + weekly_hours_worked: 14 + person2: + age: 30 + weekly_hours_worked: 14 # combined 28 hrs/week -> 121.33 hrs/mo + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_activity_eligible: true + +- name: Case 9, two parents just below 120 hrs/month combined are not activity-eligible. + period: 2026-01 + # each 13 hrs/week -> 26 combined * 4.3333 = 112.67 hrs/mo < 120. + input: + people: + person1: + age: 32 + weekly_hours_worked: 13 + person2: + age: 30 + weekly_hours_worked: 13 # combined 26 hrs/week -> 112.67 hrs/mo + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_activity_eligible: false + +- name: Case 10, two-parent household, only one a full-time student, still needs 120 work hours. + period: 2026-01 + # CC 2-3: the no-work-requirement waiver applies only when BOTH parents are full-time + # students. With just one student parent and ~87 combined work hours, the two-parent + # 120-hour test still governs, so the family is not activity-eligible. + input: + people: + person1: + age: 32 + weekly_hours_worked: 20 # 86.67 hrs/mo + is_full_time_student: false + person2: + age: 30 + weekly_hours_worked: 0 + is_full_time_student: true # student, but two-parent path ignores the exemption + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_activity_eligible: false + +- name: Case 11, household with no parents (only children) is not activity-eligible. + period: 2026-01 + # degenerate case; zero parent work hours, single-parent path with 0 >= 60 is false. + input: + people: + person1: + age: 10 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_activity_eligible: false + +# Regression test for the C1 fix: CC 2-3 states "there is no work requirement for +# either parent when both are attending school full time." The two-parent branch +# must apply the full-time-student waiver, not only the single-parent branch. +- name: Case 12, two-parent household with both parents full-time students has no work requirement. + period: 2026-01 + # Both parents are full-time students with zero work hours. The waiver applies to + # the two-parent branch, so the family is activity-eligible despite 0 < 120 hours. + input: + people: + person1: + age: 22 + weekly_hours_worked: 0 + is_full_time_student: true + person2: + age: 21 + weekly_hours_worked: 0 + is_full_time_student: true + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_activity_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_child_age_category.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_child_age_category.yaml new file mode 100644 index 00000000000..36620b66c3b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_child_age_category.yaml @@ -0,0 +1,128 @@ +# Unit tests for mt_ccap_child_age_category. +# Derived from age via age_group/months.yaml bracket: +# 0-35 months -> INFANT_TODDLER +# 36-59 months -> PRESCHOOL +# 60+ months -> SCHOOL_AGE +# age is a YEAR variable; the MONTH formula reads age * 12 for months. +- name: Case 1, infant under 3 (24 months) classified as infant/toddler. + period: 2026-01 + input: + people: + person1: + age: 2 # 24 months < 36 + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: INFANT_TODDLER + +- name: Case 2, newborn (0 months) classified as infant/toddler. + period: 2026-01 + input: + people: + person1: + age: 0 + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: INFANT_TODDLER + +- name: Case 3, three-year-old (36 months) classified as preschool. + period: 2026-01 + input: + people: + person1: + age: 3 # 36 months, boundary into preschool + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: PRESCHOOL + +- name: Case 4, four-year-old classified as preschool. + period: 2026-01 + input: + people: + person1: + age: 4 # 48 months, 36-59 range + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: PRESCHOOL + +- name: Case 5, five-year-old (60 months) classified as school age. + period: 2026-01 + input: + people: + person1: + age: 5 # 60 months, boundary into school age + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: SCHOOL_AGE + +- name: Case 6, ten-year-old classified as school age. + period: 2026-01 + input: + people: + person1: + age: 10 # 120 months + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: SCHOOL_AGE + +# --- Edge cases: bracket boundaries just-below the transition months --- +# age is a float; ages chosen so age*12 lands at month - 1 (just under the cutoff) +# to confirm the lower bracket still applies at threshold-1. +- name: Case 7, 35-month child (just below the 36-month Preschool cutoff) is infant/toddler. + period: 2026-01 + # Boundary at threshold-1; 35/12 = 2.9167 years -> 35 months < 36 stays INFANT_TODDLER. + input: + people: + person1: + age: 2.9167 # 35 months, one below the Preschool boundary + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: INFANT_TODDLER + +- name: Case 8, 59-month child (just below the 60-month School Age cutoff) is preschool. + period: 2026-01 + # Boundary at threshold-1; 59/12 = 4.9167 years -> 59 months < 60 stays PRESCHOOL. + input: + people: + person1: + age: 4.9167 # 59 months, one below the School Age boundary + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: PRESCHOOL + +- name: Case 9, adult parent age (336 months) lands in the top School Age bracket. + period: 2026-01 + # Documents that the age->category bracket has no upper cap; adults fall in SCHOOL_AGE (the default rate lookup is gated elsewhere by mt_ccap_eligible_child). + input: + people: + person1: + age: 28 # 336 months + households: + household: + members: [person1] + state_code: MT + output: + mt_ccap_child_age_category: SCHOOL_AGE diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_copay.yaml new file mode 100644 index 00000000000..a4ff1fc1d86 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_copay.yaml @@ -0,0 +1,278 @@ +# Unit tests for mt_ccap_copay (SPMUnit, MONTH). +# Non-TANF: copay = max(gross_monthly_income * copay_rate.calc(fpg_ratio), $10). +# TANF (is_tanf_enrolled): flat $10. +# fpg_ratio = countable monthly income / monthly FPG. +# 2026 FPG (contiguous), size 3 (1 parent + 2 children): 27,320 annual = 2,276.67/mo. +# Income inputs are annual; the MONTH formula reads the monthly portion. +- name: Case 1, low income copay floors at the $10 minimum. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 20_490 # 75% FPG; 0.5% of 1,707.50 = 8.54 < 10 + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 10 # max(8.54, 10) + +- name: Case 2, 100% FPG copay (0.5% bracket, just above the floor). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 27_320 # 100% FPG, ratio 1.0 -> 0.5% + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 11.38 # 2,276.67 * 0.005 = 11.38 + +- name: Case 3, 150% FPG copay (3.5% bracket). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 40_980 # 150% FPG, ratio 1.5 -> 3.5% + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 119.53 # 3,415.00 * 0.035 = 119.53 + +- name: Case 4, 185% FPG copay (top 7.0% bracket). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 50_542 # 185% FPG, ratio 1.85 -> 7.0% + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 294.83 # 4,211.83 * 0.07 = 294.83 + +- name: Case 5, 200% FPG graduated copay (7.0% bracket). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 54_640 # 200% FPG, ratio 2.0 -> 7.0% + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 318.73 # 4,553.33 * 0.07 = 318.73 + +- name: Case 6, TANF recipient pays the flat $10 copay regardless of income. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 40_980 # would be 119.53 on the scale + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: true + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 10 # flat $10 for TANF, not the 119.53 scale amount + +# --- Edge cases: $10 floor exactly, zero income, and FPG-step bracket boundaries. +# Size 3 monthly FPG = 27,320 / 12 = 2,276.67. The sliding scale uses a "+0.0001" +# threshold shift, so a family exactly at a step's top stays in the LOWER tier. --- +- name: Case 7, zero income copay floors at the $10 minimum. + period: 2026-01 + absolute_error_margin: 0.1 + # GMI 0 -> scale gives 0; max(0, 10) = 10. + input: + people: + person1: + employment_income: 0 + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 10 # max(0, 10) + +- name: Case 8, copay exactly at the $10 floor (scale amount equals the minimum). + period: 2026-01 + absolute_error_margin: 0.1 + # GMI 2,000/mo (24,000 annual, ~87.8% FPG) -> 0.5% bracket; 2,000 * 0.005 = 10 = minimum. + input: + people: + person1: + employment_income: 24_000 # 2,000/mo; 0.5% of 2,000 = 10 exactly + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 10 # max(10, 10) = 10 exactly at the floor + +- name: Case 9, exactly at 110% FPG stays in the 1.0% bracket (boundary, lower tier). + period: 2026-01 + absolute_error_margin: 0.1 + # ratio 30,052/27,320 = 1.10 exactly; threshold 1.0001 covers above 100% through 110% -> 1.0%. + input: + people: + person1: + employment_income: 30_052 # 110% of size-3 FPG (27,320 * 1.10) + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 25.04 # 2,504.33/mo * 0.01 + +- name: Case 10, just above 110% FPG jumps to the 1.5% bracket (boundary, upper tier). + period: 2026-01 + absolute_error_margin: 0.1 + # ratio 30,500/27,320 = 1.1164 > 1.1001 -> 1.5% bracket; confirms the step transition above 110%. + input: + people: + person1: + employment_income: 30_500 # ~111.6% FPG, just above the 110% step + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 38.13 # 2,541.67/mo * 0.015 + +- name: Case 11, exactly at 120% FPG stays in the 1.5% bracket (boundary, lower tier). + period: 2026-01 + absolute_error_margin: 0.1 + # ratio 32,784/27,320 = 1.20 exactly; threshold 1.1001 covers above 110% through 120% -> 1.5%. + input: + people: + person1: + employment_income: 32_784 # 120% of size-3 FPG (27,320 * 1.20) + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 40.98 # 2,732.00/mo * 0.015 + +- name: Case 12, just above 120% FPG jumps to the 2.0% bracket (boundary, upper tier). + period: 2026-01 + absolute_error_margin: 0.1 + # ratio 33,000/27,320 = 1.2079 > 1.2001 -> 2.0% bracket; confirms the step transition above 120%. + input: + people: + person1: + employment_income: 33_000 # ~120.8% FPG, just above the 120% step + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_copay: 55.00 # 2,750.00/mo * 0.02 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_countable_income.yaml new file mode 100644 index 00000000000..320fb48e804 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_countable_income.yaml @@ -0,0 +1,148 @@ +# Unit tests for mt_ccap_countable_income (SPMUnit, MONTH). +# Sums the income sources listed in income/countable_income/sources.yaml. +# SSI is NOT in the list (social_security omits SSI), so it must be excluded. +# Income inputs are YEAR variables; in a MONTH-period test they are entered as +# annual amounts and the output is the monthly value (annual / 12). +- name: Case 1, employment income only. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 24_000 # annual + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 2_000 # 24,000 / 12 + +- name: Case 2, earned plus unearned (Social Security counted). + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 12_000 # annual + social_security: 6_000 # annual, counted + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 1_500 # (12,000 + 6,000) / 12 + +- name: Case 3, SSI is excluded from countable income. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 12_000 # annual, counted + ssi: 9_000 # annual, NOT counted + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 1_000 # 12,000 / 12 (SSI excluded) + +- name: Case 4, child support and unemployment compensation counted. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + employment_income: 0 + child_support_received: 3_600 # annual, counted + unemployment_compensation: 6_000 # annual, counted + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 800 # (3,600 + 6,000) / 12 + +# --- Edge cases: zero, negative self-employment, additional sources --- +- name: Case 5, zero income yields zero countable income. + period: 2026-01 + absolute_error_margin: 0.1 + # Degenerate zero-income case; the source sum is 0. + input: + people: + person1: + employment_income: 0 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 0 + +- name: Case 6, negative self-employment loss offsets earned income in the source sum. + period: 2026-01 + absolute_error_margin: 0.1 + # A self-employment loss reduces the summed countable income; the sum is not floored at the source level. + input: + people: + person1: + employment_income: 24_000 # annual, counted + self_employment_income: -6_000 # annual loss, reduces the sum + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 1_500 # (24,000 - 6,000) / 12 + +- name: Case 7, capital gains and veterans benefits counted. + period: 2026-01 + absolute_error_margin: 0.1 + # Both capital_gains (lump sum) and veterans_benefits are listed countable sources. + input: + people: + person1: + employment_income: 0 + capital_gains: 12_000 # annual, counted lump sum + veterans_benefits: 6_000 # annual, counted + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_countable_income: 1_500 # (12,000 + 6,000) / 12 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_eligible.yaml new file mode 100644 index 00000000000..27cd67f3dc7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_eligible.yaml @@ -0,0 +1,230 @@ +# Unit tests for mt_ccap_eligible (SPMUnit, MONTH). +# Eligible = has >=1 eligible child AND income-eligible AND asset-eligible AND +# activity-eligible. Scoped to Montana (defined_for = StateCode.MT). +# Base eligible household: single working parent + one young child, low income. +- name: Case 1, eligible single-parent household. + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 30_000 # ~110% FPG for size 2 + weekly_hours_worked: 40 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: true + +- name: Case 2, identical household outside Montana is not eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 30_000 + weekly_hours_worked: 40 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + households: + household: + members: [person1, person2] + state_code: WY # not Montana + output: + mt_ccap_eligible: false + +- name: Case 3, no eligible child (only an over-age child) is not eligible. + period: 2026-01 + input: + people: + person1: + age: 35 + employment_income: 30_000 + weekly_hours_worked: 40 + person2: + age: 14 # over the age limit, not disabled + is_disabled: false + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: false + +- name: Case 4, over-income household (above 200% FPG, non-TANF) is not eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 70_000 # above 200% FPG for size 2 + weekly_hours_worked: 40 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: false + +- name: Case 5, fails the activity requirement (non-working non-student parent). + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 6_000 # low income, eligible on income + weekly_hours_worked: 0 + is_full_time_student: false + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: false + +- name: Case 6, fails the CCDF asset test (assets over $1,000,000). + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 30_000 + weekly_hours_worked: 40 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_assets: 1_500_000 # over the CCDF $1,000,000 limit + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: false + +# --- Edge cases: asset-limit boundary, TANF over-income categorical pathway, +# immigration-ineligible child, and a multi-child household. --- +- name: Case 7, assets exactly at the $1,000,000 CCDF limit are eligible (inclusive). + period: 2026-01 + # asset test uses "<="; assets == limit passes. + input: + people: + person1: + age: 30 + employment_income: 30_000 + weekly_hours_worked: 40 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_assets: 1_000_000 # exactly at the CCDF limit + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: true + +- name: Case 8, TANF-enrolled over-income family is eligible via the categorical pathway. + period: 2026-01 + # income above 200% FPG, but is_tanf_enrolled makes the family income-eligible; all other gates pass. + input: + people: + person1: + age: 30 + employment_income: 70_000 # above 200% FPG for size 2 + weekly_hours_worked: 40 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: true + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: true + +- name: Case 9, undocumented child leaves the household with no eligible child (ineligible). + period: 2026-01 + # the only child fails CCDF immigration eligibility, so has_eligible_child is false. + input: + people: + person1: + age: 30 + employment_income: 30_000 + weekly_hours_worked: 40 + person2: + age: 4 + immigration_status: UNDOCUMENTED + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible: false + +- name: Case 10, multi-child eligible household (one of several children qualifies). + period: 2026-01 + # large household with mixed-age children; at least one eligible child plus all gates pass. + input: + people: + person1: + age: 35 + employment_income: 30_000 # within income limits for size 5 + weekly_hours_worked: 40 + person2: + age: 2 + person3: + age: 6 + person4: + age: 10 + person5: + age: 15 # over the age limit, not an eligible child, but others qualify + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: MT + output: + mt_ccap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_eligible_child.yaml new file mode 100644 index 00000000000..48e9437281e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_eligible_child.yaml @@ -0,0 +1,150 @@ +# Unit tests for mt_ccap_eligible_child (Person, MONTH). +# A child is eligible if under the age limit (13, or 19 if disabled) and meets +# CCDF immigration eligibility (is_ccdf_immigration_eligible_child). +# person1 is a parent in each case so the child is a tax-unit dependent. +- name: Case 1, young child under the age limit is an eligible child. + period: 2026-01 + input: + people: + person1: + age: 35 + person2: + age: 4 + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + # person1 is an adult (over the age limit); person2 is an eligible child. + +- name: Case 2, child at the age limit (13) is not eligible (non-disabled). + period: 2026-01 + input: + people: + person1: + age: 35 + person2: + age: 13 # at the limit; eligibility is age < 13 + is_disabled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, false] + +- name: Case 3, child just under the age limit (12) is eligible. + period: 2026-01 + input: + people: + person1: + age: 35 + person2: + age: 12 + is_disabled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + +- name: Case 4, disabled child aged 14 is eligible under the special-needs limit (19). + period: 2026-01 + input: + people: + person1: + age: 40 + person2: + age: 14 + is_disabled: true + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + +- name: Case 5, disabled child at the special-needs limit (19) is not eligible. + period: 2026-01 + input: + people: + person1: + age: 45 + person2: + age: 19 # at the special-needs limit; eligibility is age < 19 + is_disabled: true + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, false] + +- name: Case 6, undocumented child fails CCDF immigration eligibility. + period: 2026-01 + input: + people: + person1: + age: 35 + person2: + age: 4 + immigration_status: UNDOCUMENTED + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, false] + +# --- Edge cases: newborn, and the special-needs age limit boundary at limit-1. --- +- name: Case 7, newborn (age 0) is an eligible child. + period: 2026-01 + # youngest possible child; age 0 < 13 limit. + input: + people: + person1: + age: 35 + person2: + age: 0 + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + +- name: Case 8, disabled child aged 18 (just under the special-needs limit) is eligible. + period: 2026-01 + # special-needs boundary at limit-1; age 18 < 19 limit for disabled children. + input: + people: + person1: + age: 45 + person2: + age: 18 + is_disabled: true + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, true] + +- name: Case 9, non-disabled child aged 14 exceeds the standard limit (only disabled get the higher limit). + period: 2026-01 + # confirms the disabled vs non-disabled age-limit branch; 14 >= 13 standard limit. + input: + people: + person1: + age: 40 + person2: + age: 14 + is_disabled: false + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_eligible_child: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_income_eligible.yaml new file mode 100644 index 00000000000..0c6086a48c8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_income_eligible.yaml @@ -0,0 +1,225 @@ +# Unit tests for mt_ccap_income_eligible (SPMUnit, MONTH). +# Eligible if (countable income <= 200% FPG) AND (countable income <= 85% SMI), +# OR the household is TANF-enrolled (categorically income-eligible). +# 2026 (contiguous), size 3 (1 parent + 2 children): +# 200% FPG = 54,640 annual +# 85% SMI = 77,167 annual (FPG limit binds first for this size) +# Income inputs are annual; the MONTH formula compares monthly portions. +- name: Case 1, income well below 200% FPG is eligible. + period: 2026-01 + input: + people: + person1: + employment_income: 30_000 # ~110% FPG + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: true + +- name: Case 2, income just below the 200% FPG ceiling is eligible. + period: 2026-01 + input: + people: + person1: + employment_income: 54_000 # below 200% FPG (54,640) and below 85% SMI + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: true + +- name: Case 3, income above 200% FPG (but below 85% SMI) is ineligible. + period: 2026-01 + input: + people: + person1: + employment_income: 58_000 # above 200% FPG (54,640), below 85% SMI (77,167) + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: false + +- name: Case 4, income above 85% SMI is ineligible (SMI ceiling). + period: 2026-01 + input: + people: + person1: + employment_income: 80_000 # above 85% SMI (77,167) and above 200% FPG + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: false + +- name: Case 5, TANF recipient over the FPG limit is still income-eligible. + period: 2026-01 + input: + people: + person1: + employment_income: 58_000 # above 200% FPG, but TANF-enrolled + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: true + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: true + +# --- Edge cases: 200% FPG graduated ceiling at limit-1 / limit / limit+1. +# Size 3 (1 parent + 2 children) 2026: 200% FPG = 54,640 annual. The formula uses +# "<=", so income exactly at the limit is eligible. 85% SMI (~77,167) does not bind +# at this size, so the FPG limit is the operative ceiling. --- +- name: Case 6, income exactly at the 200% FPG limit is eligible (inclusive boundary). + period: 2026-01 + # countable income == 200% FPG; "<=" means at-limit is eligible. + input: + people: + person1: + employment_income: 54_640 # exactly 200% of size-3 FPG (27,320 * 2) + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: true + +- name: Case 7, income just below the 200% FPG limit is eligible. + period: 2026-01 + # one dollar (annual) below the ceiling. + input: + people: + person1: + employment_income: 54_639 # one below 200% FPG + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: true + +- name: Case 8, income just above the 200% FPG limit is ineligible (non-TANF). + period: 2026-01 + # just over the graduated ceiling; FPG limit binds before 85% SMI at size 3. + input: + people: + person1: + employment_income: 54_720 # ~$80 (annual) above 200% FPG to clear the 0.1 monthly margin + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: false + +- name: Case 9, zero income is income-eligible. + period: 2026-01 + # degenerate zero-income case; well within all ceilings. + input: + people: + person1: + employment_income: 0 + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: true + +- name: Case 10, very high income exceeds both the FPG and SMI ceilings (ineligible). + period: 2026-01 + # maximum-income case; far above both 200% FPG and 85% SMI. + input: + people: + person1: + employment_income: 500_000 + person2: + age: 4 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_ccap_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_max_rate.yaml new file mode 100644 index 00000000000..eac7fa38620 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_ccap_max_rate.yaml @@ -0,0 +1,309 @@ +# Unit tests for mt_ccap_max_rate (Person, MONTH). +# 3-D lookup: p.rates.provider_rates[provider_type][age_category][care_time]. +# Values from the BBS Provider Reimbursement Monthly Rates table (effective 07-01-2025). +# Covers every provider type, both age-group endpoints, both care times, and the +# RCE / School-Age-Provider NA (= 0) cells. +# Each case has person1 = parent and person2 = the child whose rate is asserted, so +# the child is unambiguously an eligible dependent child. +# Age sets the age group: age 1 (12 mo) INFANT_TODDLER, age 4 (48 mo) PRESCHOOL, +# age 8 (96 mo) SCHOOL_AGE. care_time defaults to FULL_TIME. + +# --- Child Care Center --- +- name: Case 1, center infant/toddler full-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 1_276] + +- name: Case 2, center infant/toddler half-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: HALF_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 638] + +- name: Case 3, center school age full-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 8 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 1_100] + +# --- Group Home --- +- name: Case 4, group home preschool full-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + mt_ccap_provider_type: GROUP_HOME + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 990] + +- name: Case 5, group home infant/toddler half-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: GROUP_HOME + mt_ccap_care_time: HALF_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 528] + +# --- Family Home --- +- name: Case 6, family home preschool full-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + mt_ccap_provider_type: FAMILY_HOME + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 880] + +# --- Family, Friend & Neighbor (FFN) --- +- name: Case 7, FFN infant/toddler full-time (fractional rate). + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: FFN + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 841.5] + +- name: Case 8, FFN infant/toddler half-time (fractional rate). + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: FFN + mt_ccap_care_time: HALF_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 423.5] + +# --- Relative Care Exempt (RCE) --- +- name: Case 9, RCE infant/toddler full-time (fractional rate). + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: RELATIVE_EXEMPT + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 742.5] + +- name: Case 10, RCE preschool full-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + mt_ccap_provider_type: RELATIVE_EXEMPT + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 660] + +# --- School Age Provider --- +- name: Case 11, school age provider school age full-time. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 8 + mt_ccap_provider_type: SCHOOL_AGE_PROVIDER + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 572] + +- name: Case 12, school age provider infant/toddler is NA (encoded 0, not payable). + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: SCHOOL_AGE_PROVIDER + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 0] + +# --- Edge cases: half-time as a direct input (no care_time formula exists), plus +# the remaining NA / fractional / school-age cells to round out dimension coverage. --- +- name: Case 13, RCE infant/toddler half-time (fractional rate, direct HALF_TIME input). + period: 2026-01 + # care_time is a bare input (no formula); HALF_TIME selects the half-time RCE infant/toddler rate 374. + input: + people: + person1: + age: 30 + person2: + age: 1 + mt_ccap_provider_type: RELATIVE_EXEMPT + mt_ccap_care_time: HALF_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 374] + +- name: Case 14, school age provider school age half-time. + period: 2026-01 + # HALF_TIME direct input; school age provider school age half-time = 286. + input: + people: + person1: + age: 30 + person2: + age: 8 + mt_ccap_provider_type: SCHOOL_AGE_PROVIDER + mt_ccap_care_time: HALF_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 286] + +- name: Case 15, school age provider preschool full-time (the only non-zero SAP preschool cell). + period: 2026-01 + # School age providers may serve preschoolers; preschool full-time = 572. + input: + people: + person1: + age: 30 + person2: + age: 4 + mt_ccap_provider_type: SCHOOL_AGE_PROVIDER + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 572] + +- name: Case 16, group home school age full-time (highest age group, group home). + period: 2026-01 + # Group home school age full-time = 990 (same as preschool for group home). + input: + people: + person1: + age: 30 + person2: + age: 8 + mt_ccap_provider_type: GROUP_HOME + mt_ccap_care_time: FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 990] + +- name: Case 17, default provider type and care time (no inputs) uses CENTER full-time. + period: 2026-01 + # provider_type defaults to CENTER and care_time defaults to FULL_TIME; preschool child -> 1,210. + input: + people: + person1: + age: 30 + person2: + age: 4 # preschool, no provider/care inputs -> CENTER FULL_TIME + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_ccap_max_rate: [0, 1_210] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_child_care_subsidies.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_child_care_subsidies.yaml new file mode 100644 index 00000000000..0632d069451 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ccap/mt_child_care_subsidies.yaml @@ -0,0 +1,108 @@ +# Unit tests for mt_child_care_subsidies (SPMUnit, YEAR). +# State aggregator that sums the monthly mt_ccap benefit across the year and feeds +# the federal child_care_subsidies total. With constant monthly inputs the annual +# value is 12 * the monthly mt_ccap benefit. +- name: Case 1, eligible single-child household annual subsidy. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + # monthly mt_ccap = 1,180 (see mt_ccap.yaml Case 1) -> annual 12 * 1,180 = 14,160 + mt_child_care_subsidies: 14_160 + +- name: Case 2, ineligible household receives no annual subsidy. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 70_000 # above 200% FPG for size 2 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_child_care_subsidies: 0 + +# --- Edge cases: TANF flat-$10 copay and a non-Montana household, both through the +# annual aggregator. --- +- name: Case 3, TANF-enrolled household annual subsidy (flat $10 copay flows through). + period: 2026 + absolute_error_margin: 0.1 + # monthly mt_ccap = 1,200 (max rate 1,210 - flat $10 TANF copay); annual = 12 * 1,200. + input: + people: + person1: + age: 30 + employment_income: 24_000 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: true + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2] + state_code: MT + output: + mt_child_care_subsidies: 14_400 # 12 * 1,200 + +- name: Case 4, identical eligible household outside Montana receives no subsidy. + period: 2026 + absolute_error_margin: 0.1 + # defined_for = StateCode.MT zeroes the aggregator for non-MT households. + input: + people: + person1: + age: 30 + employment_income: 24_000 + weekly_hours_worked: 40 + person2: + age: 4 + mt_ccap_provider_type: CENTER + mt_ccap_care_time: FULL_TIME + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 18_000 + households: + household: + members: [person1, person2] + state_code: WY # not Montana + output: + mt_child_care_subsidies: 0 diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_activity_eligible.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_activity_eligible.py new file mode 100644 index 00000000000..0ace53bb6c2 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_activity_eligible.py @@ -0,0 +1,48 @@ +from policyengine_us.model_api import * + + +class mt_ccap_activity_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Activity eligible for Montana Best Beginnings Child Care Scholarship" + definition_period = MONTH + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201", + "https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/CC23NonTANFActivity070718.pdf#page=1", + ) + + def formula(spm_unit, period, parameters): + # CC 2-3: two-parent families need 120 activity hours/month total; + # single-parent families need 60 work hours/month, or full-time + # school/training (no work requirement), or part-time school plus 40 + # work hours/month. We don't track part-time vs full-time student + # status at the moment, so the part-time-student + 40-work-hours + # pathway is treated the same as full-time student. The teen-parent- + # in-school no-work exception and the exclusion of disabled parents + # from the minimum-hour computation are also not modeled (no per-parent + # inputs). + p = parameters(period).gov.states.mt.dphhs.ccap.eligibility.activity_hours + person = spm_unit.members + is_parent = person("is_tax_unit_head_or_spouse", period.this_year) + monthly_hours = ( + person("weekly_hours_worked", period.this_year) + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR + ) + is_full_time_student = person("is_full_time_student", period.this_year) + + parent_work_hours = spm_unit.sum(monthly_hours * is_parent) + parent_count = spm_unit.sum(is_parent) + full_time_student_parents = spm_unit.sum(is_parent & is_full_time_student) + two_parent = parent_count > 1 + single_parent_is_full_time_student = full_time_student_parents > 0 + both_parents_full_time_students = full_time_student_parents == parent_count + + two_parent_eligible = ( + parent_work_hours >= p.two_parent + ) | both_parents_full_time_students + single_parent_eligible = ( + parent_work_hours >= p.single_parent + ) | single_parent_is_full_time_student + return where(two_parent, two_parent_eligible, single_parent_eligible) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_eligible.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_eligible.py new file mode 100644 index 00000000000..c7565691558 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_eligible.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class mt_ccap_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for Montana Best Beginnings Child Care Scholarship" + definition_period = MONTH + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201", + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.202", + ) + + def formula(spm_unit, period, parameters): + # Modeled pathways: non-TANF income-eligible working families and + # TANF cash-assistance families (via is_tanf_enrolled). Tribal, + # tribal-TANF, and CFSD-referral pathways are not modeled at the moment + # (no tribal-enrollment or CFSD-referral inputs). + has_eligible_child = add(spm_unit, period, ["mt_ccap_eligible_child"]) > 0 + income_eligible = spm_unit("mt_ccap_income_eligible", period) + asset_eligible = spm_unit("is_ccdf_asset_eligible", period.this_year) + activity_eligible = spm_unit("mt_ccap_activity_eligible", period) + return has_eligible_child & income_eligible & asset_eligible & activity_eligible diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_eligible_child.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_eligible_child.py new file mode 100644 index 00000000000..6c9ed1a7d28 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_eligible_child.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class mt_ccap_eligible_child(Variable): + value_type = bool + entity = Person + label = "Eligible child for Montana Best Beginnings Child Care Scholarship" + definition_period = MONTH + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.201", + "https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/CC21Eligibility070718.pdf#page=4", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.mt.dphhs.ccap.eligibility + age = person("age", period.this_year) + is_disabled = person("is_disabled", period.this_year) + age_limit = where( + is_disabled, p.special_needs_child_age_limit, p.child_age_limit + ) + age_eligible = age < age_limit + immigration_eligible = person( + "is_ccdf_immigration_eligible_child", period.this_year + ) + return age_eligible & immigration_eligible diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_income_eligible.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_income_eligible.py new file mode 100644 index 00000000000..a8b25feed05 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/eligibility/mt_ccap_income_eligible.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class mt_ccap_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Income eligible for Montana Best Beginnings Child Care Scholarship" + definition_period = MONTH + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.202", + "https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/SlidingFeeScale07012023.pdf", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.mt.dphhs.ccap.income + countable_income = spm_unit("mt_ccap_countable_income", period) + # Operational sliding fee scale runs through 200% FPG (graduated tier); + # the ARM body text 150% standard limit is superseded by the published + # scale per the modeling decision. + fpg = spm_unit("spm_unit_fpg", period) + fpg_eligible = countable_income <= fpg * p.fpg_limit + # The sliding fee scale footnote sets an over-85%-SMI hard ineligibility + # ceiling that overlays the FPG brackets. + smi = spm_unit("hhs_smi", period) + smi_eligible = countable_income <= smi * p.income_limit_smi_rate + income_eligible = fpg_eligible & smi_eligible + # TANF cash-assistance families are categorically income eligible. + tanf_enrolled = spm_unit("is_tanf_enrolled", period) + return income_eligible | tanf_enrolled diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap.py new file mode 100644 index 00000000000..cf785f5890e --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class mt_ccap(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "Montana Best Beginnings Child Care Scholarship benefit amount" + definition_period = MONTH + defined_for = "mt_ccap_eligible" + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.202", + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.205", + ) + + def formula(spm_unit, period, parameters): + # ARM 37.80.202/205: the department pays the lesser of the provider's + # usual and customary charge or the state maximum rate for each eligible + # child, minus the parent copayment. We don't track per-day attendance + # at the moment, so full-month authorization is assumed (the 85% + # attendance monthly-authorization rule and per-day billing are not + # modeled). + person = spm_unit.members + is_eligible_child = person("mt_ccap_eligible_child", period) + max_rate = person("mt_ccap_max_rate", period) + pre_subsidy_expense = person("pre_subsidy_childcare_expenses", period) + per_child_reimbursement = ( + min_(pre_subsidy_expense, max_rate) * is_eligible_child + ) + total_reimbursement = spm_unit.sum(per_child_reimbursement) + copay = spm_unit("mt_ccap_copay", period) + return max_(total_reimbursement - copay, 0) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_care_time.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_care_time.py new file mode 100644 index 00000000000..bdc16cdda01 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_care_time.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class MTCCAPCareTime(Enum): + FULL_TIME = "Full-Time" + HALF_TIME = "Half-Time" + + +class mt_ccap_care_time(Variable): + value_type = Enum + entity = Person + possible_values = MTCCAPCareTime + default_value = MTCCAPCareTime.FULL_TIME + definition_period = MONTH + defined_for = StateCode.MT + label = "Montana Best Beginnings Child Care Scholarship care time" + reference = "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.205" + + # ARM 37.80.205 sets half-time as 5 or fewer hours of care per day and + # full-time as more than 5 to 12 hours per day. We don't track per-day care + # hours at the moment, so this defaults to full-time. Extended care + # (12-18 hours = 1.25x, over 18 hours = 1.5x) is not modeled. diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_child_age_category.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_child_age_category.py new file mode 100644 index 00000000000..8b68575d426 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_child_age_category.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class MTCCAPAgeGroup(Enum): + INFANT_TODDLER = "Infant/Toddler" + PRESCHOOL = "Preschool" + SCHOOL_AGE = "School Age" + + +class mt_ccap_child_age_category(Variable): + value_type = Enum + entity = Person + possible_values = MTCCAPAgeGroup + default_value = MTCCAPAgeGroup.PRESCHOOL + definition_period = MONTH + defined_for = StateCode.MT + label = "Montana Best Beginnings Child Care Scholarship child age category" + reference = "https://dphhs.mt.gov/assets/ecfsd/childcare/documentsandresources/BBSProviderRatesMonthly.pdf" + + def formula(person, period, parameters): + # The published rate table lists three age groups but states no month + # cutoffs; the boundaries (0-35mo Infant/Toddler, 36-59mo Preschool, + # 60mo+ School Age) are a documented assumption in age_group/months.yaml. + age_in_months = person("age", period.this_year) * MONTHS_IN_YEAR + p = parameters(period).gov.states.mt.dphhs.ccap.age_group + return p.months.calc(age_in_months) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_copay.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_copay.py new file mode 100644 index 00000000000..a5b665264bd --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_copay.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class mt_ccap_copay(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "Montana Best Beginnings Child Care Scholarship monthly copayment" + definition_period = MONTH + defined_for = "mt_ccap_eligible" + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.202", + "https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/SlidingFeeScale07012023.pdf", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.mt.dphhs.ccap.copayment + gmi = spm_unit("mt_ccap_countable_income", period) + fpg = spm_unit("spm_unit_fpg", period) + fpg_ratio = where(fpg > 0, gmi / fpg, 0) + + # Non-TANF families pay the greater of GMI times the sliding-scale + # percentage or the $10 minimum. + sliding_copay = max_(gmi * p.copay_rate.calc(fpg_ratio), p.minimum) + # TANF cash-assistance recipients pay a flat $10 minimum copayment. + tanf_enrolled = spm_unit("is_tanf_enrolled", period) + return where(tanf_enrolled, p.minimum, sliding_copay) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_countable_income.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_countable_income.py new file mode 100644 index 00000000000..ab8ba9f362e --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_countable_income.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class mt_ccap_countable_income(Variable): + value_type = float + entity = SPMUnit + label = "Montana Best Beginnings Child Care Scholarship countable income" + definition_period = MONTH + unit = USD + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.102", + "https://dphhs.mt.gov/assets/ecfsd/childcare/policymanual/CC26IncomeTable070718.pdf#page=1", + ) + + adds = "gov.states.mt.dphhs.ccap.income.countable_income.sources" diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_max_rate.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_max_rate.py new file mode 100644 index 00000000000..c7f0e97aed9 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_max_rate.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class mt_ccap_max_rate(Variable): + value_type = float + entity = Person + unit = USD + label = "Montana Best Beginnings Child Care Scholarship maximum monthly rate" + definition_period = MONTH + defined_for = "mt_ccap_eligible_child" + reference = ( + "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.205", + "https://dphhs.mt.gov/assets/ecfsd/childcare/documentsandresources/BBSProviderRatesMonthly.pdf", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.mt.dphhs.ccap + provider_type = person("mt_ccap_provider_type", period) + age_category = person("mt_ccap_child_age_category", period) + care_time = person("mt_ccap_care_time", period) + return p.rates.provider_rates[provider_type][age_category][care_time] diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_provider_type.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_provider_type.py new file mode 100644 index 00000000000..5b035b5184f --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_ccap_provider_type.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class MTCCAPProviderType(Enum): + CENTER = "Child Care Center" + GROUP_HOME = "Group Home" + FAMILY_HOME = "Family Home" + FFN = "Family, Friend & Neighbor" + RELATIVE_EXEMPT = "Relative Care Exempt" + SCHOOL_AGE_PROVIDER = "School Age Provider" + + +class mt_ccap_provider_type(Variable): + value_type = Enum + entity = Person + possible_values = MTCCAPProviderType + default_value = MTCCAPProviderType.CENTER + definition_period = MONTH + defined_for = StateCode.MT + label = "Montana Best Beginnings Child Care Scholarship provider type" + reference = "https://www.law.cornell.edu/regulations/montana/Mont-Admin-r-37.80.205" diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_child_care_subsidies.py b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_child_care_subsidies.py new file mode 100644 index 00000000000..03b5c238ead --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ccap/mt_child_care_subsidies.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class mt_child_care_subsidies(Variable): + value_type = float + entity = SPMUnit + label = "Montana child care subsidies" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + adds = ["mt_ccap"]