diff --git a/changelog.d/ut-ui.added.md b/changelog.d/ut-ui.added.md new file mode 100644 index 00000000000..81645e72ddd --- /dev/null +++ b/changelog.d/ut-ui.added.md @@ -0,0 +1 @@ +Added Utah Unemployment Insurance (UI), including weekly benefit amount, duration, and maximum benefit calculation using the High Quarter Wage formula. diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/max_weeks.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/max_weeks.yaml new file mode 100644 index 00000000000..36c38194051 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/max_weeks.yaml @@ -0,0 +1,12 @@ +description: Utah limits the potential benefit duration to this threshold under the Unemployment Insurance program. + +values: + 2024-01-01: 26 + +metadata: + unit: week + period: year + label: Utah UI maximum potential duration + reference: + - title: Utah Code § 35A-4-401(4)(b) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/min_weeks.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/min_weeks.yaml new file mode 100644 index 00000000000..6df01045be6 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/min_weeks.yaml @@ -0,0 +1,12 @@ +description: Utah sets the minimum potential benefit duration to this threshold under the Unemployment Insurance program. + +values: + 2024-01-01: 10 + +metadata: + unit: week + period: year + label: Utah UI minimum potential duration + reference: + - title: Utah Code § 35A-4-401(4)(b) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/multiplier.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/multiplier.yaml new file mode 100644 index 00000000000..aebc8bb45eb --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/duration/multiplier.yaml @@ -0,0 +1,12 @@ +description: Utah uses this multiplier on total base-period wages when computing the potential benefit duration under the Unemployment Insurance program. + +values: + 2024-01-01: 0.27 + +metadata: + unit: /1 + period: year + label: Utah UI duration wage multiplier + reference: + - title: Utah Code § 35A-4-401(4)(b) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/partial/earnings_disregard_rate.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/partial/earnings_disregard_rate.yaml new file mode 100644 index 00000000000..b0445256b2f --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/partial/earnings_disregard_rate.yaml @@ -0,0 +1,14 @@ +description: Utah excludes this share of the weekly benefit amount from countable earnings during partial unemployment under the Unemployment Insurance program. + +values: + 2024-01-01: 0.3 + +metadata: + unit: /1 + period: year + label: Utah UI partial benefit earnings disregard rate + reference: + - title: Utah Code § 35A-4-401(3)(a) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html + - title: Utah Admin Code R994-401-301 + href: https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-301 diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/partial/hours_threshold.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/partial/hours_threshold.yaml new file mode 100644 index 00000000000..7aa482738f8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/partial/hours_threshold.yaml @@ -0,0 +1,14 @@ +description: Utah limits weekly hours worked to less than this threshold for benefit eligibility under the Unemployment Insurance program. + +values: + 2024-01-01: 40 + +metadata: + unit: hour + period: week + label: Utah UI weekly hours threshold + reference: + - title: Utah DWS Claimant Guide (Jan 2025) + href: https://jobs.utah.gov/ui/jobseeker/claimantguide.pdf#page=13 + - title: Utah Code § 35A-4-207 + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S207.html diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/divisor.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/divisor.yaml new file mode 100644 index 00000000000..e5b8d64bb94 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/divisor.yaml @@ -0,0 +1,12 @@ +description: Utah uses this divisor on high-quarter wages when computing the weekly benefit amount under the Unemployment Insurance program. + +values: + 2024-01-01: 26 + +metadata: + unit: /1 + period: year + label: Utah UI weekly benefit amount divisor + reference: + - title: Utah Code § 35A-4-401(2)(a)(ii) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/max_amount.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/max_amount.yaml new file mode 100644 index 00000000000..4cc9f1c38df --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/max_amount.yaml @@ -0,0 +1,21 @@ +description: Utah limits the weekly benefit amount to this amount under the Unemployment Insurance program. + +# Jan-1 steps track the DWS calendar-year benefit schedule effective dates. +values: + 2024-01-01: 746 + 2025-01-01: 777 + 2026-01-01: 806 + +metadata: + unit: currency-USD + period: week + label: Utah UI maximum weekly benefit amount + reference: + - title: Utah Code § 35A-4-401(2)(b)(ii) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html + - title: Utah DWS Unemployment Insurance Benefit Schedule (Form 04-13-0126, archived 2024) + href: https://web.archive.org/web/20240613222926/https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 + - title: Utah DWS Unemployment Insurance Benefit Schedule (Form 04-13-0125, archived 2025) + href: https://web.archive.org/web/20250304230024/https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 + - title: Utah DWS Unemployment Insurance Benefit Schedule (current year — URL rolls over annually) + href: https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/subtraction.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/subtraction.yaml new file mode 100644 index 00000000000..2a318ca8fce --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/benefit/wba/subtraction.yaml @@ -0,0 +1,12 @@ +description: Utah deducts this amount from the high-quarter-derived weekly benefit amount under the Unemployment Insurance program. + +values: + 2024-01-01: 5 + +metadata: + unit: currency-USD + period: year + label: Utah UI weekly benefit amount subtraction + reference: + - title: Utah Code § 35A-4-401(2)(a)(ii) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/base_period_to_high_quarter_ratio.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/base_period_to_high_quarter_ratio.yaml new file mode 100644 index 00000000000..c9646fd8832 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/base_period_to_high_quarter_ratio.yaml @@ -0,0 +1,18 @@ +description: Utah uses this multiple of high-quarter wages as the minimum total base-period wages under the Unemployment Insurance program. + +# § 35A-4-403(1)(f)(ii) provides an alternative to this 1.5x ratio test — +# wages in insured work for at least 20 weeks in the base period. That +# alternative sunset on or before Jan 1, 2011, so only the ratio test is +# modeled. +values: + 2024-01-01: 1.5 + +metadata: + unit: /1 + period: year + label: Utah UI base period to high quarter wage ratio + reference: + - title: Utah Code § 35A-4-403(1)(f)(i) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S403.html + - title: Utah Admin Code R994-401-201 + href: https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-201 diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/min_base_period_wages.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/min_base_period_wages.yaml new file mode 100644 index 00000000000..0a6625edc0d --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/min_base_period_wages.yaml @@ -0,0 +1,25 @@ +description: Utah limits monetary eligibility to claimants with total base-period wages of at least this amount under the Unemployment Insurance program. + +# The floor is recomputed annually per Utah Code § 35A-4-201(16): +# 8% of the insured average fiscal-year wage for the preceding fiscal year, +# rounded up to the next higher multiple of $100. Each Jan-1 step tracks the +# DWS calendar-year benefit schedule effective date. The 2024 schedule PDF is +# published and states $5,100 directly (archived below). +values: + 2024-01-01: 5_100 + 2025-01-01: 5_300 + 2026-01-01: 5_500 + +metadata: + unit: currency-USD + period: year + label: Utah UI minimum base-period wages + reference: + - title: Utah Code § 35A-4-201(16) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S201.html + - title: Utah DWS Unemployment Insurance Benefit Schedule (Form 04-13-0126, archived 2024) + href: https://web.archive.org/web/20240613222926/https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 + - title: Utah DWS Unemployment Insurance Benefit Schedule (Form 04-13-0125, archived 2025) + href: https://web.archive.org/web/20250304230024/https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 + - title: Utah DWS Unemployment Insurance Benefit Schedule (current year — URL rolls over annually) + href: https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/min_quarters_with_wages.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/min_quarters_with_wages.yaml new file mode 100644 index 00000000000..43f7e80e170 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/eligibility/min_quarters_with_wages.yaml @@ -0,0 +1,16 @@ +description: Utah sets the minimum number of base-period quarters with wages to this threshold under the Unemployment Insurance program. + +values: + 2024-01-01: 2 + +metadata: + unit: /1 + period: year + label: Utah UI minimum quarters with wages + reference: + - title: Utah DWS Claimant Guide (Jan 2025) + href: https://jobs.utah.gov/ui/jobseeker/claimantguide.pdf#page=6 + - title: Utah Code § 35A-4-403(1)(f) + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S403.html + - title: Utah Admin Code R994-401-201 + href: https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-201 diff --git a/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/index.yaml b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/index.yaml new file mode 100644 index 00000000000..cb39cde8a79 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dws/unemployment_insurance/index.yaml @@ -0,0 +1,4 @@ +metadata: + propagate_metadata_to_children: true + economy: false + household: true diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 45012144afb..d2ae7f1ed7d 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1460,6 +1460,17 @@ programs: variable: ny_drive_clean_rebate parameter_prefix: gov.states.ny.nyserda.drive_clean + - id: ut_ui + name: Utah UI + full_name: Utah Unemployment Insurance + category: Benefits + agency: State + status: complete + coverage: UT + verified_years: "2024-2026" + variable: ut_ui + parameter_prefix: gov.states.ut.dws.unemployment_insurance + - id: tx_fpp_benefit name: Texas FPP full_name: Texas Family Planning Program diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/integration.yaml new file mode 100644 index 00000000000..36f811ebc1c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/integration.yaml @@ -0,0 +1,268 @@ +# Integration tests for Utah Unemployment Insurance (UT UI). +# End-to-end scenarios that exercise the headline ut_ui variable together +# with the intermediate WBA, duration, MBA, and weekly-payable variables. +# References: +# - Utah Code § 35A-4-401 (https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html) +# - Utah Code § 35A-4-403 (https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S403.html) +# - Utah Admin Code R994-401-301 (partial benefits) +# - Utah DWS 2026 Benefit Schedule (Form 04-13-0126), Claimant Guide (Jan 2025) + +- name: Case 1, no base-period wages yields zero benefit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 0 + ut_ui_high_quarter_wages: 0 + ut_ui_quarters_with_wages: 0 + ut_ui_weeks_unemployed: 0 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # All inputs zero -> fails $5,300 minimum -> not monetarily eligible. + ut_ui_monetarily_eligible: false + ut_ui_weekly_benefit_amount: 0 + ut_ui_duration_weeks: 0 + ut_ui_maximum_benefit_amount: 0 + ut_ui_weekly_payable_amount: 0 + ut_ui: 0 + +- name: Case 2, full-year unemployment yields max benefit in 2024. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 80_000 + ut_ui_high_quarter_wages: 25_000 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 26 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $80,000 >= 1.5 * $25,000 = $37,500. + ut_ui_monetarily_eligible: true + # Raw WBA = floor($25,000 / 26) - $5 = $961 - $5 = $956. + # 2024 cap = $746, so WBA = $746. + ut_ui_weekly_benefit_amount: 746 + # Raw duration = floor(floor(0.27 * $80,000) / $746) + # = floor($21,600 / $746) = floor(28.95) = 28, clamp 26. + ut_ui_duration_weeks: 26 + # MBA = $746 * 26 = $19,396. + ut_ui_maximum_benefit_amount: 19_396 + # Weekly payable = WBA when no earnings/hours. + ut_ui_weekly_payable_amount: 746 + # Annual = $746 * 26 = $19,396; MBA cap = $19,396. + ut_ui: 19_396 + +- name: Case 3, full-year unemployment hits 2026 max WBA cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 80_000 + ut_ui_high_quarter_wages: 25_000 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 26 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible. + ut_ui_monetarily_eligible: true + # Raw WBA = floor($25,000 / 26) - $5 = $956. + # 2026 cap = $806, so WBA = $806. + ut_ui_weekly_benefit_amount: 806 + # Raw duration = floor(floor(0.27 * $80,000) / $806) + # = floor($21,600 / $806) = floor(26.80) = 26. + # At max -> 26 weeks. + ut_ui_duration_weeks: 26 + # MBA = $806 * 26 = $20,956 (matches 2026 published max MBA). + ut_ui_maximum_benefit_amount: 20_956 + ut_ui_weekly_payable_amount: 806 + # Annual = $806 * 26 = $20,956. + ut_ui: 20_956 + +- name: Case 4, minimum-eligibility base wages produce 10-week duration. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + ut_ui_base_period_wages: 15_600 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 10 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible at the 1.5x boundary: + # $15,600 >= $5,300 minimum. + # $15,600 = 1.5 * $10,400. + ut_ui_monetarily_eligible: true + # WBA = floor($10,400 / 26) - $5 = $400 - $5 = $395. + ut_ui_weekly_benefit_amount: 395 + # Raw duration = floor(floor(0.27 * $15,600) / $395) + # = floor($4,212 / $395) = floor(10.66) = 10. + # At the 10-week floor. + ut_ui_duration_weeks: 10 + # MBA = $395 * 10 = $3,950. + ut_ui_maximum_benefit_amount: 3_950 + ut_ui_weekly_payable_amount: 395 + # Annual = $395 * 10 = $3,950. + ut_ui: 3_950 + +- name: Case 5, partial-week earnings reduce the weekly payable amount. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 15 + ut_ui_gross_weekly_earnings: 200 + ut_ui_weekly_hours_worked: 20 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $25,000 >= 1.5 * $10,400 = $15,600. + ut_ui_monetarily_eligible: true + # WBA = $395. + ut_ui_weekly_benefit_amount: 395 + # Duration = floor(floor(0.27 * $25,000) / $395) = floor(17.09) = 17. + ut_ui_duration_weeks: 17 + # MBA = $395 * 17 = $6,715. + ut_ui_maximum_benefit_amount: 6_715 + # 30% disregard = $118.50; excess = $200 - $118.50 = $81.50. + # Weekly payable = floor($395 - $81.50) = floor($313.50) = $313. + ut_ui_weekly_payable_amount: 313 + # Annual = $313 * 15 = $4,695; well below MBA cap. + ut_ui: 4_695 + +- name: Case 6, weekly hours of 40 or more produce zero benefit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 20 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 40 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible. + ut_ui_monetarily_eligible: true + # WBA = $395; duration = 17; MBA = $6,715. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 17 + ut_ui_maximum_benefit_amount: 6_715 + # Hours 40 >= 40-hour threshold -> disqualified -> weekly payable = $0. + ut_ui_weekly_payable_amount: 0 + # Annual = $0 * 20 = $0. + ut_ui: 0 + +- name: Case 7, out-of-state household (Texas) yields zero Utah UI benefit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 26 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: TX + output: + people: + person1: + # ut_ui has defined_for = StateCode.UT -> zero for non-UT households, + # regardless of monetary inputs. + ut_ui: 0 + +# --------------------------------------------------------------------------- +# S9(d) WEEKS UNEMPLOYED EXACTLY EQUAL TO DURATION — the min_(WBA * weeks, MBA) +# cap binds at equality (annual == MBA). Also the 10-week duration floor +# (S9(a)) is effectively unreachable: the raw duration only drops toward 10 as +# base wages approach the 1.5x monetary boundary, which is already exercised by +# integration Case 4 without a separately contrived input, so no extra case is +# added for it here. +- name: Case 8, weeks unemployed exactly equal to duration weeks hit MBA cap. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 17 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $25,000 >= 1.5 * $10,400 = $15,600. + # WBA = $395; duration = 17; MBA = $395 * 17 = $6,715. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 17 + ut_ui_maximum_benefit_amount: 6_715 + ut_ui_weekly_payable_amount: 395 + # weeks_unemployed (17) == duration (17): annual = $395 * 17 = $6,715. + # min($6,715, MBA $6,715) = $6,715 -> cap binds exactly at equality. + ut_ui: 6_715 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_duration_weeks.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_duration_weeks.yaml new file mode 100644 index 00000000000..846a6540e8f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_duration_weeks.yaml @@ -0,0 +1,102 @@ +# Tests for Utah UI potential duration in weeks. +# Formula (§ 35A-4-401(4)(b)): +# raw_weeks = floor(floor(0.27 * base_period_wages) / WBA) +# duration_weeks = clip(raw_weeks, 10, 26) +# Duration is 0 when WBA = 0 (e.g., not monetarily eligible). + +- name: Case 1, high base wages clamped to 26-week maximum. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 50_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $50,000 >= 1.5 * $10,400 = $15,600. + # WBA = floor($10,400 / 26) - $5 = $400 - $5 = $395. + # Raw weeks = floor(floor(0.27 * $50,000) / $395) + # = floor($13,500 / $395) = floor(34.18) = 34. + # Clamp to max 26 weeks. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 26 + +- name: Case 2, minimum-eligibility base wages produce 10-week floor. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 15_600 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible at the 1.5x boundary: $15,600 = 1.5 * $10,400. + # WBA = $395. + # Raw weeks = floor(floor(0.27 * $15,600) / $395) + # = floor($4,212 / $395) = floor(10.66) = 10. + # At the 10-week floor. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 10 + +- name: Case 3, mid-range base wages produce 17-week duration. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $25,000 >= 1.5 * $10,400 = $15,600. + # WBA = $395. + # Raw weeks = floor(floor(0.27 * $25,000) / $395) + # = floor($6,750 / $395) = floor(17.09) = 17. + # Between 10 and 26 -> no clamping. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 17 + +- name: Case 4, WBA equals zero yields zero duration. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_000 + ut_ui_high_quarter_wages: 2_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # $5,000 < $5,300 minimum -> not monetarily eligible -> WBA = 0. + # Duration = 0 (bypasses 10-week floor when WBA = 0). + ut_ui_weekly_benefit_amount: 0 + ut_ui_duration_weeks: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_edge_cases.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_edge_cases.yaml new file mode 100644 index 00000000000..6105d612951 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_edge_cases.yaml @@ -0,0 +1,792 @@ +# Edge case tests for Utah Unemployment Insurance (UT UI). +# +# Covers boundary conditions for the three monetary eligibility tests, +# the WBA formula and cap, the duration clamp, partial-benefit +# disregard/hour rules, annual MBA cap, and the StateCode.UT gate. +# +# References: +# - Utah Code § 35A-4-201(16) (minimum base-period wages) +# https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S201.html +# - Utah Code § 35A-4-401 (WBA, duration, MBA, partial benefit) +# https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html +# - Utah Code § 35A-4-403(1)(f)(i) (1.5x ratio test) +# https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S403.html +# - Utah Admin Code R994-401-201 (quarters test) +# https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-201 +# - Utah Admin Code R994-401-301 (partial benefit) +# https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-301 +# +# --------------------------------------------------------------------------- +# THRESHOLD BOUNDARIES — minimum base-period wages +# ($5,100 in 2024; $5,300 in 2025; $5,500 in 2026) +# --------------------------------------------------------------------------- + +- name: Case 1, base wages one dollar below $5,300 floor fails minimum test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_299 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,299 < $5,300 minimum -> fails. + # (Tests 2 and 3 would pass: 4 quarters >= 2; 5299 >= 1.5*3000=4500.) + ut_ui_monetarily_eligible: false + +- name: Case 2, base wages exactly at $5,300 floor pass the minimum test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_300 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,300 >= $5,300 minimum (>=, not strict). + # Test 2: 4 quarters >= 2. + # Test 3: $5,300 >= 1.5*$3,000 = $4,500. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +- name: Case 3, base wages one dollar above $5,300 floor pass the minimum test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_301 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,301 >= $5,300 minimum. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +# --------------------------------------------------------------------------- +# THRESHOLD BOUNDARIES — 1.5x high-quarter ratio +# --------------------------------------------------------------------------- + +- name: Case 4, base wages exactly 1.5 times high quarter pass the ratio test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 15_000 + ut_ui_high_quarter_wages: 10_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $15,000 >= $5,300 minimum. + # Test 2: 4 quarters >= 2. + # Test 3: $15,000 = 1.5 * $10,000 exactly (>=, not strict). + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +- name: Case 5, base wages one cent below 1.5x high quarter fails the ratio test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 14_999.99 + ut_ui_high_quarter_wages: 10_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 3: $14,999.99 < 1.5 * $10,000 = $15,000.00 -> fails. + ut_ui_monetarily_eligible: false + +# --------------------------------------------------------------------------- +# THRESHOLD BOUNDARIES — quarters with wages +# --------------------------------------------------------------------------- + +- name: Case 6, only one quarter of wages fails the quarter-count test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 15_000 + ut_ui_high_quarter_wages: 10_000 + ut_ui_quarters_with_wages: 1 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 2: 1 quarter < 2 required -> fails. + ut_ui_monetarily_eligible: false + +- name: Case 7, exactly two quarters of wages pass the quarter-count test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 15_000 + ut_ui_high_quarter_wages: 10_000 + ut_ui_quarters_with_wages: 2 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 2: 2 quarters >= 2 required (>=, not strict). + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +# --------------------------------------------------------------------------- +# WBA EDGE CASES — zero, negative-protected, and at-max boundaries +# --------------------------------------------------------------------------- + +- name: Case 8, zero high-quarter wages with monetary failure yields zero WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 0 + ut_ui_high_quarter_wages: 0 + ut_ui_quarters_with_wages: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Raw WBA = floor(0/26) - 5 = -5 -> max_(.,0) = 0 (no negative WBA). + # Also not monetarily eligible, so WBA forced to 0. + ut_ui_monetarily_eligible: false + ut_ui_weekly_benefit_amount: 0 + +- name: Case 9, very low high quarter with floor(HQ/26)<5 still yields zero WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 300 + ut_ui_high_quarter_wages: 100 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Consistent inputs: HQ is the highest single quarter, so total + # base-period wages ($300) cannot exceed 4 x HQ. Such a claimant is + # far below the $5,300 minimum and thus not monetarily eligible. + ut_ui_monetarily_eligible: false + # Raw WBA = floor(100/26) - 5 = 3 - 5 = -2 -> max_(.,0) = 0. + # Tests negative-protection: WBA must never be negative (and the + # monetary-ineligibility gate independently forces WBA to 0). + ut_ui_weekly_benefit_amount: 0 + +- name: Case 10, high quarter at exact lower boundary of 2024 WBA cap. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 50_000 + ut_ui_high_quarter_wages: 19_526 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $50,000 >= 1.5 * $19,526 = $29,289. + # Raw WBA = floor(19,526/26) - 5 = floor(751.0) - 5 = 751 - 5 = $746. + # 2024 cap = $746 -> WBA exactly hits the cap with no clipping. + ut_ui_weekly_benefit_amount: 746 + +- name: Case 11, high quarter of $18,668 stays below the 2024 cap. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 50_000 + ut_ui_high_quarter_wages: 18_668 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Raw WBA = floor(18,668/26) - 5 = floor(718.0) - 5 = 718 - 5 = $713. + # 2024 cap = $746 -> $713 < cap, so no cap applies. + ut_ui_weekly_benefit_amount: 713 + +- name: Case 12, high quarter at exact lower boundary of 2025 WBA cap. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 50_000 + ut_ui_high_quarter_wages: 20_332 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $50,000 >= 1.5 * $20,332 = $30,498. + # Raw WBA = floor(20,332/26) - 5 = floor(782.0) - 5 = 782 - 5 = $777. + # 2025 cap = $777 -> WBA exactly hits the cap with no clipping. + ut_ui_weekly_benefit_amount: 777 + +- name: Case 13, high quarter at exact lower boundary of 2026 WBA cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 50_000 + ut_ui_high_quarter_wages: 21_086 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $50,000 >= 1.5 * $21,086 = $31,629. + # Raw WBA = floor(21,086/26) - 5 = floor(811.0) - 5 = 811 - 5 = $806. + # 2026 cap = $806 -> WBA exactly hits the cap with no clipping. + ut_ui_weekly_benefit_amount: 806 + +# --------------------------------------------------------------------------- +# DURATION EDGE CASES — clamps and divide-by-zero guard +# --------------------------------------------------------------------------- + +- name: Case 14, zero WBA yields zero duration (divide-by-zero guard). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 300 + ut_ui_high_quarter_wages: 100 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = 0 (see Case 9; consistent HQ=$100/base=$300 combo). Duration + # formula short-circuits to 0 rather than dividing by zero or + # returning the 10-week floor. + ut_ui_weekly_benefit_amount: 0 + ut_ui_duration_weeks: 0 + +- name: Case 15, base wages exactly at 1.5x ratio produce 10-week minimum duration. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + ut_ui_base_period_wages: 15_600 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible at the 1.5x boundary: $15,600 = 1.5 * $10,400. + # WBA = floor($10,400/26) - 5 = $400 - $5 = $395. + # Raw weeks = floor(floor(0.27 * $15,600) / $395) + # = floor($4,212 / $395) = floor(10.66) = 10. + # At the 10-week floor (in practice, the 1.5x monetary boundary + # forces raw_weeks asymptotically toward 10; the floor clamp is a + # safety guarantee even though it rarely binds). + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 10 + +- name: Case 16, ratio of base*0.27/WBA at exactly 26 yields 26-week duration. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 9_630 + ut_ui_high_quarter_wages: 2_730 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $9,630 >= $5,300 min; 4 quarters >= 2; + # $9,630 >= 1.5 * $2,730 = $4,095. + # WBA = floor($2,730/26) - 5 = $105 - $5 = $100. + # Step 1: floor(0.27 * $9,630) = floor($2,600.10) = $2,600. + # Step 2: $2,600 / $100 = 26.0 -> floor = 26. + # Exactly at the 26-week cap with no clamping. + ut_ui_weekly_benefit_amount: 100 + ut_ui_duration_weeks: 26 + +- name: Case 17, ratio of base*0.27/WBA at 26.5 floors to 26-week duration. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 9_815 + ut_ui_high_quarter_wages: 2_730 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $100 (same HQ as Case 16). + # Step 1: floor(0.27 * $9,815) = floor($2,650.05) = $2,650. + # Step 2: $2,650 / $100 = 26.5 -> floor = 26. + # Hit the 26-week cap through clamping. + ut_ui_weekly_benefit_amount: 100 + ut_ui_duration_weeks: 26 + +# --------------------------------------------------------------------------- +# PARTIAL BENEFIT EDGE CASES — 30% disregard, WBA floor, 40-hour rule +# --------------------------------------------------------------------------- + +- name: Case 18, earnings exactly at WBA disqualify the week. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 395 + ut_ui_weekly_hours_worked: 20 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395 (HQ $10,400). Earnings $395 >= WBA $395 -> disqualified. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 0 + +- name: Case 19, earnings just below the 30% disregard preserve full WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 118 + ut_ui_weekly_hours_worked: 10 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395. Disregard = 0.30 * $395 = $118.50. + # Earnings = $118 (just below $118.50 disregard). + # Excess = max(0, $118 - $118.50) = max(0, -$0.50) = $0. + # Payable = floor(max(0, $395 - $0)) = $395. + # NB: testing exactly $118.50 is avoided due to IEEE-754 imprecision + # in 0.30 * WBA, which would yield a sub-cent positive excess and + # floor the result to $394. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 395 + +- name: Case 20, earnings just above 30% disregard yield a small reduction. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 120 + ut_ui_weekly_hours_worked: 10 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395. Disregard = $118.50. + # Excess = $120 - $118.50 = $1.50. + # Payable = floor(max(0, $395 - $1.50)) = floor($393.50) = $393. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 393 + +- name: Case 21, weekly hours of 39 do not trigger the 40-hour disqualification. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 39 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395. 39 hours < 40-hour threshold -> not disqualified. + # Earnings $0 < disregard $118.50 -> excess $0 -> payable = $395. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 395 + +- name: Case 22, weekly hours exactly at 40 trigger the disqualification. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 40 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395. 40 hours >= 40-hour threshold (>=, not strict) -> + # disqualified -> weekly payable = $0. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 0 + +- name: Case 23, weekly hours of 41 trigger the disqualification. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 41 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # 41 hours > 40-hour threshold -> disqualified -> payable = $0. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 0 + +- name: Case 24, negative weekly earnings (data error) do not inflate payable above WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: -100 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395. Disregard = $118.50. + # Excess = max(0, -$100 - $118.50) = max(0, -$218.50) = $0. + # Payable = floor(max(0, $395 - $0)) = $395. + # Critical: negative earnings cannot push payable above WBA via the + # max_() clamps on both the excess and the final payable. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 395 + +# --------------------------------------------------------------------------- +# ANNUAL BENEFIT (MBA cap) EDGE CASES +# --------------------------------------------------------------------------- + +- name: Case 25, zero weeks unemployed produce zero annual benefit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 0 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395; duration = 17; MBA = $6,715; payable = $395. + # Annual = min($395 * 0, $6,715) = min($0, $6,715) = $0. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 17 + ut_ui_maximum_benefit_amount: 6_715 + ut_ui_weekly_payable_amount: 395 + ut_ui: 0 + +- name: Case 26, weeks unemployed exceeding duration weeks cap at MBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 26 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395; duration = 17; MBA = $395 * 17 = $6,715. + # Uncapped annual = $395 * 26 = $10,270. + # min($10,270, $6,715) = $6,715 -> capped at MBA. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 17 + ut_ui_maximum_benefit_amount: 6_715 + ut_ui_weekly_payable_amount: 395 + ut_ui: 6_715 + +# --------------------------------------------------------------------------- +# STATE GATE — defined_for = StateCode.UT +# --------------------------------------------------------------------------- + +- name: Case 27, out-of-state household (California) yields zero Utah UI benefit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 80_000 + ut_ui_high_quarter_wages: 25_000 + ut_ui_quarters_with_wages: 4 + ut_ui_weeks_unemployed: 26 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: CA + output: + people: + person1: + # ut_ui has defined_for = StateCode.UT -> zero for non-UT households, + # regardless of all monetary and benefit inputs. + ut_ui: 0 + +# --------------------------------------------------------------------------- +# THRESHOLD BOUNDARIES — cross-year regression tests +# (2024 floor: $5,100; 2026 floor: $5,500) +# --------------------------------------------------------------------------- + +- name: Case 28, 2024 base wages one dollar below $5,100 floor fail minimum test. + period: 2024 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_099 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,099 < $5,100 (2024 minimum) -> fails. + # (Tests 2 and 3 would pass: 4 quarters >= 2; 5,099 >= 1.5*3,000=4,500.) + ut_ui_monetarily_eligible: false + +- name: Case 29, 2024 base wages exactly at $5,100 floor pass the minimum test. + period: 2024 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_100 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,100 >= $5,100 (2024 minimum). + # Test 2: 4 quarters >= 2. + # Test 3: $5,100 >= 1.5*$3,000 = $4,500. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +- name: Case 30, 2026 base wages one dollar below $5,500 floor fail minimum test. + period: 2026 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_499 + ut_ui_high_quarter_wages: 3_200 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,499 < $5,500 (2026 minimum) -> fails. + ut_ui_monetarily_eligible: false + +- name: Case 31, 2026 base wages exactly at $5,500 floor pass the minimum test. + period: 2026 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_500 + ut_ui_high_quarter_wages: 3_200 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,500 >= $5,500 (2026 minimum). + # Test 2: 4 quarters >= 2. + # Test 3: $5,500 >= 1.5*$3,200 = $4,800. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +- name: Case 32, 2026 base wages at $5,300 fail (regression — 2025 floor would pass). + period: 2026 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_300 + ut_ui_high_quarter_wages: 3_200 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,300 < $5,500 (2026 minimum) -> fails. + # Regression guard: a collapse of the parameter into a single value + # across years would let this case pass under the 2025 floor. + ut_ui_monetarily_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_maximum_benefit_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_maximum_benefit_amount.yaml new file mode 100644 index 00000000000..84440f7bf9d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_maximum_benefit_amount.yaml @@ -0,0 +1,48 @@ +# Tests for Utah UI maximum benefit amount. +# Formula (§ 35A-4-401(4)(a)): MBA = WBA * duration_weeks. + +- name: Case 1, standard MBA equals WBA times duration weeks. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395; duration = 17 weeks (see ut_ui_duration_weeks Case 3). + # MBA = $395 * 17 = $6,715. + ut_ui_weekly_benefit_amount: 395 + ut_ui_duration_weeks: 17 + ut_ui_maximum_benefit_amount: 6_715 + +- name: Case 2, MBA equals zero when WBA equals zero. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_000 + ut_ui_high_quarter_wages: 2_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Not monetarily eligible -> WBA = 0 -> duration = 0. + # MBA = 0 * 0 = $0. + ut_ui_weekly_benefit_amount: 0 + ut_ui_duration_weeks: 0 + ut_ui_maximum_benefit_amount: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_monetarily_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_monetarily_eligible.yaml new file mode 100644 index 00000000000..22678cc1da8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_monetarily_eligible.yaml @@ -0,0 +1,151 @@ +# Tests for Utah UI monetary eligibility. +# A claimant is monetarily eligible only if ALL THREE base-period tests pass: +# 1. Total base-period wages >= the annual minimum ($5,100 in 2024; +# $5,300 in 2025; $5,500 in 2026), per Utah Code § 35A-4-201(16). +# 2. Wages paid in at least 2 calendar quarters (Utah Admin Code R994-401-201). +# 3. Total base-period wages >= 1.5 x high-quarter wages (§ 35A-4-403(1)(f)(i)). + +- name: Case 1, all three monetary tests pass. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 8_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $25,000 >= $5,300 minimum. + # Test 2: 4 quarters >= 2 required. + # Test 3: $25,000 >= 1.5 * $8,000 = $12,000. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +- name: Case 2, base wages below $5,300 floor fails minimum-wage test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_000 + ut_ui_high_quarter_wages: 2_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,000 < $5,300 minimum -> fails. + ut_ui_monetarily_eligible: false + +- name: Case 3, only one quarter of wages fails quarter-count test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 8_000 + ut_ui_quarters_with_wages: 1 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 2: only 1 quarter with wages < 2 required -> fails. + ut_ui_monetarily_eligible: false + +- name: Case 4, base wages 1.4x high quarter fails ratio test. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 14_000 + ut_ui_high_quarter_wages: 10_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 3: $14,000 < 1.5 * $10,000 = $15,000 -> fails. + ut_ui_monetarily_eligible: false + +# --------------------------------------------------------------------------- +# G5 DEFENSIVE — negative wages (data error) and just-above-floor boundaries. +- name: Case 5, negative base-period and high-quarter wages fail eligibility. + period: 2025 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: -10_000 + ut_ui_high_quarter_wages: -3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: -$10,000 < $5,300 minimum -> fails. + # Negative wages can never satisfy monetary eligibility. + ut_ui_monetarily_eligible: false + +- name: Case 6, 2024 base wages one dollar above $5,100 floor pass eligibility. + period: 2024 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_101 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,101 >= $5,100 (2024 minimum). + # Test 2: 4 quarters >= 2. + # Test 3: $5,101 >= 1.5 * $3,000 = $4,500. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true + +- name: Case 7, 2026 base wages one dollar above $5,500 floor pass eligibility. + period: 2026 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_501 + ut_ui_high_quarter_wages: 3_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Test 1: $5,501 >= $5,500 (2026 minimum). + # Test 2: 4 quarters >= 2. + # Test 3: $5,501 >= 1.5 * $3,000 = $4,500. + # All tests pass -> monetarily eligible. + ut_ui_monetarily_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_benefit_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_benefit_amount.yaml new file mode 100644 index 00000000000..2296a787443 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_benefit_amount.yaml @@ -0,0 +1,168 @@ +# Tests for Utah UI weekly benefit amount. +# Formula: WBA = floor(high_quarter_wages / 26) - $5 (§ 35A-4-401(2)(a)(ii)). +# Then capped at the annual max WBA (§ 35A-4-401(2)(b)(ii)): +# 2024: $746; 2025: $777; 2026: $806. +# WBA = 0 when claimant is not monetarily eligible. + +- name: Case 1, standard WBA from $10,000 high quarter. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $25,000 >= $5,300 and >= 1.5 * $10,000 = $15,000. + # WBA = floor($10,000 / 26) - $5 = $384 - $5 = $379. + # $379 below 2025 cap of $777, so no cap applies. + ut_ui_weekly_benefit_amount: 379 + +- name: Case 2, WBA capped at 2024 max of $746. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 50_000 + ut_ui_high_quarter_wages: 20_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $50,000 >= 1.5 * $20,000 = $30,000. + # Raw WBA = floor($20,000 / 26) - $5 = $769 - $5 = $764. + # 2024 cap = $746, so WBA = $746. + ut_ui_weekly_benefit_amount: 746 + +- name: Case 3, WBA capped at 2026 max of $806. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 80_000 + ut_ui_high_quarter_wages: 25_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $80,000 >= 1.5 * $25,000 = $37,500. + # Raw WBA = floor($25,000 / 26) - $5 = $961 - $5 = $956. + # 2026 cap = $806, so WBA = $806. + ut_ui_weekly_benefit_amount: 806 + +- name: Case 4, monetarily ineligible yields zero WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 5_000 + ut_ui_high_quarter_wages: 2_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # $5,000 < $5,300 minimum -> not monetarily eligible. + # WBA = 0 regardless of high-quarter calculation. + ut_ui_weekly_benefit_amount: 0 + +# --------------------------------------------------------------------------- +# S9(b) RAW WBA SUBTRACTION FLOOR — max_(floor(HQ/26) - 5, 0) zero-crossing. +# The raw amount turns positive between HQ $130 (floor 5 -> 5-5 = 0) and +# HQ $156 (floor 6 -> 6-5 = 1). Because HQ is the single highest quarter, +# base-period wages can never exceed 4 x HQ, so a claimant with HQ in this +# range ($<= $624 base) is always far below the $5,300 minimum and thus not +# monetarily eligible. Both boundary cases therefore output WBA = 0; the pair +# documents that the raw subtraction floor never emits a negative value. +- name: Case 5, raw WBA subtraction at exactly zero (HQ $130) yields zero WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 520 + ut_ui_high_quarter_wages: 130 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Raw WBA = floor(130/26) - 5 = 5 - 5 = 0 (crossing point). + # Not monetarily eligible ($520 < $5,300) -> WBA = 0. + ut_ui_monetarily_eligible: false + ut_ui_weekly_benefit_amount: 0 + +- name: Case 6, raw WBA subtraction just positive (HQ $156) still yields zero WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 624 + ut_ui_high_quarter_wages: 156 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Raw WBA = floor(156/26) - 5 = 6 - 5 = 1 (just above the crossing). + # Not monetarily eligible ($624 < $5,300) -> WBA still forced to 0. + ut_ui_monetarily_eligible: false + ut_ui_weekly_benefit_amount: 0 + +# --------------------------------------------------------------------------- +# G5 DEFENSIVE — negative high-quarter wages (data error) never inflate WBA. +- name: Case 7, negative high-quarter wages yield zero WBA (no negative benefit). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: -5_000 + ut_ui_high_quarter_wages: -2_000 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Negative base wages fail the $5,300 minimum -> not eligible. + # Raw WBA = floor(-2000/26) - 5 = -78 - 5 = -83 -> max_(.,0) = 0. + ut_ui_monetarily_eligible: false + ut_ui_weekly_benefit_amount: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_payable_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_payable_amount.yaml new file mode 100644 index 00000000000..09beadaab6b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_payable_amount.yaml @@ -0,0 +1,158 @@ +# Tests for Utah UI weekly payable amount. +# Combines weekly benefit amount with the partial-benefit rules: +# - 30% earnings disregard: first 0.30 * WBA of weekly earnings is disregarded +# (Utah Code § 35A-4-401(3)(a); Utah Admin Code R994-401-301). +# - Excess earnings (above 0.30 * WBA) reduce WBA dollar-for-dollar. +# - Earnings >= WBA disqualify the week (Claimant Guide p. 11; PDF file p. 13). +# - Weekly hours >= 40 disqualify the week (Claimant Guide p. 11; PDF file p. 13; § 35A-4-207). +# Result is floored to a whole dollar per § 35A-4-401(3)(b). +# +# All cases below use WBA = $395 (HQ = $10,400, base = $25,000, 4 quarters). +# 30% disregard = 0.30 * $395 = $118.50. + +- name: Case 1, zero earnings yields full WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 0 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Earnings $0 < disregard $118.50 -> excess = 0. + # Payable = floor($395 - $0) = $395. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 395 + +- name: Case 2, earnings below 30% disregard yield full WBA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 100 + ut_ui_weekly_hours_worked: 20 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Earnings $100 < disregard $118.50 -> excess = 0. + # Payable = floor($395 - $0) = $395. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 395 + +- name: Case 3, earnings above disregard but below WBA produce partial reduction. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 200 + ut_ui_weekly_hours_worked: 20 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Earnings $200 > disregard $118.50. + # Excess = $200 - $118.50 = $81.50. + # Raw payable = floor($395 - $81.50) = floor($313.50) = $313. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 313 + +- name: Case 4, earnings at or above WBA disqualify the week. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 400 + ut_ui_weekly_hours_worked: 20 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Earnings $400 >= WBA $395 -> disqualified. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 0 + +- name: Case 5, weekly hours of 40 or more disqualify the week. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 0 + ut_ui_weekly_hours_worked: 40 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Earnings $0 < WBA, BUT hours 40 >= 40-hour threshold -> disqualified. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 0 + +# --------------------------------------------------------------------------- +# S9(c) EARNINGS ONE DOLLAR BELOW WBA — still payable (disqualification is at +# earnings >= WBA, so WBA-1 does not disqualify). +- name: Case 6, earnings one dollar below WBA remain payable. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 25_000 + ut_ui_high_quarter_wages: 10_400 + ut_ui_quarters_with_wages: 4 + ut_ui_gross_weekly_earnings: 394 + ut_ui_weekly_hours_worked: 20 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # WBA = $395. Earnings $394 < WBA $395 -> not disqualified. + # Disregard = $118.50; excess = $394 - $118.50 = $275.50. + # Payable = floor(max(0, $395 - $275.50)) = floor($119.50) = $119. + ut_ui_weekly_benefit_amount: 395 + ut_ui_weekly_payable_amount: 119 diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui.py new file mode 100644 index 00000000000..a7d982f223b --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class ut_ui(Variable): + value_type = float + entity = Person + label = "Utah Unemployment Insurance" + unit = USD + definition_period = YEAR + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html" + defined_for = StateCode.UT + # Annual Utah Unemployment Insurance benefit. Computed as the weekly + # payable amount times weeks unemployed, capped at the maximum benefit + # amount (WBA x duration) per Utah Code § 35A-4-401(4)(a). + # + # Not modeled (future work): + # - REQ-023: 100% retirement income offset for pensions maintained or + # contributed to by a base-period employer (Utah Code + # § 35A-4-401(2)(c)(i)-(iii); Utah Admin Code R994-401-203). Modeling + # requires base-period-employer attribution that is not available in + # the underlying microdata. + # - Waiting week: the first otherwise-eligible week is unpaid (Claimant + # Guide p.16). Not modeled — the annual formula does not track the + # week-by-week claim sequence. + # - Deductions from the benefit payment that do not change entitlement: + # child-support withholding of up to 50% of the WBA, and voluntary + # federal (10%) and Utah state (5%) income-tax withholding (Claimant + # Guide p.19). Not modeled — these are payment-side withholdings, not + # reductions in the benefit amount. + + def formula(person, period, parameters): + weekly_payable_amount = person("ut_ui_weekly_payable_amount", period) + weeks_unemployed = person("ut_ui_weeks_unemployed", period) + maximum_benefit_amount = person("ut_ui_maximum_benefit_amount", period) + annual_benefit = weekly_payable_amount * weeks_unemployed + return min_(annual_benefit, maximum_benefit_amount) diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_base_period_wages.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_base_period_wages.py new file mode 100644 index 00000000000..c74a2844cba --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_base_period_wages.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class ut_ui_base_period_wages(Variable): + value_type = float + entity = Person + label = "Utah UI base period wages" + unit = USD + definition_period = YEAR + default_value = 0 + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S201.html" + # Per Utah Code § 35A-4-201(1)(a), the base period is the first four of the + # last five completed calendar quarters preceding the first day of the + # benefit year. Per § 35A-4-201(1)(b), claims effective on or after + # 2011-01-02 may use the alternate base period (last four completed + # quarters) when the standard base period fails monetary eligibility. + # Caller should supply the more favorable of standard or alternate base + # period wages. + # Defaults to 0, so in microsimulation (where this input is not populated) + # claimants are treated as having no base-period wages and are ineligible. diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_duration_weeks.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_duration_weeks.py new file mode 100644 index 00000000000..e7973b08e1d --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_duration_weeks.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * + + +class ut_ui_duration_weeks(Variable): + value_type = int + entity = Person + label = "Utah UI potential duration in weeks" + unit = "week" + definition_period = YEAR + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html" + + def formula(person, period, parameters): + p = parameters(period).gov.states.ut.dws.unemployment_insurance + base_period_wages = person("ut_ui_base_period_wages", period) + weekly_benefit_amount = person("ut_ui_weekly_benefit_amount", period) + + # § 35A-4-401(4)(b): potential duration = + # floor(floor(0.27 × total base-period wages) / WBA), + # clamped to [min_weeks, max_weeks] (10-26 weeks). + # Guard against divide-by-zero when WBA = 0 (not monetarily eligible + # or WBA capped to zero). + has_wba = weekly_benefit_amount > 0 + numerator = np.floor(p.benefit.duration.multiplier * base_period_wages) + # Use a safe divisor of 1 where WBA is zero; the where() below + # overrides the result with 0 in that case. + safe_wba = where(has_wba, weekly_benefit_amount, 1) + raw_weeks = np.floor(numerator / safe_wba) + clamped_weeks = np.clip( + raw_weeks, p.benefit.duration.min_weeks, p.benefit.duration.max_weeks + ) + return where(has_wba, clamped_weeks, 0) diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_gross_weekly_earnings.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_gross_weekly_earnings.py new file mode 100644 index 00000000000..d11ae999cc0 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_gross_weekly_earnings.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class ut_ui_gross_weekly_earnings(Variable): + value_type = float + entity = Person + label = "Utah UI gross weekly earnings while claiming" + unit = USD + definition_period = YEAR + default_value = 0 + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html" + # Gross weekly earnings during a partial-unemployment week used to apply + # the partial-benefit deduction in Utah Code § 35A-4-401(3)(a). Per + # § 35A-4-401(3)(c), public assistance grants and similar government + # payments are excluded from "wages" for this calculation, so the caller + # is responsible for providing only countable earnings here (excluding + # public assistance benefits). + # Defaults to 0, so in microsimulation (where this input is not populated) + # claimants are treated as having no earnings while claiming. diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_high_quarter_wages.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_high_quarter_wages.py new file mode 100644 index 00000000000..fe666cb9d0c --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_high_quarter_wages.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class ut_ui_high_quarter_wages(Variable): + value_type = float + entity = Person + label = "Utah UI high quarter wages" + unit = USD + definition_period = YEAR + default_value = 0 + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S201.html" + # Wages paid during the calendar quarter of the base period in which the + # claimant's wages were highest, used to compute the weekly benefit amount + # under Utah Code § 35A-4-401(2)(a)(ii). + # Defaults to 0, so in microsimulation (where this input is not populated) + # claimants are treated as having no high-quarter wages. diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_maximum_benefit_amount.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_maximum_benefit_amount.py new file mode 100644 index 00000000000..2e22f2edce8 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_maximum_benefit_amount.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class ut_ui_maximum_benefit_amount(Variable): + value_type = float + entity = Person + label = "Utah UI maximum benefit amount" + unit = USD + definition_period = YEAR + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html" + + def formula(person, period, parameters): + # § 35A-4-401(4)(a): maximum benefit amount equals the weekly benefit + # amount times the potential duration in weeks. Returns zero when the + # claimant is not monetarily eligible (WBA = 0 or duration = 0). + weekly_benefit_amount = person("ut_ui_weekly_benefit_amount", period) + duration_weeks = person("ut_ui_duration_weeks", period) + return weekly_benefit_amount * duration_weeks diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_monetarily_eligible.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_monetarily_eligible.py new file mode 100644 index 00000000000..c421f96ae06 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_monetarily_eligible.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class ut_ui_monetarily_eligible(Variable): + value_type = bool + entity = Person + label = "Monetarily eligible for Utah Unemployment Insurance" + definition_period = YEAR + reference = ( + "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S201.html", + "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S403.html", + "https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-201", + ) + # Not modeled: § 35A-4-403(1)(f)(ii) provides an alternative to the 1.5x + # high-quarter-wage ratio test — wages in insured work for at least 20 + # weeks in the base period. That alternative sunset on or before Jan 1, + # 2011, so only the § 35A-4-403(1)(f)(i) ratio test is modeled here. + + def formula(person, period, parameters): + p = parameters(period).gov.states.ut.dws.unemployment_insurance + base_period_wages = person("ut_ui_base_period_wages", period) + high_quarter_wages = person("ut_ui_high_quarter_wages", period) + quarters_with_wages = person("ut_ui_quarters_with_wages", period) + + # § 35A-4-201(16): minimum total base-period wages. + meets_minimum_wages = base_period_wages >= p.eligibility.min_base_period_wages + # R994-401-201: wages in at least two base-period quarters. + meets_quarters_test = ( + quarters_with_wages >= p.eligibility.min_quarters_with_wages + ) + # § 35A-4-403(1)(f)(i): total base-period wages must be at least + # 1.5 times high-quarter wages. + meets_ratio_test = base_period_wages >= ( + p.eligibility.base_period_to_high_quarter_ratio * high_quarter_wages + ) + return meets_minimum_wages & meets_quarters_test & meets_ratio_test diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_quarters_with_wages.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_quarters_with_wages.py new file mode 100644 index 00000000000..a5b77a59323 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_quarters_with_wages.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class ut_ui_quarters_with_wages(Variable): + value_type = int + entity = Person + label = "Utah UI base-period quarters with wages" + definition_period = YEAR + default_value = 0 + reference = ( + "https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-201" + ) + # Number of base-period calendar quarters (0-4) in which the claimant + # earned wages. Utah Admin Code R994-401-201 requires wages in at least + # two base-period quarters. + # Defaults to 0, so in microsimulation (where this input is not populated) + # claimants are treated as having wages in no base-period quarters. diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_benefit_amount.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_benefit_amount.py new file mode 100644 index 00000000000..2ce4867cb78 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_benefit_amount.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class ut_ui_weekly_benefit_amount(Variable): + value_type = float + entity = Person + label = "Utah UI weekly benefit amount" + unit = USD + definition_period = YEAR + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html" + + def formula(person, period, parameters): + p = parameters(period).gov.states.ut.dws.unemployment_insurance + high_quarter_wages = person("ut_ui_high_quarter_wages", period) + monetarily_eligible = person("ut_ui_monetarily_eligible", period) + + # § 35A-4-401(2)(a)(ii): WBA = floor(high quarter wages / 26) - $5. + # Utah has no statutory minimum WBA, so the raw amount is floored at + # zero rather than at a positive minimum. + raw_wba = ( + np.floor(high_quarter_wages / p.benefit.wba.divisor) + - p.benefit.wba.subtraction + ) + # § 35A-4-401(2)(b)(ii): WBA is capped at the annual maximum. + capped_wba = min_(max_(raw_wba, 0), p.benefit.wba.max_amount) + # Zero out when the claimant is not monetarily eligible. + return capped_wba * monetarily_eligible diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_hours_worked.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_hours_worked.py new file mode 100644 index 00000000000..57498b5803a --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_hours_worked.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class ut_ui_weekly_hours_worked(Variable): + value_type = float + entity = Person + label = "Utah UI weekly hours worked while claiming" + unit = "hour" + definition_period = YEAR + default_value = 0 + reference = "https://jobs.utah.gov/ui/jobseeker/claimantguide.pdf#page=13" + # Hours worked during a claim week. A claimant who works at least the + # full-time hours threshold (40 hours) in a week is not eligible for + # benefits that week per Utah DWS Claimant Guide page 11 (PDF file p. 13) + # and the "total unemployment" definition in Utah Code § 35A-4-207. + # Defaults to 0, so in microsimulation (where this input is not populated) + # claimants are treated as working no hours while claiming. diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_payable_amount.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_payable_amount.py new file mode 100644 index 00000000000..fe15a936b61 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weekly_payable_amount.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class ut_ui_weekly_payable_amount(Variable): + value_type = float + entity = Person + label = "Utah UI weekly payable amount" + unit = USD + definition_period = YEAR + reference = ( + "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html", + "https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-301", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.ut.dws.unemployment_insurance + weekly_benefit_amount = person("ut_ui_weekly_benefit_amount", period) + gross_weekly_earnings = person("ut_ui_gross_weekly_earnings", period) + weekly_hours_worked = person("ut_ui_weekly_hours_worked", period) + + # § 35A-4-401(3)(a): the first 30% of WBA in weekly earnings is + # disregarded; earnings above that reduce the WBA dollar-for-dollar. + earnings_disregard = ( + p.benefit.partial.earnings_disregard_rate * weekly_benefit_amount + ) + excess_earnings = max_(gross_weekly_earnings - earnings_disregard, 0) + raw_payable = max_(np.floor(weekly_benefit_amount - excess_earnings), 0) + + # § 35A-4-401(3)(a): earnings at or above the WBA disqualify the + # week. Claimant Guide page 11 (PDF file p. 13) / § 35A-4-207: + # working at least the full-time hours threshold (40 hours) in a + # week also disqualifies. + disqualified = (gross_weekly_earnings >= weekly_benefit_amount) | ( + weekly_hours_worked >= p.benefit.partial.hours_threshold + ) + return where(disqualified, 0, raw_payable) diff --git a/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weeks_unemployed.py b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weeks_unemployed.py new file mode 100644 index 00000000000..b14a5af5b9c --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dws/unemployment_insurance/ut_ui_weeks_unemployed.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class ut_ui_weeks_unemployed(Variable): + value_type = int + entity = Person + label = "Utah UI weeks unemployed" + unit = "week" + definition_period = YEAR + default_value = 0 + reference = "https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S401.html" + # Number of weeks during the year the claimant collected (or would + # collect) Utah Unemployment Insurance benefits. Used to convert the + # weekly payable amount into an annual benefit, capped by the maximum + # benefit amount per Utah Code § 35A-4-401(4)(a). + # Defaults to 0, so in microsimulation (where this input is not populated) + # claimants are treated as unemployed for no weeks, yielding a $0 benefit.