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/lessons/agent-lessons.md b/lessons/agent-lessons.md new file mode 100644 index 00000000000..f80ca9cd847 --- /dev/null +++ b/lessons/agent-lessons.md @@ -0,0 +1,24 @@ +# Agent Lessons Learned + +Accumulated from /encode-policy-v2 and /backdate-program runs across all contributors. +Loaded by implementation agents on future runs. + +## New Lessons from Utah UI (2026-05-13) + +### PARAMETER +- For annually-recomputed parameters, never use a single dated entry to cover multiple years — create one dated entry per distinct value year so future audits and tests can isolate per-year regressions. +- When two parameters derive from the same underlying statutory quantity (e.g., max WBA and min base-period wages both keyed to the IAFY wage), cross-check them mathematically after entry; an inconsistent ratio reveals a transcription error in one of them. +- When a parameter value is sourced from a year-labeled form, embed the form number AND the effective year in the reference title so rolling-URL drift is detectable on audit. + +### REFERENCE +- When citing a document via a URL that the publisher overwrites annually (current-year landing page), pair the live URL with a Wayback Machine snapshot capturing the historical version, and label the live URL as "current year — rolls over annually." +- When the PDF's printed page number differs from the file page number (front-matter offset), annotate both in any human-readable comment (e.g., "page 11 (PDF file p. 13)") and use the file page number in the `#page=` anchor. +- Never let a code comment cite one page number while the adjacent URL anchor uses a different one — readers cannot tell which is correct without rendering the PDF. + +### TEST +- For parameters with multiple dated entries, place at least one boundary test inside each distinct year so a value regression in any single year fails its own dedicated test. +- Include a cross-year regression-guard test that uses an old year's threshold against the new year's expected behavior — this catches accidental parameter collapse (single-entry-applied-to-all-years bugs). + +### WORKFLOW +- Document-collector agents must fall back to the Read tool on PDF files when sandboxed binaries like `pdftotext`/`pdftoppm` are unavailable, rather than reporting the PDF as unreadable. +- When a parameter value changes during review, grep test files for old-value-anchored case names, header comments, and inline comments — not just numeric inputs — and rewrite the prose so documentation does not drift from the new value. diff --git a/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/duration/max_weeks.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/duration/max_weeks.yaml new file mode 100644 index 00000000000..36c38194051 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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/dwf/unemployment_insurance/benefit/duration/min_weeks.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/duration/min_weeks.yaml new file mode 100644 index 00000000000..6df01045be6 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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/dwf/unemployment_insurance/benefit/duration/multiplier.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/duration/multiplier.yaml new file mode 100644 index 00000000000..aebc8bb45eb --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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/dwf/unemployment_insurance/benefit/partial/earnings_disregard_rate.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/partial/earnings_disregard_rate.yaml new file mode 100644 index 00000000000..b0445256b2f --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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/dwf/unemployment_insurance/benefit/partial/hours_threshold.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/partial/hours_threshold.yaml new file mode 100644 index 00000000000..bf2ecec68c8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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 Code § 35A-4-207 + href: https://le.utah.gov/xcode/Title35A/Chapter4/35A-4-S207.html + - title: Utah DWS Claimant Guide (Jan 2025) + href: https://jobs.utah.gov/ui/jobseeker/claimantguide.pdf#page=13 diff --git a/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/wba/divisor.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/wba/divisor.yaml new file mode 100644 index 00000000000..e5b8d64bb94 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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/dwf/unemployment_insurance/benefit/wba/max_amount.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/wba/max_amount.yaml new file mode 100644 index 00000000000..520397469b9 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/wba/max_amount.yaml @@ -0,0 +1,16 @@ +description: Utah limits the weekly benefit amount to this amount under the Unemployment Insurance program. + +values: + 2024-01-01: 712 + 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) + href: https://jobs.utah.gov/ui/UIShared/PDFs/BenefitCalculation.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/wba/subtraction.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/benefit/wba/subtraction.yaml new file mode 100644 index 00000000000..2a318ca8fce --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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/dwf/unemployment_insurance/eligibility/base_period_to_high_quarter_ratio.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/eligibility/base_period_to_high_quarter_ratio.yaml new file mode 100644 index 00000000000..2d140d16fd5 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/eligibility/base_period_to_high_quarter_ratio.yaml @@ -0,0 +1,14 @@ +description: Utah uses this multiple of high-quarter wages as the minimum total base-period wages under the Unemployment Insurance program. + +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-202 + href: https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-202 diff --git a/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/eligibility/min_base_period_wages.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/eligibility/min_base_period_wages.yaml new file mode 100644 index 00000000000..2d64e2aefc1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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. The 2024 value is derived +# from the 2024 max WBA ($712) per UC § 35A-4-201(16) (8% of insured average +# fiscal year wage, rounded up to next $100) and § 35A-4-401(2)(b)(ii). DWS +# no longer publishes the 2024 schedule PDF, but the value is recoverable +# from the 2024 max WBA already encoded in `wba/max_amount.yaml`. +values: + 2024-01-01: 4_800 + 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-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/dwf/unemployment_insurance/eligibility/min_quarters_with_wages.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/eligibility/min_quarters_with_wages.yaml new file mode 100644 index 00000000000..5af6f368499 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/eligibility/min_quarters_with_wages.yaml @@ -0,0 +1,14 @@ +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: int + period: year + label: Utah UI minimum quarters with wages + reference: + - 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-202 + href: https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-202 diff --git a/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/index.yaml b/policyengine_us/parameters/gov/states/ut/dwf/unemployment_insurance/index.yaml new file mode 100644 index 00000000000..cb39cde8a79 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ut/dwf/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 32409746ae5..d51f662c7e9 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -900,6 +900,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.dwf.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/dwf/unemployment_insurance/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/integration.yaml new file mode 100644 index 00000000000..a09f72affb4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/integration.yaml @@ -0,0 +1,232 @@ +# 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 = $712, so WBA = $712. + ut_ui_weekly_benefit_amount: 712 + # Raw duration = floor(floor(0.27 * $80,000) / $712) + # = floor($21,600 / $712) = floor(30.34) = 30, clamp 26. + ut_ui_duration_weeks: 26 + # MBA = $712 * 26 = $18,512. + ut_ui_maximum_benefit_amount: 18_512 + # Weekly payable = WBA when no earnings/hours. + ut_ui_weekly_payable_amount: 712 + # Annual = $712 * 26 = $18,512; MBA cap = $18,512. + ut_ui: 18_512 + unemployment_compensation: 18_512 + +- 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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_duration_weeks.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/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/dwf/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/dwf/unemployment_insurance/ut_ui_edge_cases.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_edge_cases.yaml new file mode 100644 index 00000000000..4276e8a64d5 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_edge_cases.yaml @@ -0,0 +1,789 @@ +# 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-202 (quarters test) +# https://www.law.cornell.edu/regulations/utah/Utah-Admin-Code-R994-401-202 +# - 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 +# ($4,800 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: 5_500 + ut_ui_high_quarter_wages: 100 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $5,500 >= $5,300 min; 4 quarters >= 2; + # $5,500 >= 1.5*$100 = $150. + ut_ui_monetarily_eligible: true + # Raw WBA = floor(100/26) - 5 = 3 - 5 = -2 -> max_(.,0) = 0. + # Tests negative-protection: WBA must never be negative. + 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: 18_642 + ut_ui_quarters_with_wages: 4 + households: + household: + members: [person1] + state_code: UT + output: + people: + person1: + # Monetarily eligible: $50,000 >= 1.5 * $18,642 = $27,963. + # Raw WBA = floor(18,642/26) - 5 = floor(717.0) - 5 = 717 - 5 = $712. + # 2024 cap = $712 -> WBA exactly hits the cap with no clipping. + ut_ui_weekly_benefit_amount: 712 + +- name: Case 11, high quarter just above 2024 cap threshold is capped to 712. + 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 = $712 -> capped down to $712. + ut_ui_weekly_benefit_amount: 712 + +- 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: 5_500 + 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). 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: $4,800; 2026 floor: $5,500) +# --------------------------------------------------------------------------- + +- name: Case 28, 2024 base wages one dollar below $4,800 floor fail minimum test. + period: 2024 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 4_799 + 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: $4,799 < $4,800 (2024 minimum) -> fails. + # (Tests 2 and 3 would pass: 4 quarters >= 2; 4,799 >= 1.5*3,000=4,500.) + ut_ui_monetarily_eligible: false + +- name: Case 29, 2024 base wages exactly at $4,800 floor pass the minimum test. + period: 2024 + input: + people: + person1: + age: 35 + ut_ui_base_period_wages: 4_800 + 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: $4,800 >= $4,800 (2024 minimum). + # Test 2: 4 quarters >= 2. + # Test 3: $4,800 >= 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/dwf/unemployment_insurance/ut_ui_maximum_benefit_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/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/dwf/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/dwf/unemployment_insurance/ut_ui_monetarily_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_monetarily_eligible.yaml new file mode 100644 index 00000000000..f8c774c8d25 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_monetarily_eligible.yaml @@ -0,0 +1,85 @@ +# 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,300 in 2024-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-202). +# 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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_benefit_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_benefit_amount.yaml new file mode 100644 index 00000000000..2890890b9b7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_benefit_amount.yaml @@ -0,0 +1,92 @@ +# 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: $712; 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 $712. + 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 = $712, so WBA = $712. + ut_ui_weekly_benefit_amount: 712 + +- 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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_payable_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_payable_amount.yaml new file mode 100644 index 00000000000..56118a04c4a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_payable_amount.yaml @@ -0,0 +1,130 @@ +# 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 diff --git a/policyengine_us/variables/gov/states/unemployment_compensation.py b/policyengine_us/variables/gov/states/unemployment_compensation.py index 0857864ab17..f350d38737d 100644 --- a/policyengine_us/variables/gov/states/unemployment_compensation.py +++ b/policyengine_us/variables/gov/states/unemployment_compensation.py @@ -9,3 +9,4 @@ class unemployment_compensation(Variable): documentation = "Income from unemployment compensation programs." definition_period = YEAR uprating = "calibration.gov.irs.soi.unemployment_compensation" + adds = ["ut_ui"] diff --git a/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui.py new file mode 100644 index 00000000000..a2442042b31 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui.py @@ -0,0 +1,28 @@ +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. + + 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/dwf/unemployment_insurance/ut_ui_base_period_wages.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_base_period_wages.py new file mode 100644 index 00000000000..c3d46b3ca87 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_base_period_wages.py @@ -0,0 +1,18 @@ +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. diff --git a/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_duration_weeks.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_duration_weeks.py new file mode 100644 index 00000000000..ff317de6e1b --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/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.dwf.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/dwf/unemployment_insurance/ut_ui_gross_weekly_earnings.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_gross_weekly_earnings.py new file mode 100644 index 00000000000..35707de9fa7 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_gross_weekly_earnings.py @@ -0,0 +1,17 @@ +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). diff --git a/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_high_quarter_wages.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_high_quarter_wages.py new file mode 100644 index 00000000000..a7795a12a14 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_high_quarter_wages.py @@ -0,0 +1,14 @@ +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). diff --git a/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_maximum_benefit_amount.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_maximum_benefit_amount.py new file mode 100644 index 00000000000..2e22f2edce8 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/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/dwf/unemployment_insurance/ut_ui_monetarily_eligible.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_monetarily_eligible.py new file mode 100644 index 00000000000..348f85967ef --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_monetarily_eligible.py @@ -0,0 +1,32 @@ +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-202", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.ut.dwf.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-202: 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/dwf/unemployment_insurance/ut_ui_quarters_with_wages.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_quarters_with_wages.py new file mode 100644 index 00000000000..8060d2f008a --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_quarters_with_wages.py @@ -0,0 +1,15 @@ +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-202" + ) + # Number of base-period calendar quarters (0-4) in which the claimant + # earned wages. Utah Admin Code R994-401-202 requires wages in at least + # two base-period quarters. diff --git a/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_benefit_amount.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_benefit_amount.py new file mode 100644 index 00000000000..f3c27c7e30e --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/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.dwf.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/dwf/unemployment_insurance/ut_ui_weekly_hours_worked.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_hours_worked.py new file mode 100644 index 00000000000..6ae932a4d41 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_hours_worked.py @@ -0,0 +1,15 @@ +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. diff --git a/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_payable_amount.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weekly_payable_amount.py new file mode 100644 index 00000000000..593ea34d818 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/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.dwf.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/dwf/unemployment_insurance/ut_ui_weeks_unemployed.py b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weeks_unemployed.py new file mode 100644 index 00000000000..eb463115674 --- /dev/null +++ b/policyengine_us/variables/gov/states/ut/dwf/unemployment_insurance/ut_ui_weeks_unemployed.py @@ -0,0 +1,15 @@ +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).