diff --git a/changelog.d/sd-liheap.added.md b/changelog.d/sd-liheap.added.md new file mode 100644 index 00000000000..a6e58f42e10 --- /dev/null +++ b/changelog.d/sd-liheap.added.md @@ -0,0 +1 @@ +Add South Dakota LIEAP regular heating assistance with FY2024-FY2026 income limits, county heating regions, direct and heat-in-rent payments, and mixed-status households. diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml new file mode 100644 index 00000000000..d6a840b78a6 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml @@ -0,0 +1,10 @@ +description: South Dakota excludes earned income below this age under the Low Income Energy Assistance Program. +values: + 2025-10-01: 18 +metadata: + unit: year + period: year + label: South Dakota LIEAP earned income minimum age + reference: + - title: South Dakota Administrative Rule 67:15:01:18(1), Income exclusions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml new file mode 100644 index 00000000000..5677a947522 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml @@ -0,0 +1,12 @@ +description: South Dakota uses poverty guidelines from this many years before the heating year under the Low Income Energy Assistance Program. +# The 2024-01-01 date records the FY2024 schedule in the annual model, not policy +# commencement; earlier backfilled years are unverified. +values: + 2024-01-01: 1 +metadata: + unit: year + period: year + label: South Dakota LIEAP poverty guideline year lag + reference: + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, income thresholds reconciled to 2023 guidelines #page 1-5" + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml new file mode 100644 index 00000000000..9301c647b2e --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml @@ -0,0 +1,19 @@ +description: South Dakota uses this poverty guideline rate as the lower bound of its income limit under the Low Income Energy Assistance Program. +# This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; +# the date is an annual-model observation, not policy commencement. Plan section +# 2.1 prints 150% of the poverty guideline for size 10 in FY2025 and FY2026. +values: + 2024-01-01: 1.5 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP income limit lower poverty rate + reference: + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota FY2026 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 + - title: South Dakota FY2025 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2025/state-plans/SD_Plan_2025.pdf#page=8 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml new file mode 100644 index 00000000000..7e14a787934 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml @@ -0,0 +1,19 @@ +description: South Dakota limits household income to at most this share of the poverty guideline under the Low Income Energy Assistance Program. +# This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; +# the date is an annual-model observation, not policy commencement. The FY2026 +# plan, section 2.1, prints 54.14%, 55.95% and 57.07% of state median income for +# sizes 1-3, which equal 200% of the 2025 guideline at the $111,183 median: +# $31,300 / ($111,183 x 52%), $42,300 / ($111,183 x 68%), $53,300 / ($111,183 x 84%). +values: + 2024-01-01: 2 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP income limit upper poverty rate + reference: + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota FY2026 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml new file mode 100644 index 00000000000..79a27593a4e --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml @@ -0,0 +1,20 @@ +description: South Dakota limits household income to this share of state median income, within the poverty guideline bounds, under the Low Income Energy Assistance Program. +# The rate applies to the four-person state median income before the household +# size adjustment. Flooring before the size adjustment and flooring each quarter +# reproduce the FY2024 and FY2026 tables, a reconciled numerical pattern. The date +# is an FY2024 annual-model observation, not policy commencement. +values: + 2024-01-01: 0.6 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP income limit state median income rate + reference: + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota FY2026 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 + - title: South Dakota FY2025 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2025/state-plans/SD_Plan_2025.pdf#page=8 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml new file mode 100644 index 00000000000..5def67ed8fd --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml @@ -0,0 +1,61 @@ +description: South Dakota limits three-month household income to this amount under the Low Income Energy Assistance Program. +# The FY2025 plan, section 2.1 (SD_Plan_2025.pdf#page=8), lists 60% of state +# median income for sizes 1-9 and 150% of the poverty guideline for size 10; +# the published limits for sizes 1-3 equal 200% of the 2024 guideline instead. +# The table stops at size 10; larger households use the income limit rule. +metadata: + type: single_amount + threshold_unit: person + amount_unit: currency-USD + unit: currency-USD + # Amounts are three-month totals; sd_liheap_income_limit multiplies them by + # four. No quarter period exists, so this follows the year convention of + # other quarterly dollar amounts (Pennsylvania minimum_high_quarter_wages, New + # York high_quarter_minimum). + period: year + label: South Dakota LIEAP published three-month income limit + reference: + - title: South Dakota DSS, 2024-2025 Heating Season Income Limits (archived January 14, 2025) + href: https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + +brackets: + - threshold: + 2024-10-01: 1 + amount: + 2024-10-01: 7_530 + - threshold: + 2024-10-01: 2 + amount: + 2024-10-01: 10_220 + - threshold: + 2024-10-01: 3 + amount: + 2024-10-01: 12_910 + - threshold: + 2024-10-01: 4 + amount: + 2024-10-01: 15_858 + - threshold: + 2024-10-01: 5 + amount: + 2024-10-01: 18_396 + - threshold: + 2024-10-01: 6 + amount: + 2024-10-01: 20_933 + - threshold: + 2024-10-01: 7 + amount: + 2024-10-01: 21_409 + - threshold: + 2024-10-01: 8 + amount: + 2024-10-01: 21_884 + - threshold: + 2024-10-01: 9 + amount: + 2024-10-01: 22_360 + - threshold: + 2024-10-01: 10 + amount: + 2024-10-01: 23_805 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml new file mode 100644 index 00000000000..38302ef40c4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml @@ -0,0 +1,20 @@ +description: South Dakota uses its published three-month income limit table when this is true under the Low Income Energy Assistance Program. +values: + # The poverty guideline and state median income rule reproduces every + # FY2024 matrix limit (sizes 1-15). + 2023-10-01: false + # The published 2024-2025 limits for sizes 4-7 depart from that rule. + 2024-10-01: true + # The rule reproduces every published 2025-2026 limit (sizes 1-10). + 2025-10-01: false +metadata: + unit: bool + period: year + label: South Dakota LIEAP published income limit table in effect + reference: + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, 2024-2025 Heating Season Income Limits (archived January 14, 2025) + href: https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml new file mode 100644 index 00000000000..bbf9743a6b7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml @@ -0,0 +1,10 @@ +description: South Dakota excludes earned income of secondary school students below this age under the Low Income Energy Assistance Program. +values: + 2025-10-01: 19 +metadata: + unit: year + period: year + label: South Dakota LIEAP secondary school earned income age limit + reference: + - title: South Dakota Administrative Rule 67:15:01:18(1), Income exclusions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml new file mode 100644 index 00000000000..c469e817885 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml @@ -0,0 +1,20 @@ +description: South Dakota uses these sources as earned income under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - employment_income + - self_employment_income + - sstb_self_employment_income + - farm_operations_income + - partnership_s_corp_income +# Earnings of members under 18, or under 19 in high school, are excluded. +# Use existing net business inputs and floor source losses without an additional +# business-expense deduction or reconstructing gross receipts. +metadata: + unit: list + period: year + label: South Dakota LIEAP earned income sources + reference: + - title: South Dakota Administrative Rule 67:15:01:18.01, Self-employed households + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01 + - title: "South Dakota FY2026 LIHEAP State Plan, section 1.9 #page 6-7" + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml new file mode 100644 index 00000000000..70c259e11f7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -0,0 +1,57 @@ +description: South Dakota uses these sources as unearned income for members of all ages under the Low Income Energy Assistance Program. +values: + 2025-10-01: + # Social Security is counted in the formula, net of the Medicare Part B + # premium (rule 67:15:01:18(12)), and TANF through the take-up-gated tanf. + - ssi + - pension_income + # Military retirement pay is a separate input from the pension inputs that + # pension_income adds, so both are listed; a household reports the amount + # in one or the other. + - military_retirement_pay + - railroad_benefits + - survivor_benefits + - total_unemployment_compensation + # The application (page 5) lists "Worker's Compensation" as gross income, + # rule 67:15:01:01(4) counts unearned income before deductions, and rule + # 67:15:01:18 does not exclude it. + - workers_compensation + - veterans_benefits + - child_support_received + - alimony_income + - rental_income + - farm_rent_income + - strike_benefits + - general_assistance + # The manual (page 25) counts trust income ("Trusts", Y). + - estate_income + # The manual (page 27) counts "Lottery or gaming winnings" in full: "If + # received in the 3 month LIEAP time frame count entire amount." The FY2026 + # plan, section 1.9 (page 6), leaves "One-time lump-sum payments, such as + # rebates/credits, winnings from lotteries" unchecked. The manual is followed. + - gambling_winnings + # Regular cash help from family or friends counts. Rule 67:15:01:18(17) + # excludes only "Cash gifts for recurring occasions such as birthdays and + # Christmas"; the manual (page 25) counts "Cash Gifts (Monthly)" listed for + # all three months, and the application (page 5) lists "Money from Family + # or Friends" as gross income. The FY2026 plan, section 1.9 (page 6), leaves + # "Cash gifts" unchecked; the rule and manual are followed. An annual input + # is read as regular help, not a one-time or occasion gift. + - financial_assistance +# Pension and Social Security subcomponents are not added again. Interest, +# dividends and capital gains are excluded. +metadata: + unit: list + period: year + label: South Dakota LIEAP unearned income sources + reference: + - title: "South Dakota FY2026 LIHEAP State Plan, sections 1.5 and 1.9 and fixed-income processing #page 5-7,10" + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5 + - title: South Dakota Administrative Rule 67:15:01:01(4), Gross income + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:01 + - title: South Dakota Administrative Rule 67:15:01:18, Income exclusions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 + - title: South Dakota Energy Assistance Application EA-297 (08/25), income section + href: https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=5 + - title: "South Dakota LIEAP Policy and Procedures Manual (2018), countable income table #page 25,27-28" + href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=25 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml new file mode 100644 index 00000000000..dbf2ebfb7b4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml @@ -0,0 +1,157 @@ +description: South Dakota provides this amount as the direct-heating base payment under the Low Income Energy Assistance Program. + +# The FY2022 and FY2023 matrices print the same cells, as do the FY2024 and +# FY2026 matrices. No FY2025 matrix is published; the FY2025 plan's $668 +# minimum and $3,437 maximum (section 2.6, SD_Plan_2025.pdf#page=9) match the +# FY2024 cells. Coal and wood and natural gas cells are unchanged from FY2022 +# through FY2026. +# The FY2021 plan (section 2.6, SD_Plan_2021.pdf#page=9) prints a $334 minimum +# benefit, so the cells that backfill before October 2021 are unverified. +coal_wood: + 1: + 1: + 2021-10-01: 742 + 2: + 2021-10-01: 668 + 2: + 1: + 2021-10-01: 770 + 2: + 2021-10-01: 694 + 3: + 1: + 2021-10-01: 812 + 2: + 2021-10-01: 730 + 4: + 1: + 2021-10-01: 854 + 2: + 2021-10-01: 768 +electricity: + 1: + 1: + 2021-10-01: 1_210 + 2023-10-01: 1_331 + 2: + 2021-10-01: 1_089 + 2023-10-01: 1_198 + 2: + 1: + 2021-10-01: 1_281 + 2023-10-01: 1_410 + 2: + 2021-10-01: 1_154 + 2023-10-01: 1_270 + 3: + 1: + 2021-10-01: 1_378 + 2023-10-01: 1_516 + 2: + 2021-10-01: 1_244 + 2023-10-01: 1_369 + 4: + 1: + 2021-10-01: 1_478 + 2023-10-01: 1_626 + 2: + 2021-10-01: 1_328 + 2023-10-01: 1_461 +fuel_oil: + 1: + 1: + 2021-10-01: 1_905 + 2023-10-01: 2_858 + 2: + 2021-10-01: 1_715 + 2023-10-01: 2_573 + 2: + 1: + 2021-10-01: 2_012 + 2023-10-01: 3_018 + 2: + 2021-10-01: 1_810 + 2023-10-01: 2_715 + 3: + 1: + 2021-10-01: 2_157 + 2023-10-01: 3_236 + 2: + 2021-10-01: 1_940 + 2023-10-01: 2_910 + 4: + 1: + 2021-10-01: 2_291 + 2023-10-01: 3_437 + 2: + 2021-10-01: 2_062 + 2023-10-01: 3_093 +natural_gas: + 1: + 1: + 2021-10-01: 880 + 2: + 2021-10-01: 792 + 2: + 1: + 2021-10-01: 938 + 2: + 2021-10-01: 844 + 3: + 1: + 2021-10-01: 1_007 + 2: + 2021-10-01: 907 + 4: + 1: + 2021-10-01: 1_058 + 2: + 2021-10-01: 953 +propane: + 1: + 1: + 2021-10-01: 1_666 + 2023-10-01: 2_499 + 2: + 2021-10-01: 1_499 + 2023-10-01: 2_249 + 2: + 1: + 2021-10-01: 1_772 + 2023-10-01: 2_658 + 2: + 2021-10-01: 1_596 + 2023-10-01: 2_394 + 3: + 1: + 2021-10-01: 1_889 + 2023-10-01: 2_834 + 2: + 2021-10-01: 1_701 + 2023-10-01: 2_552 + 4: + 1: + 2021-10-01: 2_044 + 2023-10-01: 3_066 + 2: + 2021-10-01: 1_841 + 2023-10-01: 2_762 + +metadata: + unit: currency-USD + period: year + label: South Dakota LIEAP direct-heating base payment + breakdown: + - ['coal_wood', 'electricity', 'fuel_oil', 'natural_gas', 'propane'] + - range(1, 5) + - range(1, 3) + propagate_metadata_to_children: true + reference: + - title: South Dakota FY2022 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_BenefitMatrix_2022.pdf#page=1 + - title: South Dakota FY2023 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/docs/2023/benefits-matricies/SD_BenefitMatrix_2023.pdf#page=1 + - title: South Dakota FY2024 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota FY2026 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml new file mode 100644 index 00000000000..000011b1b47 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml @@ -0,0 +1,19 @@ +description: South Dakota provides no more than this heat-in-rent payment by heating region under the Low Income Energy Assistance Program. +1: + 2025-10-01: 800 +2: + 2025-10-01: 800 +3: + 2025-10-01: 850 +4: + 2025-10-01: 850 +metadata: + unit: currency-USD + period: year + label: South Dakota LIEAP heat-in-rent maximum by region + breakdown: + - range(1, 5) + propagate_metadata_to_children: true + reference: + - title: South Dakota FY2026 LIHEAP Benefit Matrix, Heat in Rent Cases + href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml new file mode 100644 index 00000000000..f865d52e151 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml @@ -0,0 +1,10 @@ +description: South Dakota provides heat-in-rent payments for this many months, October through April, under the Low Income Energy Assistance Program. +values: + 2025-10-01: 7 +metadata: + unit: month + period: year + label: South Dakota LIEAP heat-in-rent covered months + reference: + - title: "South Dakota LIEAP Policy and Procedures Manual, 2018, October through April #page 31-32" + href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=31 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml new file mode 100644 index 00000000000..413c8145940 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml @@ -0,0 +1,10 @@ +description: South Dakota limits heat-in-rent payments to this fraction of monthly tenant-paid rent under the Low Income Energy Assistance Program. +values: + 2025-10-01: 0.3 +metadata: + unit: /1 + period: month + label: South Dakota LIEAP heat-in-rent payment share + reference: + - title: South Dakota Administrative Rule 67:15:01:41.02, Amount of payment if rent includes cost of heat + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:41.02 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml new file mode 100644 index 00000000000..4fa23e8c7ac --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml @@ -0,0 +1,16 @@ +description: South Dakota uses this poverty guideline rate as the first income tier limit under the Low Income Energy Assistance Program. +# Rounding its three-month equivalent half up reproduces all published FY2022 and +# FY2024 first-tier thresholds; this inferred rule carries forward to the FY2026 +# Mid tier. The date is the latest explicit FY2024 annual-model observation, not +# policy commencement. +values: + 2024-01-01: 1.25 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP first-tier poverty rate + reference: + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, first-tier maximum income #page 1-5" + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota FY2026 LIHEAP Benefit Matrix, Income Tier 1, 0-Mid + href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml new file mode 100644 index 00000000000..9ea0ac21351 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml @@ -0,0 +1,30 @@ +description: South Dakota uses these counties as heating region 1 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - BENNETT_COUNTY_SD + - BON_HOMME_COUNTY_SD + - CHARLES_MIX_COUNTY_SD + - CLAY_COUNTY_SD + - CUSTER_COUNTY_SD + - DAVISON_COUNTY_SD + - DOUGLAS_COUNTY_SD + - FALL_RIVER_COUNTY_SD + - GREGORY_COUNTY_SD + - HUTCHINSON_COUNTY_SD + - JACKSON_COUNTY_SD + - JONES_COUNTY_SD + - LINCOLN_COUNTY_SD + - MELLETTE_COUNTY_SD + - OGLALA_LAKOTA_COUNTY_SD + - TODD_COUNTY_SD + - TRIPP_COUNTY_SD + - TURNER_COUNTY_SD + - UNION_COUNTY_SD +# Oglala Lakota is the current name of Shannon County in the rule. +metadata: + unit: list + period: year + label: South Dakota LIEAP region 1 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml new file mode 100644 index 00000000000..62e783c8f81 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml @@ -0,0 +1,29 @@ +description: South Dakota uses these counties as heating region 2 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - AURORA_COUNTY_SD + - BRULE_COUNTY_SD + - BUFFALO_COUNTY_SD + - BUTTE_COUNTY_SD + - HAAKON_COUNTY_SD + - HAND_COUNTY_SD + - HANSON_COUNTY_SD + - HUGHES_COUNTY_SD + - JERAULD_COUNTY_SD + - LAWRENCE_COUNTY_SD + - LYMAN_COUNTY_SD + - MCCOOK_COUNTY_SD + - MEADE_COUNTY_SD + - MINER_COUNTY_SD + - PENNINGTON_COUNTY_SD + - SANBORN_COUNTY_SD + - STANLEY_COUNTY_SD + - SULLY_COUNTY_SD + - YANKTON_COUNTY_SD +metadata: + unit: list + period: year + label: South Dakota LIEAP region 2 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml new file mode 100644 index 00000000000..540e9a1a639 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml @@ -0,0 +1,25 @@ +description: South Dakota uses these counties as heating region 3 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - BEADLE_COUNTY_SD + - DEWEY_COUNTY_SD + - EDMUNDS_COUNTY_SD + - FAULK_COUNTY_SD + - GRANT_COUNTY_SD + - HYDE_COUNTY_SD + - KINGSBURY_COUNTY_SD + - LAKE_COUNTY_SD + - MINNEHAHA_COUNTY_SD + - MOODY_COUNTY_SD + - POTTER_COUNTY_SD + - ROBERTS_COUNTY_SD + - SPINK_COUNTY_SD + - WALWORTH_COUNTY_SD + - ZIEBACH_COUNTY_SD +metadata: + unit: list + period: year + label: South Dakota LIEAP region 3 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml new file mode 100644 index 00000000000..68908908c19 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml @@ -0,0 +1,23 @@ +description: South Dakota uses these counties as heating region 4 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - BROOKINGS_COUNTY_SD + - BROWN_COUNTY_SD + - CAMPBELL_COUNTY_SD + - CLARK_COUNTY_SD + - CODINGTON_COUNTY_SD + - CORSON_COUNTY_SD + - DAY_COUNTY_SD + - DEUEL_COUNTY_SD + - HAMLIN_COUNTY_SD + - HARDING_COUNTY_SD + - MARSHALL_COUNTY_SD + - MCPHERSON_COUNTY_SD + - PERKINS_COUNTY_SD +metadata: + unit: list + period: year + label: South Dakota LIEAP region 4 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 5250ab3aa89..036d477b143 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -808,7 +808,7 @@ programs: category: Benefits agency: HHS status: partial - coverage: DC, Riverside County, MA, IL, IN, KS, KY, MD, MS, NC, NE, NJ, NY, OR, TX + coverage: DC, Riverside County, MA, IL, IN, KS, KY, MD, MS, NC, NE, NJ, NY, OR, SD, TX state_implementations: - state: DC status: complete @@ -908,6 +908,13 @@ programs: parameter_prefix: gov.states.or.ohcs.liheap verified_years: "2025-2027" notes: PY2025-PY2027 regular heating only, with the two county regions, five fuel columns and the roomer half payment. Crisis, cooling, Fuel Payment, equipment and OEAP assistance are out of scope. + - state: SD + status: partial + name: South Dakota LIEAP + full_name: South Dakota Low Income Energy Assistance Program + variable: sd_liheap + parameter_prefix: gov.states.sd.dss.liheap + notes: FY2024-FY2026 regular heating only; mixed-status income is counted in full and FY2026 Mid thresholds are inferred, so no year is verified. Crisis, cooling and weatherization are out of scope. - state: TX status: partial name: Texas CEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml new file mode 100644 index 00000000000..3ba82157314 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml @@ -0,0 +1,227 @@ +# ARSD 67:15:01:01 and :09; FY2026 plan sections 1.4a and 17.3. +# Ordinary size-one annual income limit is 4 x published quarterly $7,825. +- name: Case 1, ordinary income includes the published limit and excludes one cent above. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [2000, 2000, 2000] + employment_income: [0, 31300, 31300.01] + ssi: [0, 0, 0] + tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + output: + sd_liheap_countable_income: [0, 31300, 31300.01] + sd_liheap_income_limit: [31300, 31300, 31300] + sd_liheap_eligible: [true, true, false] +- name: Case 2, heating responsibility permits positive tenant rent without a separate bill. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NONE] + heating_expense: [100, 0, 0, 0, 1000] + employment_income: [0, 0, 0, 0, 0] + ssi: [0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0, 0] + heat_expense_included_in_rent: [false, false, true, true, false] + rent: [0, 0, 1200, 0, 0] + output: + sd_liheap_eligible: [true, false, true, false, false] +- name: Case 3, ordinary LIEAP uses federal qualification without SNAP-specific immigrant bars. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD] + age: [40, 40, 40, 40] + immigration_status: [CITIZEN, LEGAL_PERMANENT_RESIDENT, REFUGEE, UNDOCUMENTED] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [2000, 2000, 2000, 2000] + employment_income: [0, 0, 0, 0] + ssi: [0, 0, 0, 0] + tanf: [0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0] + output: + sd_liheap_eligible: [true, true, true, false] +- name: Case 4, categorical income eligibility requires every household member to receive SNAP. + period: 2026 + absolute_error_margin: 0.001 + input: + people: + adult1: + age: 40 + immigration_status: CITIZEN + employment_income: 100000 + ssi: 0 + child1: + age: 10 + immigration_status: CITIZEN + ssi: 0 + adult2: + age: 40 + immigration_status: CITIZEN + employment_income: 100000 + ssi: 0 + child2: + age: 10 + immigration_status: CITIZEN + ssi: 0 + spm_units: + unit1: + members: &id001 [adult1, child1] + heating_type: NATURAL_GAS + heating_expense: 2000 + tanf: 0 + snap: + 2026-01: 100 + snap_unit_size: + 2026-01: 2 + unit2: + members: &id002 [adult2, child2] + heating_type: NATURAL_GAS + heating_expense: 2000 + tanf: 0 + snap: + 2026-01: 100 + snap_unit_size: + 2026-01: 1 + households: + household1: + members: *id001 + state_code: SD + household2: + members: *id002 + state_code: SD + output: + sd_liheap_countable_income: [100000, 100000] + sd_liheap_eligible: [true, false] +# Manual physical page 9: mixed households are eligible, and only citizens and +# eligible aliens count as members. Size 1 (the child): limit $31,300, Mid +# $19,564; $0 income is tier 1, so Custer County (region 1) gas pays +# min($880, $2,000) = $880. +- name: Case 5, a mixed-status household is eligible and counts only its qualified member. + period: 2026 + absolute_error_margin: 0.001 + input: + people: + adult: + age: 40 + immigration_status: UNDOCUMENTED + ssi: 0 + child: + age: 10 + immigration_status: CITIZEN + ssi: 0 + spm_units: + unit: + members: [adult, child] + heating_type: NATURAL_GAS + heating_expense: 2000 + tanf: 0 + snap: + 2026-01: 0 + households: + household: + members: [adult, child] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_household_size: 1 + sd_liheap_income_limit: 31_300 + sd_liheap_eligible: true + sd_liheap: 880 +- name: Case 6, another state does not receive South Dakota eligibility. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [ND] + age: [40] + immigration_status: [CITIZEN] + heating_type: [NATURAL_GAS] + heating_expense: [2000] + employment_income: [0] + ssi: [0] + tanf: [0] + snap: + 2026-01: [0] + output: + sd_liheap_eligible: [false] + sd_liheap: [0] + +# 2024-2025 published table (DSS page archived January 14, 2025): size 4 limit +# $15,858 a quarter x 4 = $63,432. Income at the limit is eligible; one cent +# above is not. +- name: Case 7, four-person households in 2025 include the published limit and exclude one cent above. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + employment_income: 63_432 + ssi: 0 + person2: + age: 38 + immigration_status: CITIZEN + ssi: 0 + person3: + age: 10 + immigration_status: CITIZEN + ssi: 0 + person4: + age: 7 + immigration_status: CITIZEN + ssi: 0 + person5: + age: 40 + immigration_status: CITIZEN + employment_income: 63_432.01 + ssi: 0 + person6: + age: 38 + immigration_status: CITIZEN + ssi: 0 + person7: + age: 10 + immigration_status: CITIZEN + ssi: 0 + person8: + age: 7 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit1: + members: [person1, person2, person3, person4] + heating_type: NATURAL_GAS + heating_expense: 1_000 + tanf: 0 + snap: + 2025-01: 0 + spm_unit2: + members: [person5, person6, person7, person8] + heating_type: NATURAL_GAS + heating_expense: 1_000 + tanf: 0 + snap: + 2025-01: 0 + households: + household1: + members: [person1, person2, person3, person4] + state_code: SD + household2: + members: [person5, person6, person7, person8] + state_code: SD + output: + sd_liheap_income_limit: [63_432, 63_432] + sd_liheap_countable_income: [63_432, 63_432.01] + sd_liheap_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml new file mode 100644 index 00000000000..8ced372acbf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml @@ -0,0 +1,80 @@ +# Manual physical page 9: "Only citizens and eligible aliens are counted as +# actual HH members"; page 18: ineligible aliens are not in the household count. + +# Two citizens and a lawful permanent resident (a qualified noncitizen) = 3. +- name: Case 1, citizens and qualified noncitizens all count. + period: 2026 + input: + people: + person1: + age: 35 + immigration_status: CITIZEN + person2: + age: 33 + immigration_status: LEGAL_PERMANENT_RESIDENT + person3: + age: 6 + immigration_status: CITIZEN + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + sd_liheap_household_size: 3 + +# Citizen adult + citizen child = 2; the undocumented adult is not counted. +- name: Case 2, an undocumented adult is excluded from the household count. + period: 2026 + input: + people: + person1: + age: 35 + immigration_status: CITIZEN + person2: + age: 33 + immigration_status: UNDOCUMENTED + person3: + age: 6 + immigration_status: CITIZEN + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + sd_liheap_household_size: 2 + +# Neither undocumented adult is counted, so the size is 0. +- name: Case 3, a household with no citizen or qualified noncitizen has size zero. + period: 2026 + input: + people: + person1: + age: 35 + immigration_status: UNDOCUMENTED + person2: + age: 33 + immigration_status: UNDOCUMENTED + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_liheap_household_size: 0 + +# Defined for South Dakota only. +- name: Case 4, another state has no South Dakota household size. + period: 2026 + input: + state_code: [ND] + immigration_status: [CITIZEN] + output: + sd_liheap_household_size: [0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml new file mode 100644 index 00000000000..d2225a81a01 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml @@ -0,0 +1,72 @@ +# South Dakota LIEAP annual income limits: four times the three-month maximum. + +# DSS 2025-2026 Heating Season Income Limits print three-month maxima of $7,825, +# $10,575, $13,325, $16,075, $18,825, $21,575, $22,514, $23,014, $23,514 and +# $24,431; each is multiplied by 4. Sizes 7-9 use the 60% state median income term. +- name: Case 1, all ten published FY2026 three month maxima are annualized once. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + output: + sd_liheap_income_limit: [31300, 42300, 53300, 64300, 75300, 86300, 90056, 92056, 94056, 97724] + +# FY2024 matrix pages 1-5 print three-month maxima for sizes 1-15; each is multiplied +# by 4. Size 1 is 200% of the 2023 guideline, sizes 2-8 the 60% state median income +# term and sizes 9-15 150% of the guideline. +- name: Case 2, all fifteen FY2024 published quarterly maxima retain their rounded amounts. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + output: + sd_liheap_income_limit: [29160, 39308, 48560, 57808, 67056, 76308, 78040, 79776, 83548, 91260, 98968, 106680, + 114388, 122100, 129808] + +# DSS prints no limit above size 10, so sizes 11-15 follow the rule: 150% of the +# 2025 guideline ($70,650, $76,150, $81,650, $87,150, $92,650) is $105,975, +# $114,225, $122,475, $130,725 and $138,975; each quarter is floored and x 4. +- name: Case 3, derived FY2026 large household limits continue beyond the printed ten person table. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [11, 12, 13, 14, 15] + output: + sd_liheap_income_limit: [105972, 114224, 122472, 130724, 138972] + +# A household size of 0 (no citizen or eligible alien) and another state get +# no limit; size 1 uses $7,825 x 4 = $31,300. +- name: Case 4, zero household size and a different state have no South Dakota income limit. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, ND, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [0, 1, 1] + output: + sd_liheap_income_limit: [0, 0, 31300] + +# 2024-2025 published table (DSS page archived January 14, 2025), quarterly +# $7,530, $10,220, $12,910, $15,858, $18,396, $20,933, $21,409, $21,884, +# $22,360, $23,805, each x 4. Size 11 uses the rule: 2024 FPG $68,860 x 150% = +# $103,290 (above 60% SMI share 63,433 x 1.47 = $93,246.51, below 200%), quarter +# floored to $25,822 x 4 = $103,288. Cases 1 and 2 keep 2026 and 2024 unchanged. +- name: Case 5, the published 2025 three month limits apply for sizes one to ten. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + output: + sd_liheap_income_limit: [30_120, 40_880, 51_640, 63_432, 73_584, 83_732, 85_636, 87_536, 89_440, 95_220, + 103_288] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml new file mode 100644 index 00000000000..b850917e365 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml @@ -0,0 +1,726 @@ +# South Dakota LIEAP countable income: rules 67:15:01:18 and 67:15:01:18.01, FY2026 plan section 1.9 and the manual's income chart. + +# Rule 67:15:01:18.01: existing net business inputs count with no further business +# deduction: $12,000 + $600 + $600 + $1,200 + $600 = $15,000. +- name: Case 1, adult wages and existing net business sources count once. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + self_employment_income: 600 + sstb_self_employment_income: 600 + farm_operations_income: 1_200 + partnership_s_corp_income: 600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 15_000 + +# Rule 67:15:01:18.01 treats a self-employment loss as zero: $12,000 of wages + +# $600 of partnership income = $12,600. +- name: Case 2, business losses are zero before adding other income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + self_employment_income: -6_000 + farm_operations_income: -1_200 + partnership_s_corp_income: 600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 12_600 + +# Rule 67:15:01:18(1) excludes the earnings of a member under 18. The +# 17-year-old's $6,000 of wages and $1,200 of self-employment are excluded; their +# $1,200 of Social Security counts: $12,000 + $1,200 = $13,200. +- name: Case 3, age 17 nonstudent follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 17 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + is_full_time_student: false + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# Rule 67:15:01:18(1) also excludes the earnings of a high school student under +# 19: $12,000 + $1,200 of the student's Social Security = $13,200. +- name: Case 4, age 18 secondary student follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 18 + immigration_status: CITIZEN + is_in_secondary_school: true + ssi: 0 + is_full_time_student: true + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# A college student aged 18 is not in high school, so the earnings count: +# $12,000 + $6,000 + $1,200 + $1,200 = $20,400. +- name: Case 5, age 18 college student follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 18 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + is_full_time_student: true + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 20_400 + +# A 19-year-old high school student is not under 19, so the earnings count: +# $12,000 + $6,000 + $1,200 + $1,200 = $20,400. +- name: Case 6, age 19 secondary student follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 19 + immigration_status: CITIZEN + is_in_secondary_school: true + ssi: 0 + is_full_time_student: true + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 20_400 + +# FY2026 plan pages 5-7 and 10: $1,200 + $2,400 + $600 + $3,600 + $1,200 + $600 + +# $1,200 + $600 + $1,200 + $600 + $300 + $300 = $13,800. The railroad, farm rent +# and survivor inputs are disjoint. +- name: Case 7, disjoint rental survivor and ordinary unearned sources count at every age. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rental_income: 1_200 + farm_rent_income: 2_400 + survivor_benefits: 600 + social_security: 3_600 + pension_income: 1_200 + railroad_benefits: 600 + unemployment_compensation: 1_200 + veterans_benefits: 600 + child_support_received: 1_200 + alimony_income: 600 + strike_benefits: 300 + general_assistance: 300 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_800 + +# Rule 67:15:01:18 excludes capital gains (14) and only gifts for recurring +# occasions (17). Plan section 1.9 leaves interest and dividends unchecked and +# manual page 25 excludes bank interest and dividends but counts monthly cash +# gifts; application page 5 lists money from family or friends. SNAP is not +# income: $12,000 wages + $2,400 regular help = $14,400. +- name: Case 8, interest dividends and capital gains are excluded while regular help from family counts. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + interest_income: 600 + ordinary_dividend_income: 1_200 + capital_gains: 6_000 + financial_assistance: 2_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + snap: 6_000 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 14_400 + +# Plan section 1.9 counts SSI and TANF at every age: $6,000 adult wages + $1,200 +# adult SSI + $2,400 child SSI + $3,600 TANF = $13,200; annual totals sum once. +- name: Case 9, adult and child SSI and the state TANF grant are annualized once. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 1_200 + employment_income: 6_000 + person2: + age: 8 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 2_400 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 3_600 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# Rule 67:15:01:18(20): $24,000 - $3,600 of child support paid = $20,400. +- name: Case 10, income used for legally mandated child support is deducted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 24_000 + child_support_expense: 3_600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 20_400 + +# Rule 67:15:01:18(20) with a household floor: max($1,200 - $2,400, 0) = $0. +- name: Case 11, child support deductions cannot produce negative countable income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 1_200 + child_support_expense: 2_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 0 + +# FY2026 plan pp5,10 counts railroad retirement. DSS publishes a one-person +# three-month maximum of $7,825, annualized to $31,300; no final award is asserted. +- name: Case 12, railroad retirement counts without pension or Social Security receipts. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 67 + immigration_status: CITIZEN + is_in_secondary_school: false + railroad_benefits: 40_000 + pension_income: 0 + social_security: 0 + ssi: 0 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 40_000 + sd_liheap_income_limit: 31_300 + +# Rule 67:15:01:18(12); manual page 28. 2026 Part B premium $202.90 x 12 = +# $2,434.80; $32,000 - $2,434.80 = $29,565.20. +- name: Case 13, Social Security counts net of the Medicare Part B premium. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + social_security: 32_000 + medicare_part_b_premium: 2_434.8 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 29_565.2 + +# The netted Social Security floors at zero: max($1,800 - $2,434.80, 0) = $0, +# so the unrecovered $634.80 of premium does not reduce the $24,000 pension. +- name: Case 14, a Part B premium above the Social Security benefit does not reduce other income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + social_security: 1_800 + medicare_part_b_premium: 2_434.8 + pension_income: 24_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 24_000 + +# Manual page 25 counts retirement payments; military retirement pay is a +# separate input from pension_income: $35,000 counts in full. +- name: Case 15, military retirement pay counts as retirement income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + military_retirement_pay: 35_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 35_000 + +# Rule 67:15:01:18.01; manual page 25. Each source floors at zero: $12,000 wages +# + $0 rental (-$4,000) + $0 farm rent (-$1,000) + $2,400 child support received +# = $14,400, not $12,000 - $4,000 - $1,000 + $2,400 = $9,400. +- name: Case 16, rental and farm rent losses do not offset wages or other unearned income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + rental_income: -4_000 + farm_rent_income: -1_000 + child_support_received: 2_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 14_400 + +# Manual page 27 counts lottery or gaming winnings in full (the plan, section +# 1.9, leaves winnings unchecked; the manual is followed): $12,000 + $5,000. +- name: Case 17, lottery and gaming winnings count in full. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + gambling_winnings: 5_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 17_000 + +# Plan section 1.9 counts TANF received. The take-up-gated tanf includes the +# $3,600 sd_tanf grant: $6,000 wages + $3,600 = $9,600. +- name: Case 18, TANF the household takes up counts as income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 6_000 + person2: + age: 5 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 3_600 + takes_up_tanf_if_eligible: true + households: + household: + members: [person1, person2] + state_code: SD + county: CUSTER_COUNTY_SD + output: + tanf: 3_600 + sd_liheap_countable_income: 9_600 + +# An sd_tanf entitlement the household does not take up is not received: +# tanf = $0, so countable income is the $6,000 of wages. +- name: Case 19, TANF the household does not take up is not counted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 6_000 + person2: + age: 5 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 3_600 + takes_up_tanf_if_eligible: false + households: + household: + members: [person1, person2] + state_code: SD + county: CUSTER_COUNTY_SD + output: + tanf: 0 + sd_liheap_countable_income: 6_000 + +# Manual page 25 counts trust income: $20,000 wages + $6,000 estate and trust +# income = $26,000. +- name: Case 20, estate and trust income counts as unearned income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 20_000 + estate_income: 6_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 26_000 + +# Each source floors at zero: $20,000 wages + max(-$3,000 trust loss, 0) = +# $20,000, not $17,000. +- name: Case 21, an estate or trust loss does not offset wages. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 20_000 + estate_income: -3_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 20_000 + +# Application EA-297 page 5 lists Worker's Compensation as gross income. SDCL +# 62-4-3 pays 66 2/3% of weekly earnings: 2/3 x $900 = $600 a week, within the +# DLR $554-$1,108 range for July 2025-June 2026. 12 weeks of wages at $900 = +# $10,800, plus 26 weeks of benefits at $600 = $15,600, totals $26,400. +- name: Case 22, workers' compensation counts as unearned income alongside wages. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 10_800 + workers_compensation: 15_600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 26_400 + +# Plan section 1.9 (page 6) checks "Unemployment insurance" and manual page 28 +# says "All UI payments received are countable." South Dakota pays 1/26 of the +# high-quarter wages a week (DLR benefit page, $28-$575 a week): $800 a week x +# 13 weeks = $10,400 / 26 = $400. 26 weeks of wages ($20,800) and 16 weeks of +# benefits ($6,400) = $27,200. +- name: Case 23, unemployment insurance counts alongside wages earned before a layoff. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 20_800 + unemployment_compensation: 6_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + total_unemployment_compensation: 6_400 + sd_liheap_countable_income: 27_200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml new file mode 100644 index 00000000000..1a6105ab784 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml @@ -0,0 +1,139 @@ +# South Dakota LIEAP direct-heating matrix cells by fuel row, region and income tier. + +# FY2026 matrix: 5 fuel rows x 2 income tiers x 4 regions = 40 cells. The size 1 Mid +# is $19,564, so $0 is tier 1 and $20,000 is tier 2, below the $31,300 maximum. +- name: Case 1, all forty published direct heating cells retain their literal amounts. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, + SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [WOOD, WOOD, WOOD, WOOD, WOOD, WOOD, WOOD, WOOD, ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY, + ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY, FUEL_OIL, FUEL_OIL, FUEL_OIL, FUEL_OIL, FUEL_OIL, FUEL_OIL, + FUEL_OIL, FUEL_OIL, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE] + sd_liheap_region: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, + 4, 1, 2, 3, 4, 1, 2, 3, 4] + sd_liheap_countable_income: [0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, + 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, + 20000] + output: + sd_liheap_base_payment: [742, 770, 812, 854, 668, 694, 730, 768, 1331, 1410, 1516, 1626, 1198, 1270, 1369, 1461, + 2858, 3018, 3236, 3437, 2573, 2715, 2910, 3093, 880, 938, 1007, 1058, 792, 844, 907, 953, 2499, 2658, 2834, + 3066, 2249, 2394, 2552, 2762] + +# The FY2026 matrix prints one Coal & Wood row. Application EA-297 (page 4) lists one +# Fuel Oil/Kerosene category, so kerosene uses the Fuel Oil row. +- name: Case 2, wood coal and the application oil kerosene category use matching source rows. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [WOOD, COAL, FUEL_OIL, KEROSENE, WOOD, COAL, FUEL_OIL, KEROSENE] + sd_liheap_region: [1, 1, 2, 2, 3, 3, 4, 4] + sd_liheap_countable_income: [0, 0, 0, 0, 20000, 20000, 20000, 20000] + output: + sd_liheap_base_payment: [742, 742, 3018, 3018, 730, 730, 3093, 3093] + +# FY2024 matrix: the size 1 three-month Mid of $4,556 gives $18,224 a year (not the +# annual column's $18,225); size 2's $6,163 gives $24,652 (not $24,650). Income at +# the threshold is tier 1 and one cent above is tier 2: region 1 gas $880 and $792. +- name: Case 3, the rounded quarterly threshold controls exact and cent boundaries rather than the annual column. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_region: [1, 1, 1, 1, 1] + sd_liheap_household_size: [1, 1, 2, 2, 2] + sd_liheap_countable_income: [18224, 18224.01, 24650, 24652, 24652.01] + output: + sd_liheap_income_threshold: [18224, 18224, 24652, 24652, 24652] + sd_liheap_base_payment: [880, 792, 880, 880, 792] + +# FY2026 size 1 Mid: $4,891 x 4 = $19,564. Region 3 electricity pays $1,516 in +# tier 1 and $1,369 in tier 2; income at the threshold stays in tier 1. +- name: Case 4, the current threshold includes its exact boundary and switches one cent above. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD] + heating_type: [ELECTRICITY, ELECTRICITY, ELECTRICITY] + sd_liheap_region: [3, 3, 3] + sd_liheap_countable_income: [19563.99, 19564, 19564.01] + output: + sd_liheap_base_payment: [1516, 1516, 1369] + +# No matrix price exists for an unknown region (0 or 5) or for NONE, SOLAR, +# OTHER or UNSPECIFIED fuel, and another state gets nothing. These zeros are +# coverage gaps, not denials. +- name: Case 5, unknown regions unpriced fuels and other states return component placeholders. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, ND] + heating_type: [NATURAL_GAS, NATURAL_GAS, NONE, SOLAR, OTHER, UNSPECIFIED, NATURAL_GAS] + sd_liheap_region: [0, 5, 1, 1, 1, 1, 1] + sd_liheap_countable_income: [0, 0, 0, 0, 0, 0, 0] + output: + sd_liheap_base_payment: [0, 0, 0, 0, 0, 0, 0] + +# Size 1 Mid $19,564 puts $20,000 in tier 2; size 2 Mid $26,436 puts the same +# income in tier 1. Region 1 gas pays $792 and $880; size does not multiply the +# payment. +- name: Case 6, actual household membership changes the tier while leaving the regional fuel schedule shared. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult1: + age: 40 + immigration_status: CITIZEN + ssi: 0 + adult2: + age: 40 + immigration_status: CITIZEN + ssi: 0 + child: + age: 10 + immigration_status: CITIZEN + ssi: 0 + spm_units: + unit1: + members: [adult1] + heating_type: NATURAL_GAS + sd_liheap_region: 1 + sd_liheap_countable_income: 20000 + unit2: + members: [adult2, child] + heating_type: NATURAL_GAS + sd_liheap_region: 1 + sd_liheap_countable_income: 20000 + households: + household1: + members: [adult1] + state_code: SD + household2: + members: [adult2, child] + state_code: SD + output: + sd_liheap_income_threshold: [19564, 26436] + sd_liheap_base_payment: [792, 880] + +# FY2022 and FY2023 matrices print the same cells. 2023 size 1 Mid: 125% x 2022 +# FPG $13,590 / 4 = $4,246.875, rounded half up to $4,247 x 4 = $16,988, so $0 +# is tier 1 and $20,000 is tier 2. Electricity r2 t1 $1,281, r4 t2 $1,328; fuel +# oil r3 t1 $2,157, r4 t2 $2,062; kerosene uses fuel oil r2 t2 $1,810; propane +# r2 t1 $1,772, r3 t2 $1,701; natural gas r4 t1 $1,058 and coal r3 t2 $730 are +# unchanged from FY2026. +- name: Case 7, 2023 base payments use the FY2022 and FY2023 matrix cells. + period: 2023 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [ELECTRICITY, ELECTRICITY, FUEL_OIL, FUEL_OIL, KEROSENE, PROPANE, PROPANE, NATURAL_GAS, COAL] + sd_liheap_region: [2, 4, 3, 4, 2, 2, 3, 4, 3] + sd_liheap_countable_income: [0, 20_000, 0, 20_000, 20_000, 0, 20_000, 0, 20_000] + output: + sd_liheap_income_threshold: [16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988] + sd_liheap_base_payment: [1_281, 1_328, 2_157, 2_062, 1_810, 1_772, 1_701, 1_058, 730] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml new file mode 100644 index 00000000000..f2aa000a752 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml @@ -0,0 +1,221 @@ +# South Dakota LIEAP heat-in-rent payments: up to 30% of monthly tenant rent for +# October-April, capped by region. + +# FY2026 matrix heat-in-rent amounts are $800, $800, $850 and $850. Annual rent of +# $6,000 is $500 a month x 30% x 7 = $1,050, so the regional amount binds. +- name: Case 1, all four regions use their source heat in rent maximum. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [0, 0, 0, 0] + county: [CUSTER_COUNTY_SD, AURORA_COUNTY_SD, ZIEBACH_COUNTY_SD, HARDING_COUNTY_SD] + heat_expense_included_in_rent: [true, true, true, true] + rent: [6_000, 6_000, 6_000, 6_000] + output: + sd_liheap_region: [1, 2, 3, 4] + sd_liheap_heat_in_rent_payment: [800, 800, 850, 850] + +# Rule 67:15:01:41.02 and manual pages 30-32: $1,200 / 12 x 30% x 7 = $210, below +# the region 2 amount of $800. +- name: Case 2, one hundred dollars monthly tenant rent caps seven month assistance at two hundred ten. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 1_200 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 0 + tanf: 0 + heat_expense_included_in_rent: true + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_heat_in_rent_payment: 210 + +# Two adults each pay $600 a year of rent and the child $0: $1,200 / 12 x 30% x 7 = +# $210 for the household, not a regional amount per person. +- name: Case 3, person rent shares are summed once for the household cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 600 + person2: + age: 39 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 600 + person3: + age: 8 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + gas_expense: 0 + tanf: 0 + heat_expense_included_in_rent: true + households: + household: + members: [person1, person2, person3] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_heat_in_rent_payment: 210 + +# Manual page 32 allows a positive subsidized tenant share: $12,000 of gross rent - +# $9,600 of housing assistance = $2,400, $1,200 per adult. $2,400 / 12 x 30% x 7 = +# $420, below the region 3 amount of $850. +- name: Case 4, subsidized rent uses the actual positive tenant share across multiple people. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 6_000 + person2: + age: 39 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 6_000 + person3: + age: 8 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + gas_expense: 0 + tanf: 0 + heat_expense_included_in_rent: true + housing_assistance: 9_600 + receives_housing_assistance: true + households: + household: + members: [person1, person2, person3] + state_code: SD + county: ZIEBACH_COUNTY_SD + is_in_public_housing: true + output: + rent: [1_200, 1_200, 0] + sd_liheap_heat_in_rent_payment: 420 + +# Manual page 32: a $0 tenant payment has no heat cost. $12,000 of gross rent - +# $12,000 of housing assistance = $0. +- name: Case 5, zero tenant share after housing assistance produces zero component. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 12_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 0 + tanf: 0 + heat_expense_included_in_rent: true + housing_assistance: 12_000 + receives_housing_assistance: true + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + rent: 0 + sd_liheap_heat_in_rent_payment: 0 + +# This component applies only to heat included in rent; a direct heating bill goes +# through the vendor payment. +- name: Case 6, a direct heating bill without heat in rent does not receive this component. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 6_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + heat_expense_included_in_rent: false + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_heat_in_rent_payment: 0 + +# The heat-in-rent amount does not depend on fuel: UNSPECIFIED with heat in rent +# gets $1,200 / 12 x 30% x 7 = $210. NONE means no heat and gets $0. +- name: Case 7, fuel neutral heat in rent permits unknown fuel but rejects explicit no heating. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD] + heating_type: [UNSPECIFIED, NONE] + gas_expense: [0, 0] + county: [AURORA_COUNTY_SD, AURORA_COUNTY_SD] + heat_expense_included_in_rent: [true, true] + rent: [1_200, 1_200] + output: + sd_liheap_heat_in_rent_payment: [210, 0] + +# An explicit UNKNOWN county has no region and gets $0, as does a household outside +# South Dakota. +- name: Case 8, unknown region returns a safe unsupported estimate without inventing a county. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, CA] + heating_type: [NATURAL_GAS, NATURAL_GAS] + gas_expense: [0, 0] + county: [UNKNOWN, LOS_ANGELES_COUNTY_CA] + heat_expense_included_in_rent: [true, true] + rent: [6_000, 6_000] + output: + sd_liheap_region: [0, 0] + sd_liheap_heat_in_rent_payment: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml new file mode 100644 index 00000000000..56e6f59babc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml @@ -0,0 +1,43 @@ +# South Dakota LIEAP first-tier (Mid) thresholds: four times the three-month Mid. + +# FY2024 matrix pages 1-5 print three-month Mid values of 4,556, 6,163, 7,769, 9,375, +# 10,981, 12,588, 14,194, 15,800, 17,406, 19,013, 20,619, 22,225, 23,831, 25,438 and +# 27,044. The annual threshold is 4 x each, not the printed annual column: size 2 is +# 4 x $6,163 = $24,652, not $24,650. +- name: Case 1, all fifteen FY2024 tier thresholds use the printed three month amounts. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + output: + sd_liheap_income_threshold: [18224, 24652, 31076, 37500, 43924, 50352, 56776, 63200, 69624, 76052, 82476, 88900, + 95324, 101752, 108176] + +# The FY2026 matrix names Mid without printing it, so the FY2024 rule carries +# forward: 125% of the 2025 guideline ($15,650, $21,150, $59,650, $65,150, $92,650) +# over 4 is $4,890.625, $6,609.375, $18,640.625, $20,359.375 and $28,953.125; +# rounding half up and x 4 gives $19,564, $26,436, $74,564, $81,436 and $115,812. +- name: Case 2, FY2026 threshold carry forward preserves half up quarter rounding. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [1, 2, 9, 10, 15] + output: + sd_liheap_income_threshold: [19564, 26436, 74564, 81436, 115812] + +# A household size of 0 and another state get no threshold; size 1 Mid is +# $4,891 x 4 = $19,564. +- name: Case 3, zero size and another state do not receive a South Dakota tier threshold. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, ND, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [0, 1, 1] + output: + sd_liheap_income_threshold: [0, 0, 19564] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml new file mode 100644 index 00000000000..e02b5fa75d1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml @@ -0,0 +1,51 @@ +# South Dakota LIEAP heating regions under rule 67:15:01:35. + +# Rule 67:15:01:35 assigns all 66 counties to four regions (manual page 52 maps the +# same regions); former Shannon County is Oglala Lakota County, region 1. +- name: Case 1, all sixty six official county assignments match including renamed Shannon County. + period: 2026 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, + SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, + SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200] + county: [CUSTER_COUNTY_SD, BUTTE_COUNTY_SD, ZIEBACH_COUNTY_SD, HARDING_COUNTY_SD, FALL_RIVER_COUNTY_SD, + MEADE_COUNTY_SD, DEWEY_COUNTY_SD, PERKINS_COUNTY_SD, OGLALA_LAKOTA_COUNTY_SD, LAWRENCE_COUNTY_SD, + WALWORTH_COUNTY_SD, CORSON_COUNTY_SD, JACKSON_COUNTY_SD, PENNINGTON_COUNTY_SD, EDMUNDS_COUNTY_SD, + CAMPBELL_COUNTY_SD, BENNETT_COUNTY_SD, HAAKON_COUNTY_SD, POTTER_COUNTY_SD, MCPHERSON_COUNTY_SD, + JONES_COUNTY_SD, STANLEY_COUNTY_SD, FAULK_COUNTY_SD, BROWN_COUNTY_SD, MELLETTE_COUNTY_SD, SULLY_COUNTY_SD, + SPINK_COUNTY_SD, MARSHALL_COUNTY_SD, TODD_COUNTY_SD, HUGHES_COUNTY_SD, ROBERTS_COUNTY_SD, DAY_COUNTY_SD, + TRIPP_COUNTY_SD, LYMAN_COUNTY_SD, GRANT_COUNTY_SD, CLARK_COUNTY_SD, GREGORY_COUNTY_SD, HAND_COUNTY_SD, + BEADLE_COUNTY_SD, CODINGTON_COUNTY_SD, CHARLES_MIX_COUNTY_SD, BUFFALO_COUNTY_SD, KINGSBURY_COUNTY_SD, + HAMLIN_COUNTY_SD, DOUGLAS_COUNTY_SD, JERAULD_COUNTY_SD, HYDE_COUNTY_SD, BROOKINGS_COUNTY_SD, DAVISON_COUNTY_SD, + SANBORN_COUNTY_SD, LAKE_COUNTY_SD, DEUEL_COUNTY_SD, HUTCHINSON_COUNTY_SD, BRULE_COUNTY_SD, MOODY_COUNTY_SD, + TURNER_COUNTY_SD, AURORA_COUNTY_SD, MINNEHAHA_COUNTY_SD, LINCOLN_COUNTY_SD, HANSON_COUNTY_SD, BON_HOMME_COUNTY_SD, + MCCOOK_COUNTY_SD, CLAY_COUNTY_SD, YANKTON_COUNTY_SD, UNION_COUNTY_SD, MINER_COUNTY_SD] + output: + sd_liheap_region: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, + 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, + 2, 1, 2, 1, 2, 1, 2] + +# An explicit UNKNOWN or out-of-state county returns region 0. +- name: Case 2, explicit unknown and foreign counties never default to a verified region. + period: 2026 + input: + state_code: [SD, SD, CA] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_200, 1_200, 1_200] + county: [UNKNOWN, LOS_ANGELES_COUNTY_CA, LOS_ANGELES_COUNTY_CA] + output: + sd_liheap_region: [0, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml new file mode 100644 index 00000000000..c3b5668c275 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml @@ -0,0 +1,331 @@ +# FY2026 matrix, page 1; application EA-297, page 2; manual pages 30-32. +# heating_expense is read as unpaid charges in the claim window. Wood, coal and +# kerosene are paid in full (manual page 31); heat in rent assumes seven months. +- name: Case 1, vendor fuels pay the smaller of the schedule and qualifying unpaid seasonal charges. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, ELECTRICITY, ELECTRICITY, PROPANE, PROPANE, FUEL_OIL, FUEL_OIL] + heating_expense: [500, 2000, 500, 2000, 500, 3000, 500, 4000] + employment_income: [0, 0, 0, 0, 0, 0, 0, 0] + ssi: [0, 0, 0, 0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0, 0, 0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, + CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_eligible: [true, true, true, true, true, true, true, true] + sd_liheap: [500, 880, 500, 1331, 500, 2499, 500, 2858] +- name: Case 2, wood coal and kerosene pay the full schedule directly to the household. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [WOOD, COAL, KEROSENE] + heating_expense: [1, 1, 1] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap: [742, 742, 2858] +- name: Case 3, income changes the payment tier and final eligibility gates the award. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD] + age: [40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [2000, 2000, 2000, 0] + employment_income: [20000, 31300, 31300.01, 0] + ssi: [0, 0, 0, 0] + tanf: [0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap: [792, 792, 0, 0] +- name: Case 4, heat-in-rent applies the regional ceiling and seven months of thirty percent tenant rent. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [UNSPECIFIED, NATURAL_GAS, ELECTRICITY, NATURAL_GAS, NONE] + heating_expense: [0, 0, 0, 0, 0] + employment_income: [0, 0, 0, 0, 0] + ssi: [0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0, 0] + heat_expense_included_in_rent: [true, true, true, true, true] + rent: [1200, 6000, 6000, 0, 1200] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, HARDING_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap: [210, 800, 850, 0, 0] +- name: Case 5, unpriced direct fuels retain unsupported zero payments despite modeled eligibility. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [SOLAR, OTHER, UNSPECIFIED] + heating_expense: [2000, 2000, 2000] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + has_heating_expense: [true, true, true] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_eligible: [true, true, true] + sd_liheap: [0, 0, 0] +- name: Case 6, vendor balances floor at zero and unknown regions remain unsupported. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [-100, 0, 100] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + has_heating_expense: [true, true, true] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, UNKNOWN] + output: + sd_liheap_eligible: [true, true, true] + sd_liheap: [0, 0, 0] + +# 2024-2025 published table (DSS page archived January 14, 2025): size 4 limit +# $15,858 a quarter x 4 = $63,432; $63,000 of wages is under it. The rule alone +# would cap size 4 at 200% x 2024 FPG $31,200 = $62,400. Mid: 125% x $31,200 = +# $39,000, so tier 2. Lincoln County is region 1: natural gas tier 2 = $792; +# min($792, $1,000 unpaid charges) = $792. +- name: Case 7, a four-person household under the published 2025 limit receives the tier two gas award. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + employment_income: 63_000 + ssi: 0 + person2: + age: 38 + immigration_status: CITIZEN + ssi: 0 + person3: + age: 10 + immigration_status: CITIZEN + ssi: 0 + person4: + age: 7 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + heating_type: NATURAL_GAS + heating_expense: 1_000 + tanf: 0 + snap: + 2025-01: 0 + households: + household: + members: [person1, person2, person3, person4] + state_code: SD + county: LINCOLN_COUNTY_SD + output: + sd_liheap_countable_income: 63_000 + sd_liheap_income_limit: 63_432 + sd_liheap_income_threshold: 39_000 + sd_liheap_region: 1 + sd_liheap_eligible: true + sd_liheap_base_payment: 792 + sd_liheap: 792 + +# FY2022 and FY2023 matrices, region 1. 2023 size 1: limit 200% x 2022 FPG +# $13,590 = $27,180; Mid 125% x $13,590 / 4 = $4,246.875, rounded half up to +# $4,247 x 4 = $16,988. Tier 1: propane $1,666, fuel oil and kerosene $1,905, +# electricity $1,210; $20,000 of wages is tier 2: propane $1,499. Vendor fuels +# pay min(cell, $3,000); kerosene pays its cell in full. +- name: Case 8, 2023 direct payments use the FY2022 and FY2023 matrix amounts. + period: 2023 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [PROPANE, FUEL_OIL, KEROSENE, ELECTRICITY, PROPANE] + heating_expense: [3_000, 3_000, 3_000, 3_000, 3_000] + employment_income: [0, 0, 0, 0, 20_000] + ssi: [0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0] + snap: + 2023-01: [0, 0, 0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_income_limit: [27_180, 27_180, 27_180, 27_180, 27_180] + sd_liheap_income_threshold: [16_988, 16_988, 16_988, 16_988, 16_988] + sd_liheap_eligible: [true, true, true, true, true] + sd_liheap: [1_666, 1_905, 1_905, 1_210, 1_499] + +# The FY2024 matrix prints the FY2026 cells, so 2024 region 1 tier 1 pays +# propane $2,499, fuel oil $2,858 and electricity $1,331, each below the $3,000 +# unpaid charges. 2024 size 1 limit $29,160. +- name: Case 9, 2024 direct payments already use the current matrix amounts. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [PROPANE, FUEL_OIL, ELECTRICITY] + heating_expense: [3_000, 3_000, 3_000] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + tanf: [0, 0, 0] + snap: + 2024-01: [0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_income_limit: [29_160, 29_160, 29_160] + sd_liheap_eligible: [true, true, true] + sd_liheap: [2_499, 2_858, 1_331] + +# Rule 67:15:01:18(12); manual page 28. 2026 Part B premium $202.90 x 12 = +# $2,434.80. $32,000 - $2,434.80 = $29,565.20 <= $31,300 limit and > $19,564 +# Mid: tier 2 gas $792. $21,000 - $2,434.80 = $18,565.20 <= Mid: tier 1 $880. +# Both are below the $2,000 unpaid charges. +- name: Case 10, Social Security net of the Medicare premium sets eligibility and the tier. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD] + age: [70, 70] + immigration_status: [CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS] + heating_expense: [2_000, 2_000] + social_security: [32_000, 21_000] + medicare_part_b_premium: [2_434.8, 2_434.8] + ssi: [0, 0] + tanf: [0, 0] + snap: + 2026-01: [0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_countable_income: [29_565.2, 18_565.2] + sd_liheap_eligible: [true, true] + sd_liheap: [792, 880] + +# Manual page 25 counts retirement payments. $35,000 of military retirement +# pay exceeds the $31,300 size 1 limit, so no award. +- name: Case 11, military retirement pay above the limit makes the household ineligible. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD] + age: [50] + immigration_status: [CITIZEN] + heating_type: [NATURAL_GAS] + heating_expense: [2_000] + military_retirement_pay: [35_000] + ssi: [0] + tanf: [0] + snap: + 2026-01: [0] + county_str: [CUSTER_COUNTY_SD] + output: + sd_liheap_countable_income: [35_000] + sd_liheap_eligible: [false] + sd_liheap: [0] + +# Rule 67:15:01:18.01; manual page 25. The rental loss counts as zero: +# ($26,400 - $2,434.80 Part B premium) + $9,000 pension + $0 = $32,965.20, +# above the $31,300 limit. Letting the loss offset would give $28,965.20 and a +# $792 award. +- name: Case 12, a rental loss does not offset a retiree's other income. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD] + age: [68] + immigration_status: [CITIZEN] + heating_type: [NATURAL_GAS] + heating_expense: [1_200] + social_security: [26_400] + medicare_part_b_premium: [2_434.8] + pension_income: [9_000] + rental_income: [-4_000] + ssi: [0] + tanf: [0] + snap: + 2026-01: [0] + county_str: [CUSTER_COUNTY_SD] + output: + sd_liheap_countable_income: [32_965.2] + sd_liheap_eligible: [false] + sd_liheap: [0] + +# Manual page 9: mixed households are eligible; only the two citizens count, so +# size 2. Page 17 prorates ineligible members' income through an unpublished +# calculator, so the undocumented adult's wages count in full: $18,000 + +# $9,000 = $27,000 <= size 2 limit $42,300 (200% x 2025 FPG $21,150). Mid: 125% +# x $21,150 / 4 = $6,609.375, rounded half up to $6,609 x 4 = $26,436, so tier +# 2: gas $792 = min($792, $1,200). Size 3 would give Mid $33,312, tier 1, $880. +- name: Case 13, a mixed-status household is sized by its qualified members and receives an award. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + immigration_status: CITIZEN + employment_income: 18_000 + ssi: 0 + person2: + age: 33 + immigration_status: UNDOCUMENTED + employment_income: 9_000 + ssi: 0 + person3: + age: 6 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + heating_expense: 1_200 + tanf: 0 + snap: + 2026-01: 0 + households: + household: + members: [person1, person2, person3] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_household_size: 2 + sd_liheap_countable_income: 27_000 + sd_liheap_income_limit: 42_300 + sd_liheap_income_threshold: 26_436 + sd_liheap_eligible: true + sd_liheap: 792 diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py new file mode 100644 index 00000000000..2929c1ed5cc --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py @@ -0,0 +1,50 @@ +from policyengine_us.model_api import * + + +class sd_liheap_eligible(Variable): + value_type = bool + entity = SPMUnit + definition_period = YEAR + label = "Eligible for South Dakota LIEAP within the modeled household scope" + defined_for = StateCode.SD + reference = ( + "https://sdlegislature.gov/Rules/Administrative/67:15:01:01", + "https://sdlegislature.gov/Rules/Administrative/67:15:01:09", + # Sections 1.4a and 17.3: SNAP recipients and qualified noncitizens. + # PDF pages 5, 34 + "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5", + # PDF pages 9, 17 + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9", + ) + + def formula(spm_unit, period, parameters): + # Manual physical page 9: "Mixed households are eligible for assistance." + # At least one citizen or eligible alien must be in the household. + has_qualified_member = spm_unit("sd_liheap_household_size", period) > 0 + income = spm_unit("sd_liheap_countable_income", period) + limit = spm_unit("sd_liheap_income_limit", period) + # All application members must receive SNAP for categorical income + # eligibility. January receipt and SNAP unit size approximate a stable + # application-month caseload; a benefit for only some members is not + # enough. A member ineligible for SNAP, such as an ineligible alien, + # sends the household to the income worksheet (physical page 17), so + # SNAP unit size is compared with every member. + receives_snap = spm_unit("snap", period.first_month) > 0 + all_receive_snap = ( + spm_unit("snap_unit_size", period.first_month) + == spm_unit("spm_unit_size", period) + ) & receives_snap + income_eligible = (income <= limit) | all_receive_snap + heating_type = spm_unit("heating_type", period) + has_heat = heating_type != heating_type.possible_values.NONE + heat_in_rent = spm_unit("heat_expense_included_in_rent", period) + responsible_for_heat = where( + heat_in_rent, + add(spm_unit, period, ["rent"]) > 0, + spm_unit("has_heating_expense", period), + ) + # Assume an independent, noninstitutional household served by the state, + # with required heating/landlord verification and no duplicate tribal + # award. Existing inputs cannot establish tribal service-area routing, + # institutional residence, or completed administrative verification. + return has_qualified_member & income_eligible & has_heat & responsible_for_heat diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py new file mode 100644 index 00000000000..8e50bf29665 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py @@ -0,0 +1,14 @@ +from policyengine_us.model_api import * + + +class sd_liheap_household_size(Variable): + value_type = int + entity = SPMUnit + definition_period = YEAR + label = "South Dakota LIEAP household size" + defined_for = StateCode.SD + # "Only citizens and eligible aliens are counted as actual HH members"; + # ineligible aliens "are not included in the household count". + # PDF pages 9, 18 + reference = "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9" + adds = ["is_citizen_or_legal_immigrant"] diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py new file mode 100644 index 00000000000..12d4c669ccf --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py @@ -0,0 +1,58 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class sd_liheap_income_limit(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP annualized income limit" + defined_for = StateCode.SD + reference = ( + # PDF pages 1-5 + "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", + "https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", + "https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.income + # Only citizens and eligible aliens count toward household size. + size = spm_unit("sd_liheap_household_size", period) + state_group = spm_unit.household("state_group_str", period) + guideline = fpg( + max_(size, 1), state_group, period, parameters, year_lag=p.fpg_year_lag + ) + state = spm_unit.household("state_code_str", period) + smi_limit = liheap_smi_limit(size, state, p.limits.smi_rate, period, parameters) + annual_limit = min_( + guideline * p.limits.fpg_upper_rate, + max_(guideline * p.limits.fpg_lower_rate, smi_limit), + ) + # This inferred ordering matches every FY2024 and published FY2026 + # three-month maximum. Annualize the rounded quarter, not its unrounded + # annual equivalent. Sizes 11+ carry the FY2024 rule forward; earlier + # backfilled years are unverified and FY2022 used different rounding. + # NOTE: FY2026 sources conflict for sizes 4-6. The FY2026 plan, section + # 2.1, sets 60% of state median income for sizes 4-9: + # https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 + # Applied to the $111,183 FY2026 state median income, that rate gives + # three-month limits of $16,677, $19,345 and $22,014 (our derivation). + # The DSS 2025-2026 published limits cited above are $16,075, $18,825 + # and $21,575 (200% of the 2025 poverty guideline). The model follows DSS. + # The FY2027 draft plan (status "Saved"), section 2.1, lists 60% of state + # median income for sizes 4-10; its size 1-3 rates (52.02%, 53.94% and + # 55.13%) match 200% of the 2026 guideline to within 0.01 point. Revisit + # sizes 4-6 when DSS posts its 2026-2027 limits: + # https://dss.sd.gov/docs/hearings/State_Plan.pdf#page=9 + limit = 4 * np.floor(annual_limit / 4) + if p.limits.table.in_effect: + # The published 2024-2025 limits exceed the rule for sizes 4-7 and + # follow no consistent rounding, so the printed table applies to + # the sizes it lists. + table = p.limits.table.amount + in_table = size <= table.thresholds[-1] + limit = where(in_table, 4 * table.calc(size), limit) + return where(size > 0, limit, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py new file mode 100644 index 00000000000..9d931a93543 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py @@ -0,0 +1,64 @@ +from policyengine_us.model_api import * + + +class sd_liheap_countable_income(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP annual countable household income" + defined_for = StateCode.SD + reference = ( + "https://sdlegislature.gov/Rules/Administrative/67:15:01:18", + "https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01", + # PDF pages 6-7 + "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6", + # PDF pages 17, 25-28 + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=17", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.income + person = spm_unit.members + age = person("age", period) + excluded_earnings = (age < p.earned_income_min_age) | ( + (age < p.secondary_school_age_limit) + & person("is_in_secondary_school", period) + ) + # Rule 67:15:01:18.01 counts self-employment losses as zero, and the + # manual (page 25) computes rental income on the self-employment sheet. + # Each source is floored at zero, so a loss in one source cannot offset + # another. Use existing net sources without another business deduction. + earned = 0 + for source in p.sources.earned: + earned = earned + max_(person(source, period), 0) + unearned = 0 + for source in p.sources.unearned: + # ADD sums monthly sources such as ssi over the year. + unearned = unearned + max_(person(source, period, options=[ADD]), 0) + # Rule 67:15:01:18(12) excludes "Deductions from social security benefit + # payments for Medicare", and the manual (page 28) counts Social Security + # less the Part B and Part D premiums. medicare_part_b_premium is zero for + # a person who is not enrolled and excludes the share a Medicare Savings + # Program pays; no Part D premium input exists. + premium = person("medicare_part_b_premium", period) + social_security = max_(person("social_security", period) - premium, 0) + # Rule 67:15:01:18(1) exempts earnings, not minor unearned income. + income = spm_unit.sum(earned * ~excluded_earnings + unearned + social_security) + # The annual tanf aggregate includes sd_tanf and applies take-up; sd_tanf + # alone is the entitlement a nonrecipient could get. + tanf = spm_unit("tanf", period) + # Subsection (20) excludes income used to pay child support. + child_support_paid = add(spm_unit, period, ["child_support_expense"]) + # Manual physical page 17: for ineligible members, "a portion of their + # income and resources are considered when determining the household + # benefit" through the Citizenship Income Calculator (also page 26). The + # calculator's proration formula is not published and is unclear: its one + # example turns $3,000 eligible and $4,000 ineligible income into $6,200, + # matching neither full counting ($7,000) nor SNAP pro rata ($6,000). The + # model counts ineligible members' income in full, which overstates + # income for mixed-status households. + # Annual inputs approximate stable income over the prior three months. + # Existing inputs cannot isolate all jury/foster receipts, program + # overpayment withholding, or expense refunds. + return max_(income + tanf - child_support_paid, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py new file mode 100644 index 00000000000..0b2d7ce437a --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py @@ -0,0 +1,48 @@ +from policyengine_us.model_api import * + + +class sd_liheap_base_payment(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP direct-heating base payment component" + defined_for = StateCode.SD + reference = ( + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1", + # PDF pages 2, 4 + "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.payment.direct + region = spm_unit("sd_liheap_region", period) + supported_region = (region >= 1) & (region <= 4) + safe_region = clip(region, 1, 4) + income = spm_unit("sd_liheap_countable_income", period) + threshold = spm_unit("sd_liheap_income_threshold", period) + tier = where(income <= threshold, 1, 2) + heating_type = spm_unit("heating_type", period) + fuels = heating_type.possible_values + amount = select( + [ + (heating_type == fuels.COAL) | (heating_type == fuels.WOOD), + heating_type == fuels.ELECTRICITY, + (heating_type == fuels.FUEL_OIL) | (heating_type == fuels.KEROSENE), + heating_type == fuels.NATURAL_GAS, + heating_type == fuels.PROPANE, + ], + [ + p.coal_wood[safe_region][tier], + p.electricity[safe_region][tier], + p.fuel_oil[safe_region][tier], + p.natural_gas[safe_region][tier], + p.propane[safe_region][tier], + ], + default=0, + ) + # sd_liheap applies eligibility and caps vendor fuels at heating_expense, + # read as unpaid seasonal vendor charges. An unknown region or a fuel + # without a matrix row returns zero as a coverage gap, not a denial; the + # default for UNSPECIFIED fuel is tracked in #9754. + return where(supported_region, amount, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py new file mode 100644 index 00000000000..e6719f957ad --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * + + +class sd_liheap_heat_in_rent_payment(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP heat-in-rent payment component" + defined_for = StateCode.SD + reference = ( + "https://sdlegislature.gov/Rules/Administrative/67:15:01:41.02", + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", + # PDF pages 30-32 + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=30", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.payment.heat_in_rent + region = spm_unit("sd_liheap_region", period) + supported_region = (region >= 1) & (region <= 4) + maximum = p.amount[clip(region, 1, 4)] + # Existing Person rent is the net tenant share, apportioned after modeled + # HUD assistance. Report observed rent directly, or override the existing + # housing_assistance amount, when that default is not actual receipt. + annual_rent = max_(add(spm_unit, period, ["rent"]), 0) + cap = annual_rent / MONTHS_IN_YEAR * p.months * p.rent_share + heat_in_rent = spm_unit("heat_expense_included_in_rent", period) + heating_type = spm_unit("heating_type", period) + has_heat = heating_type != heating_type.possible_values.NONE + # The fuel-neutral heat-in-rent schedule permits unknown primary fuel; + # no separate heating bill is required. Subsidy does not disqualify a + # positive tenant share, while zero share yields zero through the cap. + # sd_liheap applies supported eligibility. This annual estimate assumes + # seven eligible months of steady rent and completed landlord verification; + # prior payments, changes in rent, and tribal routing are not observed. + return where( + supported_region & heat_in_rent & has_heat, + min_(maximum, cap), + 0, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py new file mode 100644 index 00000000000..16c9daf6d23 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class sd_liheap_income_threshold(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP annualized first-tier income threshold" + defined_for = StateCode.SD + reference = ( + # PDF pages 1-5 + "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap + # Only citizens and eligible aliens count toward household size. + size = spm_unit("sd_liheap_household_size", period) + state_group = spm_unit.household("state_group_str", period) + guideline = fpg( + max_(size, 1), + state_group, + period, + parameters, + year_lag=p.income.fpg_year_lag, + ) + # Half-up rounding reproduces all FY2022 and FY2024 quarterly Mid + # thresholds. Carry this latest explicit rule into the FY2026 schedule, + # which names Mid without printing it. This is a numerical inference, + # not an express rounding instruction; earlier backfill is unverified. + quarter_limit = np.floor(guideline * p.payment.income_threshold_rate / 4 + 0.5) + return where(size > 0, 4 * quarter_limit, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py new file mode 100644 index 00000000000..902c49d7c5e --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class sd_liheap_region(Variable): + value_type = int + entity = SPMUnit + definition_period = YEAR + label = "South Dakota LIEAP heating region" + defined_for = StateCode.SD + reference = "https://sdlegislature.gov/Rules/Administrative/67:15:01:35" + documentation = ( + "Regions 1 to 4 follow ARSD 67:15:01:35. Zero means the county is unknown " + "or outside South Dakota. A household with no county input takes the first " + "South Dakota county, Aurora, in region 2." + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.payment.regions + county = spm_unit.household("county_str", period) + # Former Shannon County is the Oglala Lakota county identifier. + return select( + [ + np.isin(county, p.region_1), + np.isin(county, p.region_2), + np.isin(county, p.region_3), + np.isin(county, p.region_4), + ], + [1, 2, 3, 4], + default=0, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py new file mode 100644 index 00000000000..83057f65578 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * + + +class sd_liheap(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP regular heating assistance" + defined_for = "sd_liheap_eligible" + reference = ( + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", + # PDF pages 2, 4 + "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", + # PDF pages 30-32 + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=30", + ) + + def formula(spm_unit, period, parameters): + base_payment = spm_unit("sd_liheap_base_payment", period) + heating_type = spm_unit("heating_type", period) + fuels = heating_type.possible_values + # Manual page 31 pays wood, coal and kerosene households "the entire + # benefit amount" by check. Rule 67:15:01:40 names only wood and coal, and + # application EA-297 (page 4) lists "Fuel Oil/Kerosene" with a supplier + # fill ticket and says "The payment for Heat in Rent, Wood, or Coal will be + # either direct deposited or put on an EPC card for you." Neither bars a + # check for kerosene, so the manual is followed. + cash_payment = ( + (heating_type == fuels.WOOD) + | (heating_type == fuels.COAL) + | (heating_type == fuels.KEROSENE) + ) + # heating_expense is read as unpaid eligible seasonal charges, not bills + # already paid or an unrestricted annual total. Gas and electric meter + # reads: October 1-May 15; propane and oil fills: July 1-April 30. The + # inputs do not show charge dates or balances. + expense = max_(spm_unit("heating_expense", period), 0) + direct_payment = where(cash_payment, base_payment, min_(base_payment, expense)) + # Heat-in-rent uses a steady tenant share for all seven eligible months; + # changes in rent, already-paid months and prior awards are not tracked. + # Unpriced fuels remain outside coverage. + return where( + spm_unit("heat_expense_included_in_rent", period), + spm_unit("sd_liheap_heat_in_rent_payment", period), + direct_payment, + )