diff --git a/changelog.d/co-oap-payment-floor.fixed.md b/changelog.d/co-oap-payment-floor.fixed.md new file mode 100644 index 00000000000..5a8c207ae28 --- /dev/null +++ b/changelog.d/co-oap-payment-floor.fixed.md @@ -0,0 +1 @@ +Apply Colorado's $189 Old Age Pension payment floor from January 2026, and count OAP income under 9 CCR 2503-5 3.533 and 3.534 for every client, so clients aged 60 to 64 who are not SSI-eligible no longer receive the full grant regardless of income. diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml index 22552b1c0da..4274a4a6ced 100644 --- a/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml @@ -16,6 +16,6 @@ metadata: - title: ADULT FINANCIAL PROGRAMS | Income Maintenance (Volume 3) | 3.530(A) href: https://sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11264&fileName=9%20CCR%202503-5#page=57 - title: ADULT FINANCIAL PROGRAMS | Income Maintenance (Volume 3) | 3.530(A) - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=80 + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=74 - title: Colorado Department of Human Services | OPERATION MEMO href: https://drive.google.com/file/d/1bc_hUAoSzUbyRuMCzcxk2zV7b4RqkwyC/view diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml new file mode 100644 index 00000000000..5140f1acbf8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml @@ -0,0 +1,14 @@ +description: Colorado deducts this share of the SSI benefit standard for an individual, less the child's own income, for each dependent child in the household when deeming a spouse's unearned income to an Old Age Pension client. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 0.5 + +metadata: + unit: /1 + label: Colorado OAP deeming allocation per dependent child as a share of the SSI benefit standard + reference: + - title: 9 CCR 2503-5, Section 3.534(B)(3) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78 + - title: 9 CCR 2503-5, Section 3.534(B)(3) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=48 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml new file mode 100644 index 00000000000..a46db14dcc2 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml @@ -0,0 +1,15 @@ +description: Colorado deducts this amount from monthly gross earned income when counting income for the Old Age Pension. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 65 + +metadata: + unit: currency-USD + period: month + label: Colorado OAP earned income disregard + reference: + - title: "9 CCR 2503-5, Sections 3.533(A)(1)(a), 3.534(A)(1) #page 76-77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76 + - title: 9 CCR 2503-5, Sections 3.533(A)(1)(a), 3.534(A)(1) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml new file mode 100644 index 00000000000..b7874a274fd --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml @@ -0,0 +1,14 @@ +description: Colorado counts this share of monthly earned income, after the earned income disregard, for the Old Age Pension. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 0.5 + +metadata: + unit: /1 + label: Colorado OAP countable share of earned income + reference: + - title: "9 CCR 2503-5, Sections 3.533(A)(1)(b), 3.534(A)(2) #page 76-77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76 + - title: 9 CCR 2503-5, Sections 3.533(A)(1)(b), 3.534(A)(2) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml new file mode 100644 index 00000000000..9402fbf5286 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml @@ -0,0 +1,15 @@ +description: Colorado deducts this amount from monthly unearned income other than SSI when counting income for the Old Age Pension, splitting it equally between spouses. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 20 + +metadata: + unit: currency-USD + period: month + label: Colorado OAP unearned income disregard + reference: + - title: 9 CCR 2503-5, Section 3.533(A)(2)(c), (f) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=77 + - title: 9 CCR 2503-5, Section 3.533(A)(2)(c) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml new file mode 100644 index 00000000000..c5a39e917fd --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml @@ -0,0 +1,30 @@ +description: Colorado counts these income sources as gross unearned income for the Old Age Pension, before disregards. + +values: + # 3.520.785(B) lists countable unearned income. 3.520.786 exempts the + # unearned income SSA excludes (K), which includes interest and dividends + # on countable resources (20 CFR 416.1124(c)(22)), and child support + # received for another person (N) or arrears received for the client (P). + 2014-03-02: + - pension_income + - social_security + - disability_benefits + - veterans_benefits + - survivor_benefits + - workers_compensation + - unemployment_compensation + - retirement_distributions + - gi_cash_assistance + - alimony_income + - financial_assistance + - rental_income + +metadata: + unit: list + period: year + label: Colorado OAP unearned income sources + reference: + - title: "9 CCR 2503-5, Sections 3.520.785, 3.520.786 #page 68-72" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68 + - title: 20 CFR 416.1124(c)(22), interest and dividends on countable resources + href: https://www.ecfr.gov/current/title-20/chapter-III/part-416/subpart-K/subject-group-ECFR57d93e39d624237/section-416.1124#p-416.1124(c)(22) diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml new file mode 100644 index 00000000000..7dc0e4ccd0f --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml @@ -0,0 +1,21 @@ +description: Colorado raises an Old Age Pension grant below this amount to it with a state supplementary payment. + +values: + # Before 2026 Colorado met the SSI pass-along requirement by maintaining + # total expenditures, with irregular lump-sum payments and no payment floor. + 2002-01-01: 0 + 2026-01-01: 189 + +metadata: + unit: currency-USD + period: month + label: Colorado OAP payment floor standard + reference: + - title: "9 CCR 2503-5, Sections 3.530(C), 3.531(D)(2), 3.532(B), 3.533(B), OAP payment floor standard #page 74-77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=74 + - title: 9 CCR 2503-5, Section 3.530(C) (emergency rule effective January 1, 2026) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12318#page=58 + - title: 9 CCR 2503-5, Section 3.531(D) (effective March 2, 2025), MOE payments without a payment floor + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=11861#page=58 + - title: "Colorado State Board of Human Services, Adult Financial Payment Floor and COLA 2026, rulemaking packet 2025-00596 #page 1,4,8" + href: https://www.sos.state.co.us/CCR/Upload/AGORequestEmergency/RegulatoryAnalysisAttachment2025-00596.pdf#page=1 diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py new file mode 100644 index 00000000000..acd93f2e6cf --- /dev/null +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -0,0 +1,289 @@ +"""Property tests: Colorado Old Age Pension countable income, deeming and floor. + +9 CCR 2503-5 3.533(A) counts an OAP client's own income; 3.534(A)-(B) deem +part of a spouse's income to the client, except under 3.534(C) when the +spouse receives Adult Financial grant payments, SSI or Medicaid with income +within the OAP limit; 3.533 and 3.520.781(A) make a client whose total +countable income (SSI included) equals or exceeds the grant standard +ineligible; and 3.530(C), 3.532(B), 3.533(B) raise a grant below the $189 +payment floor standard to it. + +Each case is a client aged 60-80 in their own household, with pension, +interest, Social Security, earnings, rental income or loss and child support, +optionally receiving outside shelter support, and either single or married to +a spouse who has earnings and a pension and may receive shelter support. Each married case appears four times: (A) the spouse is enrolled in +Medicaid, (B) the spouse is not, (C) as B with extra spousal earnings, and +(D) as B with the tax-unit roles swapped. For 2026: + +1. Payment: OAP is the grant standard less total countable income when that + is positive, raised to the floor if below it, and zero otherwise, with + cents dropped unless SSI reduces the grant (3.581(E)); so it is zero or + between the floor and the standard each month. +2. Singles: for a client not on SSI, with no earnings or at least $20 of + unearned income, 3.533(A) counts the same income as SSI's exclusions + (20 CFR 416.1112, 416.1124) applied to the same gross income, with + interest and child support exempt (3.520.786(K), (N)) and a rental loss + counted as zero (3.520.785(B)(20)). For a client on SSI with none of + those, 3.533(A) never counts less than SSI does. (PolicyEngine's SSI + counts interest, which 20 CFR 416.1124(c)(22) excludes on countable + resources: #10079.) +3. Exemption: a spouse's Medicaid enrollment can only lower the client's + countable income and raise OAP. +4. Monotonicity: while both spouses' SSI is unchanged, more spousal + earnings never lower the client's countable income or raise OAP. When + they change SSI, either can move the other way, as intended: OAP counts + the client's SSI (an SSI couple aged 65 with no income: $85/month of + spousal earnings cut each spouse's SSI by $0.02, and OAP rises by as + much); SSI falling under $20 releases the 3.533(A)(2)(f) spillover; a + client taken off SSI loses SSA's matching ISM, because Colorado's own ISM + for clients not on SSI (3.520.79(C)) is not modeled; and a spouse on SSI + above the OAP limit has SSI deemed (3.534(B)(1)), which their earnings + below the $65 disregard can lower. +5. Role invariance: the client's countable income and OAP do not depend on + which spouse is the tax-unit head. + +YAML states the worked cases; Python is needed to compare arrangements of +the same household in one simulation and to check against SSI's exclusions. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ssi_paid +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + +YEAR = 2026 +TOLERANCE = 0.01 +ARRANGEMENTS = "ABCD" + + +def build_situation(cases): + people, groups = ( + {}, + { + name: {} + for name in ( + "tax_units", + "marital_units", + "spm_units", + "families", + "households", + ) + }, + ) + for i, c in enumerate(cases): + for arrangement in ARRANGEMENTS if c["married"] else "A": + key = f"{arrangement}{i}" + client, spouse = f"client_{key}", f"spouse_{key}" + people[client] = { + "age": {YEAR: c["client_age"]}, + "pension_income": {YEAR: c["client_pension"]}, + "interest_income": {YEAR: c["client_interest"]}, + "social_security_retirement": {YEAR: c["client_social_security"]}, + "employment_income": {YEAR: c["client_earnings"]}, + "rental_income": {YEAR: c["client_rental"]}, + "child_support_received": {YEAR: c["client_child_support"]}, + "ssi_receives_outside_shelter_support": {YEAR: c["shelter_support"]}, + # Set for everyone: an input for some people gives the others + # its default (false), not its formula. + "co_oap_medicaid_enrolled": {YEAR: True}, + } + members = [client] + if c["married"]: + extra = c["extra_spouse_earnings"] if arrangement == "C" else 0 + people[spouse] = { + "age": {YEAR: c["spouse_age"]}, + "employment_income": {YEAR: c["spouse_earnings"] + extra}, + "pension_income": {YEAR: c["spouse_pension"]}, + "ssi_receives_outside_shelter_support": { + YEAR: c["spouse_shelter_support"] + }, + "co_oap_medicaid_enrolled": {YEAR: arrangement == "A"}, + } + members = [spouse, client] if arrangement == "D" else [client, spouse] + head, partner = members + people[head]["is_tax_unit_head"] = {YEAR: True} + people[head]["is_tax_unit_spouse"] = {YEAR: False} + people[partner]["is_tax_unit_head"] = {YEAR: False} + people[partner]["is_tax_unit_spouse"] = {YEAR: True} + for name, group in groups.items(): + group[f"{name}_{key}"] = {"members": members} + groups["households"][f"households_{key}"]["state_code"] = {YEAR: "CO"} + return {"people": people, **groups} + + +def calculate(cases): + """Arrays indexed by person name for every person in every arrangement.""" + simulation = Simulation(situation=build_situation(cases)) + names = list(simulation.populations["person"].ids) + results = {} + for variable in ( + "co_oap", + "co_oap_countable_income", + "co_oap_own_countable_income", + "co_oap_eligible", + "ssi", + "ssi_countable_income", + "ssi_earned_income", + "ssi_unearned_income", + ): + values = np.asarray(simulation.calculate(variable, YEAR)) + results[variable] = dict(zip(names, values)) + p = simulation.tax_benefit_system.parameters(f"{YEAR}-01").gov.states.co.ssa.oap + return results, simulation.tax_benefit_system.parameters, p + + +def assert_properties(cases, calculated=None): + results, parameters, p = calculated or calculate(cases) + standard, floor = p.grant_standard, p.payment_floor + oap = results["co_oap"] + countable = results["co_oap_countable_income"] + eligible = results["co_oap_eligible"] + ssi = results["ssi"] + for i, c in enumerate(cases): + for arrangement in ARRANGEMENTS if c["married"] else "A": + client = f"client_{arrangement}{i}" + # Monthly amounts: the inputs and the program are constant + # within the year. + # SSI as SSA pays it, as the model counts it. + total = float(ssi_paid(ssi[client] / 12)) + countable[client] / 12 + paid = oap[client] / 12 + # 1. Payment rule and bounds. Payments not reduced by SSI drop + # cents (3.581(E)). Within half a cent of the standard, the + # model's quarter-cent precision decides; YAML pins those. + grant = standard - total + if not eligible[client]: + assert paid == 0, c + elif grant > 0.005: + expected = max(grant, floor) + if ssi[client] > 0: + assert paid == pytest.approx(expected, abs=0.05), c + else: + assert abs(paid - round(paid)) < 0.01, c + assert expected - 1 < paid <= expected + 0.01, c + assert floor - TOLERANCE <= paid <= standard + TOLERANCE, c + elif grant < -0.005: + assert paid == 0, c + client = f"client_A{i}" + if not c["married"]: + # 2. Singles, against SSI's exclusions on the same gross income: + # interest and child support exempt (3.520.786(K), (N)), a rental + # loss counted as zero (3.520.785(B)(20)). + own = results["co_oap_own_countable_income"][client] + plain = ( + c["client_interest"] == 0 + and c["client_child_support"] == 0 + and c["client_rental"] >= 0 + ) + # OAP takes each monthly source in cents; SSI does not, so the + # comparisons allow $0.30/year of rounding. + if ssi_paid(ssi[client] / 12) > 0: + if plain: + assert own >= results["ssi_countable_income"][client] - 0.3, c + elif not c["shelter_support"]: + unearned = ( + c["client_pension"] + + c["client_social_security"] + + max(c["client_rental"], 0) + ) + if c["client_earnings"] == 0 or unearned >= 20 * 12: + expected = _apply_ssi_exclusions( + np.array([results["ssi_earned_income"][client]]), + np.array([float(unearned)]), + parameters, + YEAR, + )[0] + assert own == pytest.approx(expected, abs=0.3), c + continue + a, b, c_, d = (f"client_{x}{i}" for x in ARRANGEMENTS) + # 3. The Medicaid exemption only lowers countable income. + assert countable[a] <= countable[b] + TOLERANCE, c + assert oap[a] >= oap[b] - TOLERANCE, c + # 4. With the client's SSI unchanged, more spousal earnings never + # lower countable income or raise OAP. + spouse_b, spouse_c = f"spouse_B{i}", f"spouse_C{i}" + if ssi[c_] == pytest.approx(ssi[b], abs=TOLERANCE) and ssi[ + spouse_c + ] == pytest.approx(ssi[spouse_b], abs=TOLERANCE): + assert countable[c_] >= countable[b] - TOLERANCE, c + assert oap[c_] <= oap[b] + TOLERANCE, c + # 5. Roles. + assert countable[d] == pytest.approx(countable[b], abs=TOLERANCE), c + assert oap[d] == pytest.approx(oap[b], abs=TOLERANCE), c + + +def test_worked_example(): + """co_oap YAML Case 22, adding the spouse on Medicaid. The + client, 62, has no income; the spouse, 50, earns $2,000/month. Without + Medicaid, 3.534(A) deems (2,000 - 65) / 2 = 967.50 and the grant of + 1,032 - 967.50 = 64.50 is raised to the $189 floor. With the spouse on + Medicaid, the spouse's own countable income, (2,000 - 65 - 10) / 2 = + 962.50, is within the OAP limit, so nothing is deemed and the client gets + the full 1,032/month.""" + case = { + "client_age": 62, + "client_pension": 0, + "client_interest": 0, + "client_social_security": 0, + "client_earnings": 0, + "client_rental": 0, + "client_child_support": 0, + "shelter_support": False, + "married": True, + "spouse_age": 50, + "spouse_earnings": 24_000, + "spouse_pension": 0, + "spouse_shelter_support": False, + "extra_spouse_earnings": 1_200, + } + calculated = calculate([case]) + results = calculated[0] + oap = results["co_oap"] + countable = results["co_oap_countable_income"] + assert oap["client_A0"] == pytest.approx(12_384, abs=TOLERANCE) + assert countable["client_A0"] == pytest.approx(0, abs=TOLERANCE) + assert oap["client_B0"] == pytest.approx(2_268, abs=TOLERANCE) + assert countable["client_B0"] == pytest.approx(11_610, abs=TOLERANCE) + assert_properties([case], calculated) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + @st.composite + def case(draw): + return { + "client_age": draw(st.integers(min_value=60, max_value=80)), + "client_pension": draw(amount(12_000)), + "client_interest": draw(amount(300)), + "client_social_security": draw(amount(15_000)), + "client_earnings": draw(amount(20_000)), + "client_rental": draw(st.integers(min_value=-6_000, max_value=6_000)), + "client_child_support": draw(amount(6_000)), + "shelter_support": draw(st.booleans()), + "married": draw(st.booleans()), + "spouse_age": draw(st.integers(min_value=18, max_value=80)), + "spouse_earnings": draw(amount(30_000)), + "spouse_pension": draw(amount(24_000)), + "spouse_shelter_support": draw(st.booleans()), + "extra_spouse_earnings": draw(st.integers(min_value=1, max_value=12_000)), + } + + @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.given(st.lists(case(), min_size=1, max_size=8)) + def test_properties(cases): + assert_properties(cases) diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index a64c80a17a7..dffaf2429b0 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -57,6 +57,150 @@ def test_ssp_snap_medicaid_calculation_order(year, state, benefit): assert results[0]["medicaid_enrolled"].tolist() == [True, state == "IN"] +def co_oap_couple(year, snap=None): + """An Old Age Pension client whose spouse is a SNAP workfare participant.""" + people = { + "client": {"age": {year: 62}}, + "spouse": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + }, + } + unit = {"unit": {"members": list(people)}} + spm_units = {"unit": {"members": list(people)}} + if snap is not None: + spm_units["unit"]["snap"] = {year: snap} + return { + "people": people, + **{entity: unit for entity in ("tax_units", "marital_units", "families")}, + "spm_units": spm_units, + "households": {"unit": {"members": list(people), "state_code": {year: "CO"}}}, + } + + +@pytest.mark.parametrize("year", [2027, 2030, 2035]) +@pytest.mark.parametrize("snap", [None, 12]) +def test_co_oap_snap_medicaid_calculation_order(year, snap): + """Colorado OAP reads the spouse's Medicaid enrollment to decide whether the + spouse's income is deemed (9 CCR 2503-5 3.534(C)), and SNAP counts OAP as + income. The spouse is enrolled through SNAP's pass-through from the + community engagement requirement, with SNAP computed or supplied, and + earns $5,000/year, well within the OAP limit, so nothing is deemed and the + client gets the full 1,032/month in either order. YAML cannot express + repeated calculations in different orders.""" + variables = ("co_oap", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=co_oap_couple(year, snap)) + results.append({name: simulation.calculate(name, year) for name in order}) + assert not any("_medicaid_" in name for name in simulation.branches) + for name in variables: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["medicaid_enrolled"].tolist() == [True, True] + assert results[0]["co_oap"][0] == pytest.approx(12_384) + assert results[0]["snap"][0] > 0 + + +def test_co_oap_medicaid_without_a_consistent_outcome(): + """A household with no self-consistent outcome resolves the same way in + either order. The client's spouse, 40, earns $5,000/year and is a SNAP + workfare participant; an unrelated adult in the same SNAP unit has a + $31,250/year pension. With the spouse's income deemed, OAP is lower and + the unit gets SNAP, which enrolls the spouse in Medicaid through the + pass-through; with the spouse exempt, OAP is higher and the unit gets no + SNAP. co_oap_medicaid_enrolled decides enrollment on the SNAP received + while the spouse's income is still deemed, so the spouse is exempt and + the client gets the full 1,032/month.""" + year = 2027 + people = { + "client": {"age": {year: 62}}, + "spouse": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + }, + "adult": { + "age": {year: 40}, + "pension_income": {year: 31_250}, + "is_snap_workfare_participant": {year: True}, + }, + } + couple = {"members": ["client", "spouse"]} + single = {"members": ["adult"]} + everyone = {"members": list(people)} + situation = { + "people": people, + **{ + entity: {"couple": couple, "single": single} + for entity in ("tax_units", "marital_units", "families") + }, + "spm_units": {"unit": everyone}, + "households": {"unit": {**everyone, "state_code": {year: "CO"}}}, + } + variables = ("co_oap", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=situation) + results.append({name: simulation.calculate(name, year) for name in order}) + for name in variables: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["co_oap"][0] == pytest.approx(12_384) + + +def test_co_oap_medicaid_branch_keeps_reported_coverage(): + """Reported Medicaid coverage holds inside the branch. Two OAP couples and + an unrelated adult with a $14,000/year pension share one SNAP unit. Spouse + A reports Medicaid coverage, so A's client gets the full 1,032/month. + Spouse B, 40, earns $5,000/year as a SNAP workfare participant. With A's + income exempt the unit gets no SNAP, so B has no route to Medicaid and + B's earnings are deemed: (5,000 / 12 - 65) / 2 = 175.83/month, a grant + of 856.17 paid as 856 (3.581(E)), or 10,272/year.""" + year = 2027 + people = { + "client_a": {"age": {year: 62}}, + "spouse_a": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + "receives_medicaid": {year: True}, + "takes_up_medicaid_if_eligible": {year: False}, + }, + "client_b": {"age": {year: 62}}, + "spouse_b": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + }, + "adult": { + "age": {year: 40}, + "pension_income": {year: 14_000}, + "is_snap_workfare_participant": {year: True}, + }, + } + units = { + "a": {"members": ["client_a", "spouse_a"]}, + "b": {"members": ["client_b", "spouse_b"]}, + "adult": {"members": ["adult"]}, + } + everyone = {"members": list(people)} + situation = { + "people": people, + **{entity: units for entity in ("tax_units", "marital_units", "families")}, + "spm_units": {"unit": everyone}, + "households": {"unit": {**everyone, "state_code": {year: "CO"}}}, + } + variables = ("co_oap", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=situation) + results.append({name: simulation.calculate(name, year) for name in order}) + for name in variables: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["co_oap"][0] == pytest.approx(12_384) + assert results[0]["co_oap"][2] == pytest.approx(10_272) + + @pytest.mark.parametrize( "variable", ["medicaid_enrolled", "is_medicaid_eligible", "takes_up_medicaid_if_eligible"], diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index b5fa2f9a340..5afc06b3ef2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -1,35 +1,554 @@ -- name: Countable Income +- name: Case 1, countable income other than SSI reduces the grant. + # 9 CCR 2503-5 3.533(B): 952 - 100 = 852/month, or 10,224/year. period: 2023 input: co_oap_eligible: true - ssi_countable_income: 1_000 + co_oap_countable_income: 1_200 ssi: 0 output: - co_oap: 10_424 + co_oap: 10_224 -- name: Has some SSI +- name: Case 2, SSI reduces the grant. + # 3.533(A)(2)(d) adds SSI back to countable income: 952 - 200 = 752/month, + # or 9,024/year. period: 2023 input: co_oap_eligible: true - ssi_countable_income: 0 - ssi: 2_000 + co_oap_countable_income: 0 + ssi: 2_400 output: - co_oap: 9_424 + co_oap: 9_024 -- name: More total income than grant standard +- name: Case 3, total countable income above the grant standard. period: 2023 input: co_oap_eligible: true - ssi_countable_income: 6_000 + co_oap_countable_income: 6_000 ssi: 6_000 output: co_oap: 0 -- name: 2026 grant standard +- name: Case 4, 2026 grant standard. period: 2026 input: co_oap_eligible: true - ssi_countable_income: 0 + co_oap_countable_income: 0 ssi: 0 output: co_oap: 12_384 + +- name: Case 5, the 2026 payment floor raises a grant below $189. + # SSI of $994/month: 1,032 - 994 = 38, raised to the $189 payment floor + # standard (3.530(C)) by a state supplementary payment (3.532(B), 3.533(B)). + # 189 x 12 = 2,268. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 0 + ssi: 11_928 + output: + co_oap: 2_268 + +- name: Case 6, a grant at the floor is unchanged. + # 1,032 - 843 = 189/month. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 10_116 + ssi: 0 + output: + co_oap: 2_268 + +- name: Case 7, a grant above the floor is unchanged. + # 1,032 - 800 = 232/month, or 2,784/year. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 9_600 + ssi: 0 + output: + co_oap: 2_784 + +- name: Case 8, a grant of a fraction of a dollar is raised to the floor. + # 1,032 - 1,031.50 = 0.50/month, raised to 189. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 12_378 + ssi: 0 + output: + co_oap: 2_268 + +- name: Case 9, income equal to the grant standard gets no floor. + # 3.533: a client whose total countable income equals or exceeds the grant + # standard is denied or discontinued (also 3.520.781(A)), so there is no + # grant to raise. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 6_000 + ssi: 6_384 + output: + co_oap: 0 + +- name: Case 10, income above the grant standard gets no floor. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 13_200 + ssi: 0 + output: + co_oap: 0 + +- name: Case 11, there was no payment floor in 2025. + # 1,005 - 1,000 = 5/month, or 60/year. Colorado met the pass-along + # requirement by total expenditures until the 2026 rule. + period: 2025 + input: + co_oap_eligible: true + co_oap_countable_income: 12_000 + ssi: 0 + output: + co_oap: 60 + +- name: Case 12, the floor applies month by month. + # January-June: 1,032 - 1,000 = 32, raised to 189. July-December: income of + # 1,100 exceeds the standard, so nothing is paid. 6 x 189 = 1,134. + period: 2026 + input: + co_oap_eligible: true + ssi: 0 + co_oap_countable_income: + 2026-01: 1_000 + 2026-02: 1_000 + 2026-03: 1_000 + 2026-04: 1_000 + 2026-05: 1_000 + 2026-06: 1_000 + 2026-07: 1_100 + 2026-08: 1_100 + 2026-09: 1_100 + 2026-10: 1_100 + 2026-11: 1_100 + 2026-12: 1_100 + output: + co_oap: 1_134 + +- name: Case 13, an ineligible person gets nothing. + period: 2026 + input: + co_oap_eligible: false + co_oap_countable_income: 0 + ssi: 0 + output: + co_oap: 0 + +- name: Case 14, a 62-year-old's pension reduces OAP. + # Not aged, blind or disabled, so ssi_countable_income is 0. 3.533(A)(2): + # 2,500/month of pension less the $20 disregard is 2,480, above the $1,032 + # standard. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 30_000 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 0 + co_oap_countable_income: 29_760 + co_oap: 0 + +- name: Case 15, a 62-year-old's earnings reduce OAP. + # 3.533(A)(1), (A)(2)(f): 5,000/month of earnings, no unearned income, so + # the whole $20 disregard reaches earnings: (5,000 - 65 - 20) / 2 = 2,457.50. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 60_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_countable_income: 29_490 + co_oap: 0 + +- name: Case 16, a 62-year-old with earnings just under the standard gets the floor. + # (2,080 - 65 - 20) / 2 = 997.50; 1,032 - 997.50 = 34.50, raised to 189. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 24_960 + households: + household: + members: [person] + state_code: CO + output: + co_oap_countable_income: 11_970 + co_oap: 2_268 + +- name: Case 17, early Social Security retirement benefits below the floor threshold. + # 900/month less the $20 disregard is 880; 1,032 - 880 = 152, raised to 189. + period: 2026 + input: + people: + person: + age: 62 + social_security_retirement: 10_800 + households: + household: + members: [person] + state_code: CO + output: + co_oap_countable_income: 10_560 + co_oap: 2_268 + +- name: Case 18, early Social Security retirement benefits above the floor threshold. + # 800 - 20 = 780; 1,032 - 780 = 252/month, or 3,024/year. + period: 2026 + input: + people: + person: + age: 62 + social_security_retirement: 9_600 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 3_024 + +- name: Case 19, the same benefits in 2025, before the floor. + # 1,005 - 880 = 125/month, or 1,500/year. + period: 2025 + input: + people: + person: + age: 62 + social_security_retirement: 10_800 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 1_500 + +- name: Case 20, an SSI recipient with no other income gets the floor. + # SSI pays 994/month; 1,032 - 994 = 38, raised to 189. + period: 2026 + input: + people: + person: + age: 70 + households: + household: + members: [person] + state_code: CO + output: + ssi: 11_928 + co_oap_countable_income: 0 + co_oap: 2_268 + +- name: Case 21, each member of an SSI couple is a client. + # SSI pays each spouse 745.50/month (half the $1,491 couple rate). Each is + # exempt from the other's deeming as an SSI recipient (3.534(C)): + # 1,032 - 745.50 = 286.50/month, or 3,438/year, above the floor. + period: 2026 + input: + people: + head: + age: 70 + is_tax_unit_head: true + spouse: + age: 68 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [8_946, 8_946] + co_oap_deemed_income: [0, 0] + co_oap: [3_438, 3_438] + +- name: Case 22, a working spouse's deemed earnings leave a grant below the floor. + # The client is 62 with no income. The spouse, 50, earns $2,000/month and + # does not take up Medicaid. 3.534(A): (2,000 - 65) / 2 = 967.50 deemed; + # 1,032 - 967.50 = 64.50, raised to 189. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [11_610, 0] + co_oap: [2_268, 0] + +- name: Case 23, a spouse's pension above the grant standard is deemed. + # Both are 62. The spouse's $1,500/month pension: own countable income + # 1,500 - 10 = 1,490 exceeds the OAP limit, so it is deemed (3.534(B)): + # 1,500 - 1,032 = 468. The client: 1,032 - 468 = 564/month, or 6,768/year. + # The client has no income and is an OAP client within the limit, so + # nothing is deemed to the spouse, whose own income makes them ineligible. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 62 + pension_income: 18_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [5_616, 0] + co_oap: [6_768, 0] + +- name: Case 24, income equal to the standard in cents gets no floor. + # $65.40/month of earnings and $1,051.80/month of pension: (65.40 - 65) / 2 + # = 0.20 plus 1,051.80 - 20 = 1,031.80 is exactly 1,032. 3.533: denied. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 784.80 + pension_income: 12_621.60 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 0 + +- name: Case 25, income one cent under the standard gets the floor. + # As Case 24 with $1,051.79/month of pension: total 1,031.99, a grant of + # 0.01 raised to 189. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 784.80 + pension_income: 12_621.48 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 2_268 + +- name: Case 26, a half-cent grant is a grant. + # $65.09/month of earnings and $1,051.95/month of pension: + # (65.09 - 65) / 2 + 1,051.95 - 20 = 1,031.995, below the standard, so the + # grant of 0.005 is raised to the floor. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 781.08 + pension_income: 12_623.40 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 2_268 + +- name: Case 27, payments drop cents when SSI does not reduce the grant. + # $516.50/month of pension: 516.50 - 20 = 496.50; 1,032 - 496.50 = 535.50, + # paid as 535 (3.581(E)), or 6,420/year. Case 21 keeps the cents of a grant + # reduced by SSI. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 6_198 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 5_958 + co_oap: 6_420 + +- name: Case 28, monthly income is recorded in cents. + # $12,623.99/year of rent averages to $1,051.9992/month (3.520.781(G)), a + # monthly income of $1,052.00 in cents. Less the $20 disregard that is + # 1,032, equal to the standard: denied (3.533). + period: 2026 + input: + people: + person: + age: 62 + rental_income: 12_623.99 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 0 + +- name: Case 29, a grant from income recorded in cents drops its cents. + # $6,192.01/year of rent is $516.00/month in cents: 516 - 20 = 496; + # 1,032 - 496 = 536/month, or 6,432/year. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 6_192.01 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 6_432 + +- name: Case 30, a monthly average just under a cent boundary is recorded in cents. + # $12,623.96/year of rent averages to $1,051.9967/month, $1,052.00 in + # cents. Less $20 that equals the standard: denied, as in Case 28. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 12_623.96 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 0 + +- name: Case 31, a monthly average just over a cent boundary is recorded in cents. + # $6,192.04/year of rent averages to $516.0033/month, $516.00 in cents: + # the grant is 536/month, or 6,432/year, as in Case 29. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 6_192.04 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 6_432 + +- name: Case 32, SSI counts as SSA pays it, a fractional cent raised. + # Both are 65. The client earns $1,291/month and the spouse $100.03/month. + # The model computes SSI of $418.9925/month each; SSA raises a fractional + # cent to the next cent (POMS SI 02001.010), paying $419.00. The client's + # only unearned income is SSI, so (1,291 - 65) / 2 = 613, and 613 + 419 = + # 1,032 equals the standard: denied. The spouse is on SSI within the limit + # ((100.03 - 65) / 2 + 419 = 436.515), so nothing is deemed either way; + # the spouse's grant, 1,032 - 436.515 = 595.485, is reduced by SSI, so it + # keeps its cents and is paid to the cent: 595.49, or 7,145.88/year. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 65 + employment_income: 15_492 + is_tax_unit_head: true + spouse: + age: 65 + employment_income: 1_200.36 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [5_027.91, 5_027.91] + co_oap: [0, 7_145.88] + +- name: Case 33, float residue just under the standard is no grant. + # 12,383.9988 / 12 = 1,031.9999, the residue of an income of 1,032. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 12_383.9988 + ssi: 0 + output: + co_oap: 0 + +- name: Case 34, a half-cent grant from countable income is a grant. + # 12,383.94 / 12 = 1,031.995 leaves a grant of 0.005, raised to 189. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 12_383.94 + ssi: 0 + output: + co_oap: 2_268 + +- name: Case 35, a monthly average of exactly half a cent rounds up. + # $9,000.06/year of rent averages to $750.005/month, $750.01 in cents + # (half a cent rounded up). 750.01 - 20 = 730.01; 1,032 - 730.01 = 301.99, + # paid as 301 (3.581(E)), or 3,612/year. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 9_000.06 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 8_760.12 + co_oap: 3_612 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml new file mode 100644 index 00000000000..470e7e55d0d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml @@ -0,0 +1,648 @@ +- name: Case 1, a working spouse's earnings are deemed with no threshold. + # The client is 62 with no income. The spouse, 50, earns $1,500/month and + # does not take up Medicaid. 9 CCR 2503-5 3.534(A): + # (1,500 - 65) / 2 = 717.50/month, or 8,610/year. The spouse is not + # OAP-eligible, so nothing is deemed the other way. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 18_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, false] + co_oap_deemed_income: [8_610, 0] + +- name: Case 2, a spouse on Medicaid with income within the OAP limit has nothing deemed. + # Same couple, with the spouse enrolled in Medicaid. The spouse's own + # countable income, (1,500 - 65 - 10) / 2 = 712.50, is within the $1,032 + # limit, so 3.534(C) deems none of it. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 18_000 + medicaid_enrolled: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_own_countable_income: [0, 8_550] + co_oap_deeming_exempt: [false, true] + co_oap_deemed_income: [0, 0] + +- name: Case 3, a spouse's unearned income above the grant standard is deemed. + # The spouse, 50, has a $1,500/month pension. 3.534(B)(2): + # 1,500 - 1,032 = 468/month, or 5,616/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 18_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [5_616, 0] + +- name: Case 4, the spouse's court-ordered child support is deducted. + # As Case 3, with $200/month of child support paid. 3.534(B)(5): + # 1,500 - 1,032 - 200 = 268/month, or 3,216/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 18_000 + child_support_expense: 2_400 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [3_216, 0] + +- name: Case 5, an allocation for a dependent child with no income. + # The spouse, 50, has a $2,000/month pension; their child, 10, has no + # income. 3.534(B)(3) deducts half the $994 SSI benefit standard: + # 2,000 - 1,032 - 497 = 471/month, or 5,652/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [5_652, 0, 0] + +- name: Case 6, the child allocation is reduced by the child's own income. + # As Case 5, with the child receiving $100/month of Social Security + # survivor benefits. The + # allocation is 497 - 100 = 397: 2,000 - 1,032 - 397 = 571/month, or + # 6,852/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + social_security_survivors: 1_200 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [6_852, 0, 0] + +- name: Case 7, a spouse on SSI has nothing deemed. + # The spouse, 66, earns $1,000/month. SSI counts (1,000 - 20 - 65) / 2 = + # 457.50 and pays 536.50. The spouse's OAP income, (1,000 - 65) / 2 + + # 536.50 = 1,004, is within the $1,032 limit, so 3.534(C) deems nothing. + # 3.534(A) would otherwise deem (1,000 - 65) / 2 = 467.50. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 66 + employment_income: 12_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [0, 6_438] + co_oap_deeming_exempt: [true, true] + co_oap_deemed_income: [0, 0] + +- name: Case 8, spousal earnings under $65 deem nothing. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 600 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [0, 0] + +- name: Case 9, two OAP clients within the limit have nothing deemed to each other. + # Both are 62. The spouse's $600/month pension gives own countable income + # 600 - 10 = 590, within the limit, and the spouse is an OAP client + # (1,032 - 590 = 442/month, or 5,304/year), so 3.534(C) deems nothing to + # the head, who gets the full 1,032/month. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 62 + pension_income: 7_200 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, true] + co_oap_deemed_income: [0, 0] + co_oap: [12_384, 5_304] + +- name: Case 10, earned and unearned income are deemed together. + # The spouse, 50, earns $1,000/month and has a $1,200/month pension. + # (1,000 - 65) / 2 + (1,200 - 1,032) = 467.50 + 168 = 635.50/month, or + # 7,626/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 12_000 + pension_income: 14_400 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [7_626, 0] + # The spouse: (1,200 - 10) + (1,000 - 65) / 2 = 1,657.50/month of their own. + co_oap_countable_income: [7_626, 19_890] + +- name: Case 11, a child's SSI counts as the child's own income. + # As Case 5, with the child receiving $600/month of SSI. 3.534(B)(3): the + # allocation is max(0, 497 - 600) = 0, so 2,000 - 1,032 = 968/month, or + # 11,616/year, is deemed. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + ssi: 7_200 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [11_616, 0, 0] + +- name: Case 12, a spouse on SSI above the OAP limit has SSI deemed with the rest. + # Both are 65; the spouse has a $1,000/month pension. SSI computes the + # couple: 1,491 - (1,000 - 20) = 511, or 255.50 each. The spouse's own OAP + # income, 1,000 - 10 + 255.50 = 1,245.50, exceeds the limit, so 3.534(C) + # does not apply. 3.534(B): 1,000 + 255.50 - 1,032 = 223.50/month is + # deemed, or 2,682/year. The client: 1,032 - 255.50 - 223.50 = 553/month, + # or 6,636/year. + period: 2026 + input: + people: + head: + age: 65 + is_tax_unit_head: true + spouse: + age: 65 + pension_income: 12_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [3_066, 3_066] + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [2_682, 0] + co_oap: [6_636, 0] + +- name: Case 13, the child allocation comes from the spouse's household. + # The client, 62, lives alone. The spouse, 50, lives in another Colorado + # household with their child, 10, and has a $2,000/month pension. + # 2,000 - 1,032 - 497 = 471/month is deemed, or 5,652/year. The client: + # 1,032 - 471 = 561/month, or 6,732/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + head_household: + members: [head] + state_code: CO + spouse_household: + members: [spouse, child] + state_code: CO + output: + co_oap_deemed_income: [5_652, 0, 0] + co_oap: [6_732, 0, 0] + +- name: Case 14, a spouse in another state on Medicaid has nothing deemed. + # The client, 62, lives in Colorado. The spouse, 50, lives in New York, + # earns $1,000/month and is enrolled in Medicaid. 3.534(C) sets no + # residence condition: the spouse's own countable income, + # (1,000 - 65 - 10) / 2 = 462.50, is within the limit, so nothing is deemed. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 12_000 + medicaid_enrolled: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + head_household: + members: [head] + state_code: CO + spouse_household: + members: [spouse] + state_code: NY + output: + co_oap_deeming_exempt: [false, true] + co_oap_deemed_income: [0, 0] + co_oap: [12_384, 0] + +- name: Case 15, a spouse whose income equals the standard receives no OAP and is deemed. + # Both are 62. The spouse earns $2,138.87/month and has $0.13/month of + # pension: (2,138.87 - 65 - 9.87) / 2 = 1,032, so the spouse is denied + # (3.533) and is not receiving Adult Financial payments. 3.534(A) deems + # (2,138.87 - 65) / 2 = 1,036.935/month, or 12,443.22/year, and the client + # is denied too. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 62 + employment_income: 25_666.44 + pension_income: 1.56 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [12_443.22, 0] + co_oap: [0, 0] + +- name: Case 16, reported current Medicaid coverage exempts the spouse. + # 2027, so the Medicaid branch runs. The spouse, 40, earns $5,000/year and + # reports current Medicaid coverage but would not take it up as modeled. + # 3.534(C) asks whether the spouse is receiving Medicaid, so nothing is + # deemed and the client gets the full 1,032/month. + period: 2027 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 40 + employment_income: 5_000 + receives_medicaid: true + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, true] + co_oap: [12_384, 0] + +- name: Case 17, a child's own child support reduces the child allocation. + # As Case 5, with the child receiving $900/month of child support. + # 3.520.786 exempts income of the client, spouse and sponsor, not the + # child, so the allocation is max(0, 497 - 900) = 0 and 2,000 - 1,032 = + # 968/month is deemed, or 11,616/year. The client: 1,032 - 968 = 64, + # raised to 189. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + child_support_received: 10_800 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [11_616, 0, 0] + co_oap: [2_268, 0, 0] + +- name: Case 18, a non-exempt spouse's matching ISM is deemed. + # Both are 65 and receive shelter from outside. SSA charges the couple a + # presumed maximum value of 1,491 / 3 + 20 = 517, or 258.50 each, and with + # the spouse's $950/month pension pays SSI of 1,491 - (950 + 517 - 20) = 44, + # or 22 each. The spouse's own OAP income, 950 + 258.50 - 10 + 22 = + # 1,220.50, exceeds the limit. 3.534(B): 950 + 258.50 + 22 - 1,032 = + # 198.50/month is deemed, or 2,382/year. The client: 1,032 - (258.50 - 10 + # + 22) - 198.50 = 563/month, or 6,756/year; SSI reduces this grant, so + # cents would be kept. + period: 2026 + input: + people: + head: + age: 65 + ssi_receives_outside_shelter_support: true + is_tax_unit_head: true + spouse: + age: 65 + pension_income: 11_400 + ssi_receives_outside_shelter_support: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [264, 264] + co_oap_deemed_income: [2_382, 0] + co_oap: [6_756, 0] + +- name: Case 19, an 18-year-old in secondary school is a dependent child. + # As Case 5, with the child 18 and in secondary school. The rule does not + # define "dependent child"; TANF's ages (under 19 for a secondary school + # student) apply, so 2,000 - 1,032 - 497 = 471/month is deemed, or + # 5,652/year. The client: 1,032 - 471 = 561/month, or 6,732/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 18 + is_in_secondary_school: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [5_652, 0, 0] + co_oap: [6_732, 0, 0] + +- name: Case 20, a child's rental loss does not offset the child's other income. + # As Case 5, with the child receiving $600/month of survivor benefits and a + # $300/month rental loss. The loss counts as zero, so the allocation is + # max(0, 497 - 600) = 0 and 968/month is deemed, or 11,616/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + social_security_survivors: 7_200 + rental_income: -3_600 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [11_616, 0, 0] + co_oap: [2_268, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml new file mode 100644 index 00000000000..51f56be613d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml @@ -0,0 +1,229 @@ +- name: Case 1, a spouse on SSI within the OAP limit. + # 9 CCR 2503-5 3.534(C): 400 + 600 = 1,000/month is within the $1,032 limit. + # Each case's client is OAP-eligible with no income; the first output is + # the client, who is not the spouse of an OAP-eligible client unless the + # spouse is OAP-eligible too. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 4_800 + co_oap_own_countable_income: 7_200 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, true] + +- name: Case 2, a spouse on SSI above the OAP limit. + # 100 + 1,000 = 1,100/month exceeds the limit. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 1_200 + co_oap_own_countable_income: 12_000 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, false] + +- name: Case 3, a spouse on Medicaid with income equal to the OAP limit. + # "No greater than the OAP limit" includes income equal to it. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_384 + co_oap_eligible: false + co_oap_medicaid_enrolled: true + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, true] + +- name: Case 4, a spouse on Medicaid above the OAP limit. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_396 + co_oap_eligible: false + co_oap_medicaid_enrolled: true + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, false] + +- name: Case 5, a spouse who is an OAP client receives Adult Financial grant payments. + # Own countable income of 1,031/month leaves a grant of 1, so the spouse + # receives OAP with income within the limit. The client, married to an + # OAP-eligible spouse and within the limit, is exempt too. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_372 + co_oap_eligible: true + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, true] + +- name: Case 6, an OAP-eligible spouse with income equal to the standard receives no grant. + # 3.533: income equal to the grant standard means denial, so the spouse is + # not receiving Adult Financial grant payments. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_384 + co_oap_eligible: true + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] + +- name: Case 7, a spouse with no assistance has income deemed however low it is. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 0 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, false] + +- name: Case 8, reported SSI receipt counts. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 0 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + receives_ssi: true + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, true] + +- name: Case 9, income a hair under the standard from float residue is not a grant. + # Own countable income of 1,031.9999/month, the residue of 1,032 computed + # from annual amounts, gives the OAP-eligible spouse no grant. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_383.9988 + co_oap_eligible: true + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml new file mode 100644 index 00000000000..79beddccd90 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml @@ -0,0 +1,269 @@ +- name: Case 1, earnings only, with the whole disregard reaching earnings. + # 9 CCR 2503-5 3.533(A)(2)(f): gross unearned income of $0 is under $20, so + # the $20 reduces earnings: (500 - 65 - 20) / 2 = 207.50/month, or + # 2,490/year. At 62 the client is not SSI-eligible, so SSI counts nothing. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 0 + co_oap_own_countable_income: 2_490 + +- name: Case 2, unearned income less the $20 disregard. + # 3.533(A)(2): 500 - 20 = 480/month, or 5,760/year. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 5_760 + +- name: Case 3, unearned income under $20 passes the rest of the disregard to earnings. + # $10/month of pension and $500/month of earnings. The disregard covers the + # $10 of pension (3.533(A)(2)(c)) and the other $10 reduces earnings + # (3.533(A)(2)(f)): (500 - 65 - 10) / 2 = 212.50/month, or 2,550/year. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 120 + employment_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 2_550 + +- name: Case 4, a married client's half of the disregard reaches earnings. + # 3.533(A)(2)(c)(1) splits the $20 between the spouses, so the client's + # disregard is $10: (500 - 65 - 10) / 2 = 212.50/month. The spouse has no + # income. + period: 2026 + input: + people: + head: + age: 62 + employment_income: 6_000 + is_tax_unit_head: true + spouse: + age: 62 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_own_countable_income: [2_550, 0] + +- name: Case 5, a married client's half of the disregard reduces unearned income. + # 500 - 10 = 490/month, or 5,880/year. + period: 2026 + input: + people: + head: + age: 62 + pension_income: 6_000 + is_tax_unit_head: true + spouse: + age: 62 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_own_countable_income: [5_880, 0] + +- name: Case 6, an SSI-only client with earnings gets no unearned disregard. + # $300/month of earnings. SSI moves its $20 general exclusion to earnings: + # (300 - 20 - 65) / 2 = 107.50, so SSI pays 994 - 107.50 = 886.50. + # 3.533(A)(2)(c)(2) gives a client whose only unearned income is SSI no + # disregard, so none reaches earnings: (300 - 65) / 2 = 117.50/month, or + # 1,410/year. + period: 2026 + input: + people: + person: + age: 70 + employment_income: 3_600 + households: + household: + members: [person] + state_code: CO + output: + ssi: 10_638 + ssi_countable_income: 1_290 + co_oap_own_countable_income: 1_410 + +- name: Case 7, SSI brings gross unearned income above $20, so nothing reaches earnings. + # $5/month of interest and $200/month of earnings. The disregard covers the + # $5 of interest; gross unearned income including SSI is far above $20, so + # nothing reaches earnings (3.533(A)(2)(f)): (200 - 65) / 2 = 67.50/month, + # or 810/year. SSI moves the unused $15 to earnings: + # (200 - 65 - 15) / 2 = 60, or 720/year. + period: 2026 + input: + people: + person: + age: 70 + interest_income: 60 + employment_income: 2_400 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 720 + co_oap_own_countable_income: 810 + +- name: Case 8, earnings under the $65 disregard count as nothing. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 600 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 0 + +- name: Case 9, an SSI recipient charged ISM by SSA is charged a matching ISM. + # In their own household with shelter supplied from outside, SSA counts the + # presumed maximum value of 994 / 3 + 20 = 351.33/month and pays + # 994 - 331.33 = 662.67. 3.520.79(B)(6) charges the same ISM, which OAP + # counts as unearned income less the $20 disregard: 331.33/month, or + # 3,976/year. Total countable income 662.67 + 331.33 = 994 leaves a grant + # of 38, raised to the $189 floor. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + person: + age: 70 + ssi_receives_outside_shelter_support: true + households: + household: + members: [person] + state_code: CO + output: + ssi: 7_952 + co_oap_own_countable_income: 3_976 + co_oap: 2_268 + +- name: Case 10, SSI's ISM is not charged to a client who is not on SSI. + # Colorado's own ISM for clients not on SSI (3.520.79(C)) is not modeled. + period: 2026 + input: + people: + person: + age: 62 + ssi_receives_outside_shelter_support: true + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 0 + +- name: Case 11, an SSI-only client with SSI under $20 still passes nothing to earnings. + # SSI of $10/month is the only unearned income, so 3.533(A)(2)(c)(2) gives + # no disregard to pass on, though gross unearned income is under $20: + # (500 - 65) / 2 = 217.50/month, or 2,610/year. + period: 2026 + input: + state_code: CO + ssi: 120 + employment_income: 6_000 + output: + co_oap_own_countable_income: 2_610 + +- name: Case 12, interest and dividends on countable resources are exempt. + # 3.520.786(K) exempts the unearned income SSA excludes, including interest + # and dividends on countable resources (20 CFR 416.1124(c)(22)). + period: 2026 + input: + people: + person: + age: 62 + interest_income: 360 + dividend_income: 240 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 0 + co_oap: 12_384 + +- name: Case 13, child support received is exempt. + # 3.520.786(N) exempts child support received by the client for another + # person, and (P) arrears received for the client. A 62-year-old caring for + # their child receives $500/month. + period: 2026 + input: + people: + parent: + age: 62 + child_support_received: 6_000 + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + co_oap_own_countable_income: [0, 0] + co_oap: [12_384, 0] + +- name: Case 14, a rental loss does not offset other income. + # 3.520.785(B)(20): rental income counts to the extent it exceeds expenses. + # $1,000/month of pension less the $20 disregard is 980/month, or + # 11,760/year; the grant of 52 is raised to the floor. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 12_000 + rental_income: -6_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 11_760 + co_oap: 2_268 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py new file mode 100644 index 00000000000..b784b65c44f --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -0,0 +1,39 @@ +import numpy as np + +# Colorado records monthly income and grants in dollars and cents +# (9 CCR 2503-5 3.581(E) speaks of cents; 3.533 and 3.534 of $65, $20.00 and +# halves). The OAP variables therefore take each monthly amount in cents: +# income averaged from an annual amount (3.520.781(G)) to the nearest cent, +# half a cent rounded up, and SSI as SSA pays it. The rule's own arithmetic +# then stays on a half-cent grid (earnings are halved), and anything smaller +# than a tenth of a cent is float32 residue. +PRECISION = 0.001 + +# Annual amounts in cents average to multiples of 1/1,200 of a dollar, so an +# average is either exactly a half cent past a whole cent or at least $0.00083 +# away from one. Storing the annual amount in float32 and then dividing it by +# 12 in float32 leaves about $0.0001 of residue near $1,000 a month, so $0.0004 +# of slack, about halfway between, keeps a true half cent from rounding down +# without moving any other average across a tie. That holds for a monthly +# amount from one source below about $4,000; above that, float32 cannot +# resolve the 1/1,200 grid, and a tie can round either way. +_TIE_SLACK = 0.0004 + + +def cents(amount): + """A monthly amount to the nearest cent, half a cent rounded up.""" + amount = np.asarray(amount, dtype=np.float64) + return np.floor(amount * 100 + 0.5 + _TIE_SLACK * 100) / 100 + + +def ssi_paid(amount): + """A monthly SSI amount as SSA pays it. + + POMS SI 02001.010: a fractional cent of at least a tenth of a cent raises + the payment to the next higher cent; a smaller one is dropped. + """ + amount = np.asarray(amount, dtype=np.float64) + # Whole mills, with half a tenth of a mill ($0.00005) of slack for + # float32 residue. + mills = np.floor(amount * 1_000 + 0.05) + return np.ceil(mills / 10) / 100 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py index 9a95709317f..a97efb8add0 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py @@ -1,17 +1,40 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + PRECISION, + cents, + ssi_paid, +) class co_oap(Variable): value_type = float entity = Person label = "Colorado Old Age Pension" + unit = USD definition_period = YEAR defined_for = "co_oap_eligible" + # PDF pages 64, 74-77, 134 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=64" def formula(person, period, parameters): - income = person("ssi_countable_income", period) - ssi = person("ssi", period) - total_countable_income = ssi + income - p = parameters(period).gov.states.co.ssa.oap - grant_standard = p.grant_standard * MONTHS_IN_YEAR - return max_(0, grant_standard - total_countable_income) + total = 0 + for month in period.get_subperiods(MONTH): + p = parameters(month).gov.states.co.ssa.oap + # 9 CCR 2503-5 3.532(B), 3.533(B): the grant is the grant standard + # less total countable income, SSI included (3.533(A)(2)(d)). + # PE-US's SSI counts interest that 20 CFR 416.1124(c)(22) + # excludes (#10079); OAP, which adds SSI back, offsets it. + ssi = ssi_paid(person("ssi", month)) + income = ssi + person("co_oap_countable_income", month) + grant = p.grant_standard - income + # 3.533 and 3.520.781(A): a client whose total countable income + # equals or exceeds the grant standard is not eligible. + # 3.531(D)(2), 3.532(B), 3.533(B): an eligible client's grant below + # the payment floor standard is raised to it by a state + # supplementary payment, month by month. + payment = where(grant > PRECISION, max_(grant, p.payment_floor), 0) + # 3.581(E): payments drop any cents, "except in cases where SSI + # income reduces the grant"; those are paid to the cent. + payment = where(ssi > 0, cents(payment), np.floor(payment + PRECISION)) + total = total + payment + return total diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py new file mode 100644 index 00000000000..46abc1893a7 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py @@ -0,0 +1,14 @@ +from policyengine_us.model_api import * + + +class co_oap_countable_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension countable income other than SSI" + unit = USD + definition_period = MONTH + # PDF pages 76-78 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76" + # 9 CCR 2503-5 3.520.781(D)-(F): the client's own countable income + # (3.533) plus income deemed from the spouse (3.534). + adds = ["co_oap_own_countable_income", "co_oap_deemed_income"] diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py new file mode 100644 index 00000000000..d8f74ed484e --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py @@ -0,0 +1,100 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + cents, + ssi_paid, +) + + +class co_oap_deemed_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension income deemed from a spouse" + unit = USD + definition_period = MONTH + defined_for = StateCode.CO + # PDF pages 77-78 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=77" + + def formula(person, period, parameters): + p = parameters(period).gov.states.co.ssa.oap + # 3.534(C): no income is deemed from a spouse who receives Adult + # Financial, SSI or Medicaid with income within the OAP limit. + deemable = ~person("co_oap_deeming_exempt", period) + # Monthly amounts in cents, SSI as SSA pays it (see _precision.py). + ssi = ssi_paid(person("ssi", period)) + earned = cents(person("ssi_earned_income", period)) * deemable + # 3.534(B)(1): the total unearned income of the spouse, SSI and any + # matching ISM included (3.520.785(B)(1), 3.520.79(B)(6)), less exempt + # income (3.520.786). + unearned = ( + add( + person, + period, + ["co_oap_unearned_income", "co_oap_in_kind_support_and_maintenance"], + ) + + ssi + ) * deemable + # 3.534(B)(5): court-ordered support obligations of the spouse. We + # treat alimony paid as court-ordered. Whether the income is garnished + # instead is not modeled. + support = ( + cents(add(person, period, ["child_support_expense", "alimony_expense"])) + * deemable + ) + + # 3.534(B)(3): for each dependent child in the spouse's household, + # half the SSI benefit standard for an individual less the child's own + # income. The rule does not define "dependent child"; an Adult + # Financial client with one must apply for Colorado Works (3.520.71(F)), + # so we use TANF's ages: under 18, or under 19 in secondary school. + # 3.520.786's exemptions apply to the client, spouse and sponsor, not + # the child, so the child's gross income counts, child support and SSI + # included, with no source below zero. + year = period.this_year + tanf_age = parameters(period).gov.hhs.tanf.cash.eligibility.age_limit + age_limit = where( + person("is_in_secondary_school", period), + tanf_age.student, + tanf_age.non_student, + ) + dependent_child = person("is_tax_unit_dependent", year) & ( + person("age", year) < age_limit + ) + ssi_sources = parameters(period).gov.ssa.ssi.income.sources + child_sources = [ + cents(max_(0, person(source, period))) + for source in ["ssi_earned_income", *ssi_sources.unearned] + ] + child_income = sum(child_sources) + ssi + fbr = parameters(period).gov.ssa.ssi.amount.individual + child_allocation = dependent_child * max_( + 0, fbr * p.income.deeming.child_allocation_share - child_income + ) + # Each person's household total; the spouse's reaches the client + # through the marital unit. + child_allocations = person.household.sum(child_allocation) * deemable + + marital_unit = person.marital_unit + + def spouse(values): + return marital_unit.sum(values) - values + + # 3.534(A): deduct $65 from the spouse's gross earned income and halve + # the rest. Unlike SSI (20 CFR 416.1163) there is no deeming threshold. + deemed_earned = ( + max_(0, spouse(earned) - p.income.disregards.earned) + * p.income.disregards.earned_share + ) + # 3.534(B): deduct the OAP grant standard, child allocations and + # support obligations from the spouse's unearned income. Medical care + # payments for uninsured dependents (3.534(B)(4)) are not modeled. + deemed_unearned = max_( + 0, + spouse(unearned) + - p.grant_standard + - spouse(child_allocations) + - spouse(support), + ) + # Sponsor deeming (3.534(D)) and the institutionalized spouse's MMMNA + # (3.534(C)) are not modeled. + return deemed_earned + deemed_unearned diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py new file mode 100644 index 00000000000..227011917f2 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py @@ -0,0 +1,55 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + PRECISION, + ssi_paid, +) + + +class co_oap_deeming_exempt(Variable): + value_type = bool + entity = Person + label = "Colorado Old Age Pension client's spouse whose income is not deemed" + documentation = ( + "True for the spouse of an Old Age Pension-eligible client when " + "3.534(C) keeps the spouse's income from being deemed; false for " + "everyone else." + ) + definition_period = MONTH + # Not restricted to Colorado: 3.534(C) sets no residence condition on the + # spouse. + # PDF page 78 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.534(C): the income of a spouse who receives Adult + # Financial grant payments, SSI or Medicaid "and has income no greater + # than the OAP limit" is not deemed to the OAP client. + p = parameters(period).gov.states.co.ssa.oap + ssi = ssi_paid(person("ssi", period)) + # The rule does not define the OAP limit. We read it as the OAP grant + # standard, against the spouse's own total countable income under + # 3.533, SSI included. + income = person("co_oap_own_countable_income", period) + ssi + within_limit = income <= p.grant_standard + PRECISION + receives_ssi = (ssi > 0) | person("receives_ssi", period) + # Old Age Pension is the Adult Financial program modeled for someone + # who is not on SSI; AND-CS requires SSI. Receipt is judged on the + # spouse's own income, as co_oap judges a positive grant: when both + # spouses are OAP clients within the limit, neither one's income is + # deemed to the other. (A reading in which neither receives OAP, and + # each one's income is deemed to the other, can also be + # self-consistent; the model takes the one in which both are paid.) + year = period.this_year + eligible = person("co_oap_eligible", year) + receives_oap = eligible & (p.grant_standard - income > PRECISION) + spouse_of_client = person.marital_unit.sum(eligible) - eligible > 0 + candidate = spouse_of_client & within_limit + exempt = candidate & (receives_ssi | receives_oap) + # Medicaid decides only for a candidate not exempt otherwise. Skipping + # it when no one needs it avoids computing Medicaid enrollment for + # nothing. + needs_medicaid = candidate & ~exempt + if not needs_medicaid.any(): + return exempt + medicaid = person("co_oap_medicaid_enrolled", year) + return exempt | (needs_medicaid & medicaid) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py new file mode 100644 index 00000000000..076bbaa30a3 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + cents, + ssi_paid, +) + + +class co_oap_in_kind_support_and_maintenance(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension in-kind support and maintenance" + unit = USD + definition_period = MONTH + # PDF pages 72-73 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=72" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.520.79(B)(6): an SSI recipient charged in-kind support + # and maintenance by SSA is charged a matching ISM, counted as unearned + # income. A joint couple's ISM is one presumed maximum value for the + # couple, which SSI splits equally between the spouses. ISM through + # SSA's one-third reduction, and Colorado's own ISM for a client not + # on SSI (3.520.79(C)), are not modeled. + ism = person("ssi_in_kind_support_and_maintenance", period) + couple_computation = person("ssi_couple_computation_applies", period) + receives_ssi = ssi_paid(person("ssi", period)) > 0 + ism = where(couple_computation, ism / 2, ism) * receives_ssi + return cents(ism) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py new file mode 100644 index 00000000000..fafc5859681 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py @@ -0,0 +1,53 @@ +from policyengine_us.model_api import * + + +class co_oap_medicaid_enrolled(Variable): + value_type = bool + entity = Person + label = "Medicaid enrollment for the Colorado Old Age Pension deeming exemption" + definition_period = YEAR + reference = ( + # 9 CCR 2503-5 3.534(C) + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78", + "https://www.ecfr.gov/current/title-42/section-435.554#p-435.554(c)(7)", + ) + + def formula(person, period, parameters): + # Reported current coverage counts as receiving Medicaid assistance + # (3.534(C)) whatever projected eligibility and take-up say. + reported = person("receives_medicaid", period) + p = parameters(period).gov.hhs.medicaid.eligibility.work_requirements + if not p.applies: + return person("medicaid_enrolled", period) | reported + + # SNAP counts OAP as income, and Medicaid's SNAP pass-through from the + # community engagement requirement (42 CFR 435.554(c)(7)) reads SNAP + # receipt, so OAP, a spouse's Medicaid enrollment and SNAP depend on + # each other. Medicaid is computed in a private branch where only + # reported coverage exempts a spouse's income from OAP deeming: + # enrollment is decided on the SNAP the household receives while the + # other spouses' income is still deemed, before an exemption raises + # OAP. The spouse keeps every Medicaid route, supplied inputs + # included, and the result does not depend on calculation order. + # Where a single unresolved exemption is all that links a group of + # households through SNAP units and marriages, higher OAP can only + # lower SNAP, so at most one outcome is self-consistent, and this + # finds it when it exists; when none exists (the exemption's higher + # OAP would end the SNAP receipt that gave the spouse Medicaid), it + # treats the spouse as enrolled. With several unresolved exemptions + # linked that way, enrollment is decided with all of their income + # deemed, which need not be self-consistent. + simulation = person.simulation + branch_name = f"{simulation.branch_name}_co_oap_medicaid_{period}" + try: + branch = get_override_branch( + simulation, + branch_name, + period, + {"co_oap_medicaid_enrolled": np.asarray(reported, dtype=bool)}, + ) + return branch.calculate("medicaid_enrolled", period) | reported + finally: + # A branch clones cached arrays; retaining it would duplicate a + # substantial part of the full-population simulation in memory. + simulation.branches.pop(branch_name, None) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py new file mode 100644 index 00000000000..538edb6de0b --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py @@ -0,0 +1,51 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + cents, + ssi_paid, +) + + +class co_oap_own_countable_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension countable income of the client other than SSI" + unit = USD + definition_period = MONTH + # Not restricted to Colorado: a client's spouse may live elsewhere + # (3.534(C)). co_oap itself is defined only for Colorado residents. + # PDF pages 68-73, 76-77 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.533(A) counts the client's own income whether or not + # they are eligible for SSI. The SSI that 3.533(A)(2)(d) adds back is + # added in co_oap. + p = parameters(period).gov.states.co.ssa.oap.income + # 3.520.786 limits exempt earned income to the items it lists, so + # SSI's student earned income exclusion is not deducted. + # Monthly amounts in cents, SSI as SSA pays it (see _precision.py). + earned = cents(person("ssi_earned_income", period)) + ssi = ssi_paid(person("ssi", period)) + receives_ssi = ssi > 0 + unearned = person("co_oap_unearned_income", period) + person( + "co_oap_in_kind_support_and_maintenance", period + ) + # 3.533(A)(2)(c)(1): a married client's $20 disregard is split equally + # with the spouse, so that no more than $20 applies to the couple. + married = person.marital_unit.nb_persons() == 2 + disregard = where(married, p.disregards.unearned / 2, p.disregards.unearned) + # 3.533(A)(2)(c): the disregard applies to unearned income other than + # SSI; min_ gives 0 to a client whose only unearned income is SSI + # (3.533(A)(2)(c)(2)). + unearned_disregard = min_(disregard, unearned) + # 3.533(A)(2)(f): when gross unearned income, SSI included, is under + # $20, the rest of the disregard reduces earned income. A client whose + # only unearned income is SSI has no disregard to pass on. + ssi_only = receives_ssi & (unearned == 0) + earned_disregard = where(ssi_only, 0, max_(0, disregard - unearned - ssi)) + # 3.533(A)(1): deduct $65 from gross earned income and halve the rest. + countable_earned = ( + max_(0, earned - p.disregards.earned - earned_disregard) + * p.disregards.earned_share + ) + return unearned - unearned_disregard + countable_earned diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py new file mode 100644 index 00000000000..a3bfd10badd --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import cents + + +class co_oap_unearned_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension gross unearned income other than SSI" + unit = USD + definition_period = MONTH + # PDF pages 68-72 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68" + + def formula(person, period, parameters): + sources = parameters(period).gov.states.co.ssa.oap.income.sources.unearned + # 9 CCR 2503-5 3.520.785(B)(20): rental income counts "to the extent + # it exceeds allowable expenses", so a loss does not offset other + # income. No source counts below zero. Each monthly amount is taken + # in cents (see _precision.py). + monthly = [cents(max_(0, person(source, period))) for source in sources] + return sum(monthly)