diff --git a/changelog.d/ssi-couples-and-deeming-cap.fixed.md b/changelog.d/ssi-couples-and-deeming-cap.fixed.md new file mode 100644 index 00000000000..ac3dfef9fae --- /dev/null +++ b/changelog.d/ssi-couples-and-deeming-cap.fixed.md @@ -0,0 +1,2 @@ +SSI under spousal income deeming is now the lesser of the couple computation and the individual Federal benefit rate less the person's own countable income (20 CFR 416.1163(e)(2)), not the couple computation capped at the individual rate; uncapped_ssi applies the same rule, so state supplements that read it see the federal shortfall. SSI couples are now identified by the co-resident spouse test that resource deeming uses rather than by tax-unit head and spouse roles, for joint claims, combined couple income and resources, and income deeming from an ineligible spouse (416.1801(c), 416.1806, 416.1160(d)). State supplement couple rates, income, resources, care categories and spouse allowances now use only the actual spouses when a situation omits marital units and includes a third adult. Colorado OAP retains declared spouses living apart; Missouri retains their combined resources for medical separation, Virginia and South Dakota retain joint care-entry budgets, and Oregon retains the community-care spouse payment across facilities. Iowa pools only the identified spouses across its state supplement paths, retains its prior-SSI residential-care entry budget across facilities, and requires an explicit shared-room input for the same-month entry rule without prior SSI. +Retained state spouse budgets now reject explicit parent links and tax-dependent pairs across households. Non-MAGI Medicaid income, premium, resource and spenddown budgets and California CAPI use only the identified spouses when marital units are omitted. Regressions pin claimant-wage deeming continuity and the 2026 wage/self-employment cases. diff --git a/policyengine_us/tests/core/test_ssi_spousal_deeming_invariants.py b/policyengine_us/tests/core/test_ssi_spousal_deeming_invariants.py new file mode 100644 index 00000000000..a7b7ea58073 --- /dev/null +++ b/policyengine_us/tests/core/test_ssi_spousal_deeming_invariants.py @@ -0,0 +1,1221 @@ +"""Simulation-API invariants for SSI couples and spousal deeming. + +Two vectorized grid simulations, each built once per module, plus small +simulations for the generated income and state-supplement properties: + +- An income grid of 400 two-person households: an aged claimant with only + unearned income and a spouse under 65 with unearned and earned income. The + axes include the $20 and $65 exclusion edges, the $497 deeming threshold + ($1,491 couple less $994 individual Federal benefit rate in 2026) and its + neighbours, and a claimant income that leaves nothing of the individual + rate. SSI must equal an independent numpy reading of 20 CFR 416.1163, never + exceed the benefit without deeming (416.1163(e)(2)), and never rise when any + income rises. +- A role grid of 72 three-adult households: a married couple living with a + third adult, over all nine assignments of tax-unit head and spouse roles + among the three adults and aged or not for each adult, plus two-person and + one-person households in the same simulation. Couple status and SSI must + not depend on tax roles or on the third adult (416.1801(c), 416.1806, + 416.1160(d)), and joint claims must be symmetric and disjoint from + ineligible-spouse status. +- Every implemented state supplement, comparing a couple and third adult + with the API's default marital unit against explicit couple/single units. + Hypothesis varies income, age, resources and care membership; explicit + examples retain the payment, exclusion, eligibility and resource edges. + Each comparison uses two independent three-person simulations sharing the + same read-only system as the grids. +- Declared spouses who live in separate households in Colorado, Missouri, + Oregon, South Dakota, Virginia and Iowa, comparing their supplements before and + after an unrelated adult joins either household. The marriage, separate + tax/family/SPM budgets, and all benefit inputs stay the same. Six explicit + examples and two generated examples use two small independent simulations + apiece per state, sharing the same read-only system. Generated examples + enter the states' care budgets; independent budget checks also detect a + lost spouse when both simulations make the same error. +- Separated parent/child and dependent pairs in the retained-marriage state + programs, comparing the default marital unit with explicit singletons. + Generated incomes and resources exercise both relationship exclusions. + +YAML covers worked cases. Python is needed to check the for-all properties +over every ordered edge of the grids and to compare with an oracle that does +not use the model. Both grids share the read-only system singleton; neither +constructs a model per household. This file routes to the Rest/core group. +""" + +from copy import deepcopy +from itertools import product +from pathlib import Path + +import numpy as np +import pytest +import yaml +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st +from policyengine_core.periods import period as make_period + +from policyengine_us import Simulation +from policyengine_us.variables.gov.states._spouses import ( + state_couple_total, + state_is_married, +) + +YEAR = 2026 +PERIOD = "2026-01" +INDIVIDUAL_FBR = 994 +COUPLE_FBR = 1_491 +GENERAL_EXCLUSION = 20 +EARNED_EXCLUSION = 65 + +CLAIMANT_UNEARNED = (0, 19, 20, 400, 743, 974, 1_014, 1_200) +SPOUSE_UNEARNED = (0, 100, 300, 497, 498) +SPOUSE_EARNED = (0, 64, 65, 400, 496, 497, 498, 600, 1_000, 3_100) + + +def _countable(unearned, earned): + """416.1112, 416.1124: $20 general, then $65 and half of the rest.""" + general = np.minimum(GENERAL_EXCLUSION, unearned) + leftover = GENERAL_EXCLUSION - general + earned_after = np.maximum(earned - EARNED_EXCLUSION - leftover, 0) / 2 + return unearned - general + earned_after + + +def _oracle_ssi(claimant_unearned, spouse_unearned, spouse_earned, claimant_earned=0): + """Monthly SSI for an aged claimant with an ineligible spouse. + + 416.1163(d)(1): no deeming when the spouse's income is not more than the + couple rate less the individual rate. (d)(2): otherwise the couple's + combined countable income is subtracted from the couple rate. (e)(2): the + benefit is the lesser of that and the individual rate less the claimant's + own countable income. + """ + alone = np.maximum( + INDIVIDUAL_FBR - _countable(claimant_unearned, claimant_earned), + 0, + ) + couple = np.maximum( + COUPLE_FBR + - _countable( + claimant_unearned + spouse_unearned, claimant_earned + spouse_earned + ), + 0, + ) + deems = spouse_unearned + spouse_earned > COUPLE_FBR - INDIVIDUAL_FBR + return np.where(deems, np.minimum(couple, alone), alone) + + +def _income_situation(rows, claimant_earnings=None): + situation = { + plural: {} + for plural in ( + "people", + "tax_units", + "households", + "marital_units", + "families", + "spm_units", + ) + } + for index, (claimant_unearned, spouse_unearned, spouse_earned) in enumerate(rows): + claimant, spouse = f"c{index}", f"s{index}" + situation["people"][claimant] = { + "age": {YEAR: 70}, + "pension_income": {YEAR: 12 * claimant_unearned}, + "employment_income": { + YEAR: 12 * claimant_earnings[index] if claimant_earnings else 0 + }, + } + situation["people"][spouse] = { + "age": {YEAR: 60}, + "pension_income": {YEAR: 12 * spouse_unearned}, + "employment_income": {YEAR: 12 * spouse_earned}, + } + members = [claimant, spouse] + for plural in ( + "tax_units", + "households", + "marital_units", + "families", + "spm_units", + ): + situation[plural][f"u{index}"] = {"members": members} + situation["households"][f"u{index}"]["state_code"] = {YEAR: "TX"} + return situation + + +def _income_ssi(rows, claimant_earnings=None): + simulation = Simulation(situation=_income_situation(rows, claimant_earnings)) + ssi = simulation.calculate("ssi", PERIOD) + without_deeming = simulation.calculate( + "uncapped_ssi_without_spousal_deeming", PERIOD + ) + return ssi[0::2], ssi[1::2], np.maximum(without_deeming[0::2], 0) + + +@pytest.fixture(scope="module") +def income_grid(): + rows = list(product(CLAIMANT_UNEARNED, SPOUSE_UNEARNED, SPOUSE_EARNED)) + claimant_ssi, spouse_ssi, without_deeming = _income_ssi(rows) + shape = (len(CLAIMANT_UNEARNED), len(SPOUSE_UNEARNED), len(SPOUSE_EARNED)) + columns = np.array(rows, dtype=float).T + return { + "rows": columns, + "ssi": claimant_ssi.reshape(shape), + "spouse_ssi": spouse_ssi, + "without_deeming": without_deeming.reshape(shape), + "oracle": _oracle_ssi(*columns).reshape(shape), + } + + +def test_income_grid_matches_independent_oracle(income_grid): + np.testing.assert_allclose(income_grid["ssi"], income_grid["oracle"], atol=0.01) + assert np.all(income_grid["spouse_ssi"] == 0) + + +def test_deemed_benefit_never_exceeds_benefit_without_deeming(income_grid): + assert np.all(income_grid["ssi"] <= income_grid["without_deeming"] + 0.01) + + +@pytest.mark.parametrize("axis", [0, 1, 2], ids=["claimant", "unearned", "earned"]) +def test_ssi_never_rises_with_income(income_grid, axis): + # Each axis is sorted ascending, so every ordered edge is a rise in one + # income with the others fixed. + assert np.all(np.diff(income_grid["ssi"], axis=axis) <= 0.01) + + +def test_claimant_wages_preserve_the_cap_with_and_without_deeming(): + """The two #10138 cases share the same individual countable-income cap.""" + spouse_earnings = (6_000, 5_900) + situation = _income_situation( + [(0, 0, income / 12) for income in spouse_earnings], + claimant_earnings=(1_000, 1_000), + ) + for index, income in enumerate(spouse_earnings): + claimant = situation["people"][f"c{index}"] + spouse = situation["people"][f"s{index}"] + claimant["age"] = {YEAR: 67} + spouse["age"] = {YEAR: 45} + spouse.pop("employment_income") + spouse["self_employment_income"] = {YEAR: income} + simulation = Simulation(situation=situation) + np.testing.assert_array_equal( + simulation.calculate("is_ssi_spousal_deeming_applies", YEAR)[0::2], + [True, False], + ) + # $994 - ($1,000 - $20 - $65) / 2 = $536.50 monthly, $6,438 annually. + np.testing.assert_allclose( + simulation.calculate("ssi", YEAR)[0::2], [6_438, 6_438], atol=0.01 + ) + + +@settings( + max_examples=3, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + st.lists( + st.tuples( + st.integers(0, 1_300), + st.integers(0, 900), + st.integers(0, 3_500), + st.integers(0, 2_500), + ), + min_size=40, + max_size=40, + ) +) +def test_random_incomes_match_independent_oracle(rows): + # Add the three sides of the $497 threshold to the same simulation, so + # generated claimant wages exercise 416.1163(e)(2) without more setup. + income_rows = [row[:3] for row in rows] + claimant_earnings = [row[3] for row in rows] + for claimant_unearned, _, _, claimant_earned in rows: + for spouse_unearned in (496, 497, 498): + income_rows.append((claimant_unearned, spouse_unearned, 0)) + claimant_earnings.append(claimant_earned) + claimant_ssi, _, _ = _income_ssi(income_rows, claimant_earnings) + expected = _oracle_ssi( + *np.array(income_rows, dtype=float).T, np.asarray(claimant_earnings) + ) + np.testing.assert_allclose(claimant_ssi, expected, atol=0.01) + threshold = claimant_ssi[len(rows) :].reshape(len(rows), 3) + np.testing.assert_allclose(threshold[:, 0], threshold[:, 1], atol=0.01) + # One extra dollar of spouse income may reduce SSI by at most a dollar; + # starting deeming cannot cause a jump when the claimant earns wages. + assert np.all(np.diff(threshold, axis=1) <= 0.01) + assert np.all(np.diff(threshold, axis=1) >= -1.01) + + +# Role grid: adults "a" and "b" are married; "c" lives with them in its own +# marital unit. Every head/spouse assignment among the three is supplied. +# The same simulation also holds two-person and one-person households, so +# household positions differ across rows. +ADULTS = ("a", "b", "c") +ROLE_ASSIGNMENTS = [ + (head, spouse) for head in ADULTS for spouse in (None, *ADULTS) if spouse != head +] +AGED = (False, True) +INCOME = {"a": 600, "b": 300, "c": 900} + + +def _adult(age, income, head=False, spouse=False): + return { + "age": {YEAR: age}, + "pension_income": {YEAR: 12 * income}, + "is_tax_unit_head": {YEAR: head}, + "is_tax_unit_spouse": {YEAR: spouse}, + } + + +def _role_situation(): + situation = { + plural: {} + for plural in ( + "people", + "tax_units", + "households", + "marital_units", + "families", + "spm_units", + ) + } + + def add_household(key, people, marital_units): + situation["people"].update(people) + for plural in ("tax_units", "households", "families", "spm_units"): + situation[plural][key] = {"members": list(people)} + situation["households"][key]["state_code"] = {YEAR: "TX"} + for index, members in enumerate(marital_units): + situation["marital_units"][f"{key}_m{index}"] = {"members": members} + + configurations = list(product(product(AGED, repeat=3), ROLE_ASSIGNMENTS)) + for index, (aged, (head, spouse)) in enumerate(configurations): + names = {adult: f"{adult}{index}" for adult in ADULTS} + people = { + names[adult]: _adult( + 70 if is_aged else 50, + INCOME[adult], + head=adult == head, + spouse=adult == spouse, + ) + for adult, is_aged in zip(ADULTS, aged) + } + add_household(f"u{index}", people, [[names["a"], names["b"]], [names["c"]]]) + extras = list(product(AGED, repeat=2)) + for index, (a_aged, b_aged) in enumerate(extras): + couple = { + f"pa{index}": _adult(70 if a_aged else 50, INCOME["a"], head=True), + f"pb{index}": _adult(70 if b_aged else 50, INCOME["b"], spouse=True), + } + add_household(f"pair{index}", couple, [list(couple)]) + single = {f"s{index}": _adult(70 if a_aged else 50, INCOME["c"], head=True)} + add_household(f"single{index}", single, [list(single)]) + return situation, configurations, extras + + +@pytest.fixture(scope="module") +def role_grid(): + situation, configurations, extras = _role_situation() + simulation = Simulation(situation=situation) + grid_people = 3 * len(configurations) + shape = (len(configurations) // len(ROLE_ASSIGNMENTS), len(ROLE_ASSIGNMENTS), 3) + + def calculate(variable, period): + values = simulation.calculate(variable, period) + return values[:grid_people].reshape(shape), values[grid_people:].reshape( + len(extras), 3 + ) + + joint, joint_extra = calculate("ssi_claim_is_joint", YEAR) + ineligible, ineligible_extra = calculate("is_ssi_ineligible_spouse", YEAR) + ssi, ssi_extra = calculate("ssi", PERIOD) + return { + "aged": [aged for aged, _ in configurations[:: len(ROLE_ASSIGNMENTS)]], + "joint": joint, + "ineligible_spouse": ineligible, + "ssi": ssi, + "extras": extras, + "joint_extra": joint_extra, + "ineligible_extra": ineligible_extra, + "ssi_extra": ssi_extra, + } + + +def test_couple_status_and_ssi_do_not_depend_on_tax_roles(role_grid): + for name in ("joint", "ineligible_spouse", "ssi"): + values = role_grid[name] + assert np.all(values == values[:, :1, :]), name + + +def test_couple_status_matches_the_couple_definitions(role_grid): + # 416.1801(c) and 416.1160(d), with aged, blind or disabled status + # standing for SSI eligibility (ssi_claim_is_joint does not apply the + # citizenship condition). + for index, aged in enumerate(role_grid["aged"]): + couple = aged[0] and aged[1] + assert np.all(role_grid["joint"][index, :, :2] == couple) + # The third adult is not married to either spouse. + assert not np.any(role_grid["joint"][index, :, 2]) + assert not np.any(role_grid["ineligible_spouse"][index, :, 2]) + ineligible = role_grid["ineligible_spouse"][index] + assert np.all(ineligible[:, 0] == (aged[1] and not aged[0])) + assert np.all(ineligible[:, 1] == (aged[0] and not aged[1])) + assert not np.any(role_grid["joint"] & role_grid["ineligible_spouse"]) + + +def test_role_grid_couples_match_the_same_couple_living_alone(role_grid): + # Columns are (pair a, pair b, single). A couple's status and SSI do not + # change when a third adult shares their household and tax unit. + joint_extra = role_grid["joint_extra"] + ssi_extra = role_grid["ssi_extra"] + for index, (a_aged, b_aged) in enumerate(role_grid["extras"]): + assert np.all(joint_extra[index, :2] == (a_aged and b_aged)) + assert not joint_extra[index, 2] + assert not role_grid["ineligible_extra"][index, 2] + rows = [ + row + for row, aged in enumerate(role_grid["aged"]) + if aged[:2] == (a_aged, b_aged) + ] + for row in rows: + np.testing.assert_allclose( + role_grid["ssi"][row, 0, :2], ssi_extra[index, :2], atol=0.01 + ) + + +# Every implemented state supplement, including programs paid to an SPM unit +# rather than directly to a person. Annual programs are compared annually. +SUPPLEMENT_PROGRAMS = { + "AK": ("ak_ssp",), + "AL": ("al_ssp",), + "CA": ("ca_state_supplement", "ca_capi"), + "CO": ("co_state_supplement", "co_oap"), + "CT": ("ct_ssp",), + "DC": ("dc_ossp",), + "DE": ("de_ssp",), + "FL": ("fl_oss",), + "GA": ("ga_ssp",), + "HI": ("hi_oss",), + "IA": ("ia_ssa",), + "ID": ("id_aabd",), + "IL": ("il_aabd",), + "IN": ("in_ssp",), + "KS": ("ks_sspp",), + "KY": ("ky_ssp",), + "LA": ("la_oss",), + "MA": ("ma_state_supplement",), + "MD": ("md_paa",), + "ME": ("me_ssp",), + "MI": ("mi_ssp",), + "MN": ("mn_msa",), + "MO": ("mo_ssp",), + "MT": ("mt_ssp",), + "NC": ("nc_sa",), + "NE": ("ne_aabd",), + "NH": ("nh_ssp",), + "NJ": ("nj_ssp",), + "NM": ("nm_ssi_state_supplement",), + "NV": ("nv_oss",), + "NY": ("ny_ssp",), + "OH": ("oh_rss",), + "OK": ("ok_ssp",), + "OR": ("or_ssp",), + "PA": ("pa_ssp",), + "RI": ("ri_ssp",), + "SC": ("sc_ssi_state_supplement",), + "SD": ("sd_ssp",), + "TX": ("tx_ssi_state_supplement",), + "UT": ("ut_ssp",), + "VA": ("va_ag",), + "VT": ("vt_aabd",), + "WA": ("wa_ssp",), + "WI": ("wi_ssp",), + "WY": ("wy_ssp",), +} + + +def test_state_supplement_inventory_matches_program_registry(): + """The property includes every implemented supplement in the registry.""" + registry_path = Path(__file__).resolve().parents[2] / "programs.yaml" + registry = yaml.safe_load(registry_path.read_text()) + supplements = next( + program + for program in registry["programs"] + if program["id"] == "ssi_state_supplement" + ) + implemented = { + entry["state"]: entry["variable"] + for entry in supplements["state_implementations"] + if entry.get("variable") + } + assert implemented.keys() == SUPPLEMENT_PROGRAMS.keys() + for state, variable in implemented.items(): + assert variable in SUPPLEMENT_PROGRAMS[state], (state, variable) + + +# Conditions that exercise care-only supplements and CAPI. These are inputs, +# not substitute payment amounts or eligibility results. The builder reads +# each variable's entity and period: Delaware's arrangement belongs to the +# household, while most states' care settings belong to the person. +SUPPLEMENT_CARE_INPUTS = { + "AL": { + "al_receives_elderly_disabled_medicaid_waiver": True, + "al_meets_snf_criteria": True, + "al_ssp_care_setting": "PRIVATE_HOME_IHC", + }, + "CA": { + "immigration_status_str": "LEGAL_PERMANENT_RESIDENT", + "ssi_qualifying_quarters_earnings": 0, + }, + "DC": {"dc_ossp_in_adult_foster_care": True}, + "DE": {"de_ssp_living_arrangement": "CERTIFIED_RESIDENTIAL_CARE_HOME"}, + "FL": {"fl_oss_program_track": "REDESIGN", "fl_oss_community_care_type": "ALF"}, + "HI": {"hi_oss_care_facility_type": "COMMUNITY_CARE"}, + "IA": {"ia_ssa_category": "FAMILY_LIFE_HOME"}, + "ID": {"id_aabd_living_arrangement": "ROOM_AND_BOARD"}, + "IN": { + "ssi_lives_in_medical_treatment_facility": True, + "ssi_medicaid_pays_majority_of_care": True, + "medicaid_enrolled": True, + }, + "KS": { + "ssi_lives_in_medical_treatment_facility": True, + "ssi_medicaid_pays_majority_of_care": True, + }, + "KY": {"ky_ssp_category": "CARETAKER", "ky_ssp_care_receivers": "BOTH"}, + "LA": { + "ssi_lives_in_medical_treatment_facility": True, + "ssi_medicaid_pays_majority_of_care": True, + }, + "MD": {"md_paa_living_arrangement": "CARE_HOME_LEVEL_A"}, + "ME": {"me_ssp_living_arrangement": "ADULT_FOSTER_HOME"}, + "MN": { + "mn_msa_living_arrangement": ( + "COUPLE_LIVING_WITH_OTHERS", + "COUPLE_LIVING_WITH_OTHERS", + "INDIVIDUAL_LIVING_WITH_OTHERS", + ) + }, + "MO": { + "mo_ssp_living_arrangement": "RCF_LEVEL_I", + "mo_snc_facility_base_charge": 2_000, + }, + "MT": {"mt_ssp_payment_category": "ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME"}, + "NC": {"nc_sa_care_setting": "ADULT_CARE_HOME"}, + "NE": {"ne_aabd_alternate_living_arrangement": "ASSISTED_LIVING_FACILITY"}, + "NM": {"is_in_adult_residential_care": True}, + "OH": {"oh_rss_certified": True}, + "OR": {"or_ssp_category": "COMMUNITY_CARE"}, + "SC": {"is_in_residential_care_facility": True}, + "SD": { + "sd_ssp_care_joint_entry": True, + "sd_ssp_care_certified": True, + "sd_ssp_care_setting": "ADULT_FOSTER_CARE", + "sd_ssp_approved_care_cost": 1_500, + }, + "TX": { + "is_in_medicaid_facility": True, + "ssi_lives_in_medical_treatment_facility": True, + "ssi_medicaid_pays_majority_of_care": True, + }, + "VA": {"va_ag_certified": True, "va_ag_is_joint_initial_month": True}, + "VT": { + "vt_aabd_ep_level": ("PARTIAL_34", "PARTIAL_67", "FULL"), + "vt_aabd_ep_nonspouse_essential_person": True, + }, + "WI": {"wi_ssi_e_certified": True}, +} + +# Also reach California SSP with citizens in nonmedical care, the under-60 +# Colorado program and the old Washington category for a disabled claimant +# with an ineligible spouse. +SUPPLEMENT_CASES = [(state, YEAR, "standard") for state in SUPPLEMENT_PROGRAMS] + [ + ("CA", YEAR, "ssi_ssp"), + ("CO", YEAR, "and_cs"), + ("IA", YEAR, "ia_dependent"), + ("WA", 2022, "disabled"), +] +SUPPLEMENT_SCENARIO_INPUTS = { + "ia_rcf": { + "ia_ssa_category": "RESIDENTIAL_CARE", + "ia_ssa_care_initial_partial_month": True, + "ia_ssa_rcf_received_ssi_before_entry": True, + }, + "ia_in_home": { + "ia_ssa_category": "IN_HOME_CARE", + "ia_ssa_ihhrc_monthly_cost": 400, + }, + "ia_smme": { + "ia_ssa_category": "SMME", + "ia_ssa_smme_coverage_eligible": True, + # Exercise the SMME payment rather than only its zero-income denial. + "pension_income": (26_400, 26_400, 36_000), + "ssi_countable_resources": (1_000, 1_000, 0), + }, + "ia_dependent": { + "ia_ssa_category": "DEPENDENT_PERSON", + "ia_ssa_dependent_countable_income": 0, + }, + "ssi_ssp": { + "immigration_status_str": "CITIZEN", + "ssi_qualifying_quarters_earnings": 40, + "in_out_of_home_care_facility": True, + }, + "and_cs": { + "age": (50, 52, 70), + "is_disabled": (True, True, False), + "meets_ssi_disability_criteria": (True, True, False), + }, + "disabled": { + "age": (40, 40, 70), + "is_disabled": (True, False, False), + "meets_ssi_disability_criteria": (True, False, False), + }, +} + + +def _supplement_situation( + system, + state, + pensions, + ages, + resources, + care, + third_care, + explicit, + year=YEAR, + scenario="standard", +): + names = ("claimant", "spouse", "third_adult") + situation = {"people": {}} + for index, name in enumerate(names): + situation["people"][name] = { + "age": {year: ages[index]}, + "pension_income": {year: 12 * pensions[index]}, + "ssi_countable_resources": {year: resources[index]}, + "is_tax_unit_head": {year: index == 0}, + "is_tax_unit_spouse": {year: index == 1}, + "is_tax_unit_dependent": {year: index == 2}, + } + groups = { + "household": "households", + "tax_unit": "tax_units", + "family": "families", + "spm_unit": "spm_units", + } + for singular, plural in groups.items(): + situation[plural] = {singular: {"members": list(names)}} + situation["households"]["household"]["state_code"] = {year: state} + + def set_inputs(inputs, members): + for name, value in inputs.items(): + variable = system.variables[name] + period = f"{year}-01" if variable.definition_period == "month" else year + if variable.entity.is_person: + for index, member in enumerate(names): + if member in members: + selected = value[index] if isinstance(value, tuple) else value + situation["people"][member][name] = {period: selected} + else: + entity = variable.entity.key + situation[groups[entity]][entity][name] = {period: value} + + if care: + set_inputs( + SUPPLEMENT_CARE_INPUTS.get(state, {}), names if third_care else names[:2] + ) + if state == "IA": + if care: + set_inputs( + SUPPLEMENT_SCENARIO_INPUTS.get(scenario, {}), + names if third_care else names[:2], + ) + else: + set_inputs(SUPPLEMENT_SCENARIO_INPUTS.get(scenario, {}), names) + if explicit: + situation["marital_units"] = { + "couple": {"members": list(names[:2])}, + "single": {"members": [names[2]]}, + } + return situation + + +def _supplement_amounts( + state, + pensions, + ages, + resources, + care, + third_care, + explicit, + year=YEAR, + scenario="standard", +): + # Reuse the same read-only system as the existing grids. Only simulations + # and their caches are independent. Each has three people, so omitting + # marital_units really uses the API's default unit, without mixing other + # households or states into that default group. + system = Simulation.default_tax_benefit_system_instance + simulation = Simulation( + situation=_supplement_situation( + system, + state, + pensions, + ages, + resources, + care, + third_care, + explicit, + year, + scenario, + ) + ) + return { + name: simulation.calculate( + name, + f"{year}-01" + if system.variables[name].definition_period == "month" + else year, + ) + for name in SUPPLEMENT_PROGRAMS[state] + } + + +@pytest.mark.parametrize(("state", "year", "scenario"), SUPPLEMENT_CASES) +@settings( + max_examples=2, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) +@example( + pensions=(0, 0, 0), + ages=(65, 65, 65), + resources=(0, 0, 0), + care=True, + third_care=True, + iowa_scenario="standard", +) +@example( + pensions=(0, 497, 994), + ages=(65, 64, 90), + resources=(0, 0, 2_001), + care=True, + third_care=False, + iowa_scenario="ia_rcf", +) +@example( + pensions=(20, 65, 1_491), + ages=(90, 65, 64), + resources=(2_000, 0, 3_001), + care=True, + third_care=True, + iowa_scenario="ia_in_home", +) +@example( + pensions=(994, 1_491, 20), + ages=(65, 90, 65), + resources=(3_000, 0, 0), + care=False, + third_care=False, + iowa_scenario="standard", +) +@example( + pensions=(0, 0, 0), + ages=(65, 65, 90), + resources=(7_000, 1_000, 5_000), + care=True, + third_care=True, + iowa_scenario="ia_smme", +) +@given( + pensions=st.tuples(*(st.integers(0, 1_600) for _ in range(3))), + ages=st.tuples(*(st.sampled_from((64, 65, 90)) for _ in range(3))), + resources=st.tuples(*(st.integers(0, 8_000) for _ in range(3))), + care=st.booleans(), + third_care=st.booleans(), + iowa_scenario=st.sampled_from(("standard", "ia_rcf", "ia_in_home", "ia_smme")), +) +def test_state_supplements_do_not_depend_on_omitted_marital_units( + state, year, scenario, pensions, ages, resources, care, third_care, iowa_scenario +): + """A third adult never becomes a spouse through the default group. + + YAML checks each state's independently calculated payment. This compares + two API representations of the same people and marriage, including + ineligible spouses, a third adult in a different care category, income + exclusions/deeming edges, and the individual/couple resource limits. + """ + if state == "IA" and scenario == "standard": + scenario = iowa_scenario + default = _supplement_amounts( + state, pensions, ages, resources, care, third_care, False, year, scenario + ) + explicit = _supplement_amounts( + state, pensions, ages, resources, care, third_care, True, year, scenario + ) + for name in SUPPLEMENT_PROGRAMS[state]: + np.testing.assert_allclose( + default[name], explicit[name], atol=0.01, err_msg=f"{state}: {name}" + ) + + +# These states preserve a declared marriage for at least one separated-spouse +# supplement budget. The cited YAML cases check that those budgets retain +# both spouses; this property separately checks that a household member who +# is not a spouse cannot alter them. +# CO: 9 CCR 2503-5 3.533(A)(2)(c), 3.534(B)-(C). +# MO: RSMo 208.010.1; DSS Manual 1035.005.00 (medical/nursing separation). +# OR: OAR 461-155-0630(3), hub ruling +# us-or:ssp-community-care-spouses-separate-facilities. +# SD: ARSD 67:46:04:10(6), 67:46:05:30 (joint care-entry month). +# VA: AG Manual Chapter B section 6 (first moment of the initial month). +# IA: 6-B Spouses Living Separately p.60 (prior-SSI RCF entry month). +SEPARATED_SUPPLEMENT_STATES = ("CO", "MO", "OR", "SD", "VA", "IA") +SEPARATED_PERIOD = "2026-07" + + +def _retained_state_pair_situation(): + situation = _income_situation([(0, 0, 0)]) + names = ("c0", "s0") + for name in names: + situation["people"][name].update( + { + "is_tax_unit_head": {YEAR: True}, + "is_tax_unit_spouse": {YEAR: False}, + "is_tax_unit_dependent": {YEAR: False}, + } + ) + for plural in ("tax_units", "families", "spm_units", "households"): + situation[plural] = {name: {"members": [name]} for name in names} + for household in situation["households"].values(): + household["state_code"] = {YEAR: "IA"} + return situation + + +def test_retained_state_spouse_helpers_keep_separated_marriage(): + """Exercise the helpers without calculating a state's whole grant chain.""" + simulation = Simulation(situation=_retained_state_pair_situation()) + person = simulation.get_population("people") + year = make_period(YEAR) + np.testing.assert_array_equal(state_is_married(person, year), [True, True]) + np.testing.assert_array_equal( + state_couple_total(person, year, np.array([175, 100])), [275, 275] + ) + + +def test_retained_state_spouse_helpers_exclude_cross_household_parent(): + """A parent link alone excludes a pair across households and tax units.""" + situation = _retained_state_pair_situation() + del situation["marital_units"] + situation["people"]["c0"]["person_id"] = {YEAR: 1} + situation["people"]["s0"]["person_id"] = {YEAR: 2} + situation["people"]["s0"]["parent_1_id"] = {YEAR: 1} + simulation = Simulation(situation=situation) + person = simulation.get_population("people") + year = make_period(YEAR) + np.testing.assert_array_equal(state_is_married(person, year), [False, False]) + np.testing.assert_array_equal( + state_couple_total(person, year, np.array([175, 100])), [175, 100] + ) + + +def test_retained_state_spouse_helpers_prefer_co_resident_parent_identity(): + """A co-resident parent's id cannot identify the remote spouse instead. + + Ids may repeat across households when every tax unit stays within its + household and no claiming tax unit is set. The declared marriage spans + households, but the claimant's parent link names the co-resident parent. + """ + situation = _retained_state_pair_situation() + situation["people"]["c0"].update(person_id={YEAR: 1}, parent_1_id={YEAR: 2}) + situation["people"]["s0"]["person_id"] = {YEAR: 2} + situation["people"]["parent"] = { + "age": {YEAR: 95}, + "person_id": {YEAR: 2}, + "is_tax_unit_head": {YEAR: True}, + "is_tax_unit_spouse": {YEAR: False}, + "is_tax_unit_dependent": {YEAR: False}, + } + for plural in ("tax_units", "families", "spm_units", "marital_units"): + situation[plural]["parent"] = {"members": ["parent"]} + situation["households"]["c0"]["members"].append("parent") + simulation = Simulation(situation=situation) + person = simulation.get_population("people") + year = make_period(YEAR) + np.testing.assert_array_equal(state_is_married(person, year), [True, True, False]) + np.testing.assert_array_equal( + state_couple_total(person, year, np.array([175, 100, 500])), [275, 275, 500] + ) + + +def _separated_supplement_situation( + system, + state, + pensions, + ages, + resources, + care, + joint_entry, + third_care, + third_household, + add_third, +): + situation = _supplement_situation( + system, + state, + pensions, + ages, + resources, + care, + third_care, + True, + scenario="ia_rcf" if state == "IA" else "standard", + ) + names = ("claimant", "spouse", "third_adult") + if not add_third: + del situation["people"][names[2]] + names = names[:2] + + for person in situation["people"].values(): + # Use assets as a primitive input, so SSI resource eligibility is + # derived alongside federal income, couple status and payments. + person["bank_account_assets"] = person.pop("ssi_countable_resources") + person["is_tax_unit_head"] = {YEAR: True} + person["is_tax_unit_spouse"] = {YEAR: False} + person["is_tax_unit_dependent"] = {YEAR: False} + for values in person.values(): + if PERIOD in values: + values[SEPARATED_PERIOD] = values.pop(PERIOD) + + # Copy entity inputs, then give everyone independent nonmarital budgets. + # An unrelated adult shares only the chosen household. The marriage is + # always the same explicit two-person unit spanning the two households. + for plural in ("tax_units", "families", "spm_units"): + original = next(iter(situation[plural].values())) + inputs = {name: value for name, value in original.items() if name != "members"} + situation[plural] = {name: {**inputs, "members": [name]} for name in names} + household = situation["households"]["household"] + household_inputs = { + name: value for name, value in household.items() if name != "members" + } + situation["households"] = { + name: {**household_inputs, "members": [name]} for name in names[:2] + } + if add_third: + situation["households"][names[third_household]]["members"].append(names[2]) + situation["marital_units"] = {"couple": {"members": list(names[:2])}} + if add_third: + situation["marital_units"]["single"] = {"members": [names[2]]} + + entry_input = { + "SD": "sd_ssp_care_joint_entry", + "VA": "va_ag_is_joint_initial_month", + }.get(state) + for index, name in enumerate(names): + person = situation["people"][name] + if entry_input: + person[entry_input] = { + SEPARATED_PERIOD: joint_entry and care and (index < 2 or third_care) + } + if care and (index < 2 or third_care): + if state == "SD": + person["sd_ssp_approved_care_cost"] = {SEPARATED_PERIOD: 2_000} + elif state == "OR": + # OAR 461-140-0040(5)(e)(D): inaccessible income is excluded. + # This primitive keeps the community-care payment positive + # when modeled SSI would otherwise fill the federal standard. + person["or_ssp_inaccessible_income"] = { + SEPARATED_PERIOD: (194, 94, 0)[index] + } + elif state == "IA": + person["ia_ssa_care_initial_partial_month"] = { + SEPARATED_PERIOD: joint_entry + } + return situation + + +def _assert_separated_care_budget(simulation, state, pensions, amounts): + """An independent spouse-budget check in the existing simulations. + + Removing the spouse in both representations would satisfy the unrelated + adult comparison. These generated ranges make the legally shared budget + observable: low resources, eligible ages and unequal pensions; Oregon + instead keeps modeled SSI positive for both community-care residents. + No extra simulation or parameter-tree construction is needed. + """ + if state == "MO": + # RSMo 208.010.1 retains the couple resource test during medical + # separation. The claimant is above the individual limit, while the + # combined resources remain below the couple limit. + assert np.all( + simulation.calculate("mo_ssp_resource_eligible", SEPARATED_PERIOD)[:2] + ) + elif state == "OR": + # OAR 461-155-0630(3): two SSI community-care spouses qualify even in + # separate facilities. Their own income after the documented + # inaccessible amounts is below the individual standard, so neither + # spouse has an excess to subtract from the other's payment. + own = np.asarray(pensions[:2]) + federal = np.maximum(INDIVIDUAL_FBR - np.maximum(own - GENERAL_EXCLUSION, 0), 0) + assert np.all(federal > 0) + expected = INDIVIDUAL_FBR - (own + federal - np.array([194, 94])) + np.testing.assert_allclose(amounts["or_ssp"], expected, atol=0.01) + elif state == "SD": + # ARSD 67:46:04:10(6), 67:46:06:03: both initial-month residents + # contribute half their combined income. These pensions eliminate + # SSI and stay below the entered $2,000 approved foster-care cost. + expected = 2_000 - sum(pensions[:2]) / 2 + np.testing.assert_allclose(amounts["sd_ssp"], expected, atol=0.01) + elif state == "VA": + # AG Manual Chapter B section 6: initial-month spouses share the + # combined income after one $20 exclusion. Neither receives SSI in + # these ranges, so the AG's own countable-income budget applies. + expected = (sum(pensions[:2]) - GENERAL_EXCLUSION) / 2 + np.testing.assert_allclose( + simulation.calculate("va_ag_countable_income", SEPARATED_PERIOD)[:2], + expected, + atol=0.01, + ) + elif state == "IA": + # 6-B p.60 retains the prior-SSI couple's RCF entry-month budget. + # Care does not apply the general exclusion; with current SSI zero, + # each resident counts half the combined pension income. + assert np.all( + simulation.calculate("ia_ssa_rcf_couple_budget", SEPARATED_PERIOD)[:2] + ) + np.testing.assert_allclose( + simulation.calculate("ia_ssa_care_countable_income", SEPARATED_PERIOD)[:2], + sum(pensions[:2]) / 2, + atol=0.01, + ) + else: + # 9 CCR 2503-5 3.533(A)(2)(c): Colorado divides a married client's + # $20 unearned-income disregard even when the spouses live apart. + np.testing.assert_allclose( + simulation.calculate("co_oap_own_countable_income", SEPARATED_PERIOD)[:2], + np.asarray(pensions[:2]) - GENERAL_EXCLUSION / 2, + atol=0.01, + ) + + +@pytest.mark.parametrize("state", SEPARATED_SUPPLEMENT_STATES) +@settings( + max_examples=2, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) +@example( + pensions=(0, 0, 1_491), + ages=(65, 65, 90), + resources=(7_000, 1_000, 5_000), + care=True, + joint_entry=True, + third_care=False, + third_household=0, + check_care_budget=False, +) +@example( + pensions=(20, 65, 0), + ages=(90, 65, 64), + resources=(1_000, 15_000, 0), + care=True, + joint_entry=False, + third_care=True, + third_household=1, + check_care_budget=False, +) +@example( + pensions=(1_200, 1_200, 3_000), + ages=(65, 65, 90), + resources=(0, 0, 3_001), + care=True, + joint_entry=True, + third_care=False, + third_household=0, + check_care_budget=False, +) +@example( + pensions=(1_800, 1_200, 20), + ages=(65, 90, 65), + resources=(1_600, 1_400, 2_001), + care=True, + joint_entry=True, + third_care=True, + third_household=1, + check_care_budget=False, +) +@example( + pensions=(0, 0, 994), + ages=(65, 65, 65), + resources=(1_600, 1_600, 0), + care=True, + joint_entry=True, + third_care=True, + third_household=0, + check_care_budget=False, +) +@example( + pensions=(994, 1_491, 20), + ages=(65, 64, 90), + resources=(2_000, 0, 0), + care=False, + joint_entry=False, + third_care=False, + third_household=1, + check_care_budget=False, +) +@given( + pensions=st.tuples( + st.integers(1_100, 1_400), st.integers(1_600, 1_800), st.integers(0, 3_000) + ), + ages=st.tuples( + st.sampled_from((65, 90)), + st.sampled_from((65, 90)), + st.sampled_from((64, 65, 90)), + ), + resources=st.tuples( + st.integers(0, 1_000), st.integers(0, 1_000), st.integers(0, 16_000) + ), + care=st.just(True), + joint_entry=st.just(True), + third_care=st.booleans(), + third_household=st.integers(0, 1), + check_care_budget=st.just(True), +) +def test_separated_spouses_supplements_ignore_unrelated_household_adult( + state, + pensions, + ages, + resources, + care, + joint_entry, + third_care, + third_household, + check_care_budget, +): + """A third adult cannot change either separately housed spouse's grant. + + YAML checks worked legal computations. The generated cases always enter + a care or joint-entry budget and check it independently, so losing the + spouse identically in both simulations is also detected. This comparison + needs the simulation API to add a person while preserving the explicit + marriage across two households and the spouses' independent other units. + Both simulations share only the read-only system; caches stay separate. + """ + if check_care_budget: + if state == "OR": + pensions = (pensions[0] - 1_000, pensions[1] - 1_000, pensions[2]) + elif state == "MO": + resources = (resources[0] + 6_500, resources[1], resources[2]) + system = Simulation.default_tax_benefit_system_instance + amounts = [] + for add_third in (False, True): + simulation = Simulation( + situation=_separated_supplement_situation( + system, + state, + pensions, + ages, + resources, + care, + joint_entry, + third_care, + third_household, + add_third, + ) + ) + amounts.append( + { + name: simulation.calculate( + name, + SEPARATED_PERIOD + if system.variables[name].definition_period == "month" + else YEAR, + )[:2] + for name in SUPPLEMENT_PROGRAMS[state] + } + ) + if check_care_budget: + _assert_separated_care_budget(simulation, state, pensions, amounts[-1]) + for name in SUPPLEMENT_PROGRAMS[state]: + np.testing.assert_allclose( + amounts[0][name], amounts[1][name], atol=0.01, err_msg=f"{state}: {name}" + ) + + +@pytest.mark.parametrize("state", ("MO", "OR", "SD", "VA", "IA")) +@pytest.mark.parametrize("relationship", ("parent", "dependent")) +@settings( + max_examples=2, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + pensions=st.tuples(st.integers(0, 300), st.integers(400, 600)), + resources=st.tuples(st.integers(1_501, 2_000), st.integers(1_501, 2_000)), + child_age=st.integers(65, 70), + parent_first=st.booleans(), +) +def test_separated_known_nonspouses_match_explicit_singletons( + state, relationship, pensions, resources, child_age, parent_first +): + """A default two-person unit cannot marry known nonspouses living apart. + + Parent links and dependency are tested separately, so either exclusion + being dropped is observable. Both representations have the same entry + and care facts; only their marital-unit representation differs. + """ + system = Simulation.default_tax_benefit_system_instance + default = _separated_supplement_situation( + system, + state, + (*pensions, 0), + (child_age + 30, child_age, 65), + (*resources, 0), + care=True, + joint_entry=True, + third_care=False, + third_household=0, + add_third=False, + ) + del default["marital_units"] + names = ("claimant", "spouse") + parent, child = names if parent_first else names[::-1] + default["people"][parent]["age"] = {YEAR: child_age + 30} + default["people"][child]["age"] = {YEAR: child_age} + if relationship == "parent": + # Keep independent tax units: only the cross-household parent link + # proves this pair cannot be spouses. + default["people"][parent]["person_id"] = {YEAR: 1} + default["people"][child]["person_id"] = {YEAR: 2} + default["people"][child]["parent_1_id"] = {YEAR: 1} + else: + # A dependent and the person claiming them cannot be spouses, even + # without a supplied parent link and after moving to separate homes. + default["tax_units"] = {"family": {"members": list(names)}} + default["people"][parent]["is_tax_unit_head"] = {YEAR: True} + default["people"][child]["is_tax_unit_head"] = {YEAR: False} + default["people"][child]["is_tax_unit_dependent"] = {YEAR: True} + explicit = deepcopy(default) + explicit["marital_units"] = {name: {"members": [name]} for name in names} + amounts = [] + for situation in (default, explicit): + simulation = Simulation(situation=situation) + amounts.append( + { + name: simulation.calculate(name, SEPARATED_PERIOD) + for name in SUPPLEMENT_PROGRAMS[state] + } + ) + if state == "IA": + assert not np.any( + simulation.calculate("ia_ssa_rcf_couple_budget", SEPARATED_PERIOD) + ) + for name in SUPPLEMENT_PROGRAMS[state]: + np.testing.assert_allclose( + amounts[0][name], amounts[1][name], atol=0.01, err_msg=f"{state}: {name}" + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/medicaid_ssi_couple_three_adult.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/medicaid_ssi_couple_three_adult.yaml new file mode 100644 index 00000000000..225ab9e193f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/medicaid_ssi_couple_three_adult.yaml @@ -0,0 +1,280 @@ +# Omitted marital units form one default group. Only the identified spouses +# may enter these non-MAGI Medicaid budgets; the third adult is unrelated. +- name: California excludes an unrelated adult's premiums from a couple budget. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 67 + is_tax_unit_head: true + pension_income: 36_000 + medicare_enrolled: false + spouse: + age: 67 + is_tax_unit_spouse: true + pension_income: 24_000 + medicare_enrolled: false + third: + age: 67 + pension_income: 12_000 + health_insurance_premiums: 12_000 + medicare_enrolled: false + ssi_federal_living_arrangement: + 2026-01: MEDICAL_TREATMENT_FACILITY + tax_units: + couple: + members: [head, spouse] + third: + members: [third] + families: + couple: + members: [head, spouse] + third: + members: [third] + spm_units: + couple: + members: [head, spouse] + third: + members: [third] + households: + household: + members: [head, spouse, third] + state_code: CA + output: + ssi_couple_computation_applies: + 2026-01: [true, true, false] + # (36,000 + 24,000 - 240) / 2; the third adult's 12,000 is not deducted. + medicaid_optional_senior_or_disabled_countable_income: [29_880, 29_880, 0] + +- name: Optional ABD income and assets exclude an unrelated adult and retain the couple limits. + period: 2026 + input: + people: + head: + age: 70 + is_tax_unit_head: true + medicaid_optional_senior_or_disabled_countable_income: 8_946 + ssi_countable_resources: 1_200 + spouse: + age: 68 + is_tax_unit_spouse: true + medicaid_optional_senior_or_disabled_countable_income: 8_946 + ssi_countable_resources: 1_200 + third: + age: 70 + medicaid_optional_senior_or_disabled_countable_income: 50_000 + ssi_countable_resources: 50_000 + tax_units: + couple: + members: [head, spouse] + third: + members: [third] + families: + couple: + members: [head, spouse] + third: + members: [third] + spm_units: + couple: + members: [head, spouse] + third: + members: [third] + households: + household: + members: [head, spouse, third] + state_code: WV + output: + # West Virginia's 2026 annual SSI rates: couple 17,892; individual 11,928. + medicaid_optional_senior_or_disabled_income_limit: [17_892, 17_892, 11_928] + is_optional_senior_or_disabled_income_eligible: [true, true, false] + # Couple assets 2,400 are below 3,000; the third adult's 50,000 is excluded. + is_optional_senior_or_disabled_asset_eligible: [true, true, false] + +- name: Section 209b spenddown excludes an unrelated adult's medical expenses. + period: 2026 + input: + people: + head: + age: 70 + is_tax_unit_head: true + is_optional_senior_or_disabled_income_eligible: false + medicaid_optional_senior_or_disabled_countable_income: 20_000 + medicaid_medically_needy_medical_expenses: 5_000 + medicaid_optional_senior_or_disabled_income_limit: 25_000 + spouse: + age: 68 + is_tax_unit_spouse: true + is_optional_senior_or_disabled_income_eligible: false + medicaid_optional_senior_or_disabled_countable_income: 20_000 + medicaid_medically_needy_medical_expenses: 5_000 + medicaid_optional_senior_or_disabled_income_limit: 25_000 + third: + age: 70 + is_optional_senior_or_disabled_income_eligible: false + medicaid_optional_senior_or_disabled_countable_income: 0 + medicaid_medically_needy_medical_expenses: 12_000 + medicaid_optional_senior_or_disabled_income_limit: 25_000 + tax_units: + couple: + members: [head, spouse] + third: + members: [third] + families: + couple: + members: [head, spouse] + third: + members: [third] + spm_units: + couple: + members: [head, spouse] + third: + members: [third] + households: + household: + members: [head, spouse, third] + state_code: IL + output: + # 2 x (20,000 - 5,000) = 30,000, above 25,000; subtracting the third + # adult's expenses would incorrectly bring the couple down to 18,000. + is_209b_ssi_recipient_income_eligible_for_medicaid: [false, false, true] + +- name: Medically needy income and assets exclude an unrelated adult. + period: 2026 + input: + people: + head: + age: 70 + is_tax_unit_head: true + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + is_ssi_recipient_for_medicaid: false + ssi_countable_income: 10_000 + medicaid_medically_needy_medical_expenses: 6_500 + ssi_countable_resources: 1_000 + spouse: + age: 68 + is_tax_unit_spouse: true + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + is_ssi_recipient_for_medicaid: false + ssi_countable_income: 10_000 + medicaid_medically_needy_medical_expenses: 6_500 + ssi_countable_resources: 1_000 + third: + age: 70 + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + is_ssi_recipient_for_medicaid: false + ssi_countable_income: 50_000 + ssi_countable_resources: 50_000 + tax_units: + couple: + members: [head, spouse] + third: + members: [third] + families: + couple: + members: [head, spouse] + third: + members: [third] + spm_units: + couple: + members: [head, spouse] + third: + members: [third] + households: + household: + members: [head, spouse, third] + state_code: MA + output: + # 7,000 income <= 7,800 couple limit; 2,000 assets <= 3,000. The + # unrelated adult's income and assets do not enter either comparison. + is_medically_needy_for_medicaid: [true, true, false] + +- name: Missouri spouse deeming excludes an unrelated third adult's income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_tax_unit_head: true + ssi_unearned_income: 17_400 + spouse: + age: 60 + is_tax_unit_spouse: true + ssi_earned_income: 4_800 + third: + age: 60 + ssi_earned_income: 120_000 + ssi_unearned_income: 120_000 + tax_units: + couple: + members: [head, spouse] + third: + members: [third] + families: + couple: + members: [head, spouse] + third: + members: [third] + spm_units: + couple: + members: [head, spouse] + third: + members: [third] + households: + household: + members: [head, spouse, third] + state_code: MO + output: + # The SSI spouse-status mask already excludes an unrelated singleton. + is_ssi_ineligible_spouse: [false, true, false] + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: [2_004, 0, 0] + +- name: Missouri spouse deeming excludes another couple's ineligible spouse. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_tax_unit_head: true + ssi_unearned_income: 17_400 + spouse: + age: 60 + is_tax_unit_spouse: true + ssi_earned_income: 4_800 + other_head: + age: 70 + is_tax_unit_head: true + other_spouse: + age: 60 + is_tax_unit_spouse: true + ssi_earned_income: 12_000 + ssi_unearned_income: 12_000 + tax_units: + couple: + members: [head, spouse] + other_couple: + members: [other_head, other_spouse] + families: + couple: + members: [head, spouse] + other_couple: + members: [other_head, other_spouse] + spm_units: + couple: + members: [head, spouse] + other_couple: + members: [other_head, other_spouse] + households: + household: + members: [head, spouse, other_head, other_spouse] + state_code: MO + output: + is_ssi_ineligible_spouse: [false, true, false, true] + # First couple: floor((400 - 65) / 2 + 1,450 - 20) - 1,430 = 167 + # monthly. Second: floor((1,000 - 65) / 2 + 1,000 - 20) = 1,447. + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: [2_004, 0, 17_364, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_income_deemed_from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_income_deemed_from_ineligible_spouse.yaml index a8f66e81e0d..2ec599e9540 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_income_deemed_from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_income_deemed_from_ineligible_spouse.yaml @@ -154,3 +154,43 @@ members: [person1, person2] output: ssi_income_deemed_from_ineligible_spouse: [0, 0] + +# 416.1160(d): 'Ineligible spouse means someone who lives with you as your +# husband or wife and is not eligible for SSI benefits.' Tax-unit roles do +# not decide it: here the model makes the elder head and the claimant the +# tax-unit spouse, and the claimant's husband a dependent. +# 2026 rates: $994 individual, $1,491 couple. +- name: Income of an ineligible spouse who is a tax-unit dependent is deemed + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 90 + social_security_retirement: 900 * 12 + claimant: + age: 70 + social_security_retirement: 600 * 12 + husband: + age: 60 + employment_income: 2_000 * 12 + tax_units: + tax_unit: + members: [elder, claimant, husband] + marital_units: + elder_unit: + members: [elder] + couple: + members: [claimant, husband] + households: + household: + members: [elder, claimant, husband] + state_code: FL + output: + is_ssi_ineligible_spouse: [false, false, true] + # $2,000 > $497, so income is deemed (416.1163(d)(2)). Couple countable + # income: ($600 - $20) + ($2,000 - $65) / 2 = $1,547.50; alone $580; + # deemed $967.50. $1,547.50 exceeds the $1,491 couple rate, so the + # claimant is ineligible on income. + ssi_income_deemed_from_ineligible_spouse: [0, 967.50, 0] + ssi: [114, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml index 569eef9cce1..d3bb2ad88cb 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.yaml @@ -355,14 +355,18 @@ home: members: [mother, claimant, spouse] output: - is_ssi_spousal_resource_deeming_applies: [false, true, true] - ssi_resources_deemed_from_ineligible_spouse: [0, 3001, 0] + # 416.1801(c): two aged, blind or disabled spouses living together are an + # eligible couple whatever their tax roles, so 416.1205(b) combines their + # resources through the joint claim rather than spousal deeming. + ssi_claim_is_joint: [false, true, true] + is_ssi_spousal_resource_deeming_applies: [false, false, false] + ssi_resources_deemed_from_ineligible_spouse: [0, 0, 0] meets_ssi_resource_test: [true, false, false] # Explicit marital units stand whatever tax roles the model infers: with # roles omitted the model may treat one spouse as the mother's tax spouse and -# the other as her dependent, but 416.1205(b) still combines the couple's -# resources: 0 + 3,001 > 3,000. +# the other as her dependent, but the couple is still a joint claim and +# 416.1205(b) combines their resources: 0 + 3,001 > 3,000. - name: Married adults in a parent's tax unit with roles omitted count each other's resources period: 2026-01 input: @@ -394,7 +398,8 @@ home: members: [mother, claimant, spouse] output: - ssi_resources_deemed_from_ineligible_spouse: [0, 3001, 0] + ssi_claim_is_joint: [false, true, true] + ssi_resources_deemed_from_ineligible_spouse: [0, 0, 0] meets_ssi_resource_test: [true, false, false] # 416.1202(a)(2) excludes retroactive military payments of an ineligible @@ -441,7 +446,10 @@ home: members: [mother, claimant, spouse] output: - ssi_resources_deemed_from_ineligible_spouse: [0, 2000, 2000] + # The couple is a joint claim (416.1801(c)), so each spouse's 2,000 + # counts in full in the combined test. + ssi_claim_is_joint: [false, true, true] + ssi_resources_deemed_from_ineligible_spouse: [0, 0, 0] meets_ssi_resource_test: [true, false, false] # A spouse who fails SSI's citizenship or qualified-noncitizen condition is @@ -492,3 +500,38 @@ ssi_resources_deemed_from_ineligible_spouse: [0, 1001, 0] meets_ssi_resource_test: [true, true, false] +# The same rule for a couple in the usual tax-unit head and spouse roles: +# both are aged, so their claim is joint, but the spouse is not eligible for +# SSI (416.1801(c)(1), 416.1160(d)). Resources are deemed with the +# ineligible-spouse exclusion: 3,001 - 2,000 = 1,001 <= 3,000. +- name: An immigration-ineligible aged spouse in the tax-unit spouse role keeps ineligible-spouse exclusions + period: 2026-01 + input: + people: + claimant: + age: 70 + immigration_status: CITIZEN + is_tax_unit_head: true + ssi_countable_resources: + '2026': 0 + spouse: + age: 68 + immigration_status: UNDOCUMENTED + is_tax_unit_spouse: true + ssi_countable_resources: + '2026': 3001 + ssi_resource_deeming_excluded_military_resources: 2000 + tax_units: + tax_unit: + members: [claimant, spouse] + marital_units: + couple: + members: [claimant, spouse] + households: + home: + members: [claimant, spouse] + output: + ssi_claim_is_joint: [true, true] + is_ssi_spousal_resource_deeming_applies: [true, true] + ssi_resources_deemed_from_ineligible_spouse: [1001, 0] + meets_ssi_resource_test: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/is_ssi_qualified_noncitizen.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/is_ssi_qualified_noncitizen.yaml index f958c8690f7..6322cb9a473 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/is_ssi_qualified_noncitizen.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/is_ssi_qualified_noncitizen.yaml @@ -40,6 +40,47 @@ is_disabled: true meets_ssi_disability_criteria: true immigration_status: UNDOCUMENTED + # Each person is single and files alone. Without units, the model would + # make person0 and person1 the head and spouse of one tax unit, and so a + # married couple. + tax_units: + unit0: + members: [person0] + unit1: + members: [person1] + unit2: + members: [person2] + unit3: + members: [person3] + unit4: + members: [person4] + unit5: + members: [person5] + unit6: + members: [person6] + unit7: + members: [person7] + unit8: + members: [person8] + marital_units: + unit0: + members: [person0] + unit1: + members: [person1] + unit2: + members: [person2] + unit3: + members: [person3] + unit4: + members: [person4] + unit5: + members: [person5] + unit6: + members: [person6] + unit7: + members: [person7] + unit8: + members: [person8] output: is_ssi_qualified_noncitizen: [false, true, true, true, true, true, true, true, false] is_ssi_eligible_individual: [false, true, true, true, true, true, true, true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_claim_is_joint.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_claim_is_joint.yaml index 2a93f0279ed..00448eb3134 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_claim_is_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_claim_is_joint.yaml @@ -156,3 +156,134 @@ members: [person1, person2, person3] output: ssi_claim_is_joint: [true, true, false] + +# 20 CFR 416.1801(c): an eligible spouse is a person eligible for SSI whom +# SSA considers the spouse of another person eligible for SSI and who lives +# in the same household with that person. Tax-unit roles play no part, so an +# aged couple whose tax unit also holds an older adult is a joint claim. +# 2026 rates: $994 individual, $1,491 couple; resource limits $2,000 and +# $3,000 (416.1205). +- name: Case 6, aged couple sharing a tax unit with an older adult is a joint claim. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 90 + social_security_retirement: 900 * 12 + husband: + age: 70 + social_security_retirement: 700 * 12 + bank_account_assets: 2_500 + wife: + age: 68 + social_security_retirement: 600 * 12 + tax_units: + tax_unit: + members: [elder, husband, wife] + marital_units: + elder_unit: + members: [elder] + couple: + members: [husband, wife] + households: + household: + members: [elder, husband, wife] + state_code: FL + output: + # The model makes the elder head and the husband the tax-unit spouse; + # the wife is a dependent. The couple is still an eligible couple. + ssi_claim_is_joint: [false, true, true] + # 416.1205(b): combined resources $2,500 <= $3,000. + meets_ssi_resource_test: [true, true, true] + # 416.1802(b)(1), 416.412: combined countable income $700 + $600 - $20 = + # $1,280; $1,491 - $1,280 = $211, $105.50 each. The elder is an + # individual: $994 - ($900 - $20) = $114. + ssi: [114, 105.50, 105.50] + +- name: Case 7, the same couple fails the couple resource limit together. + period: 2026-01 + input: + people: + elder: + age: 90 + husband: + age: 70 + bank_account_assets: 2_000 + wife: + age: 68 + bank_account_assets: 1_500 + tax_units: + tax_unit: + members: [elder, husband, wife] + marital_units: + elder_unit: + members: [elder] + couple: + members: [husband, wife] + households: + household: + members: [elder, husband, wife] + state_code: FL + output: + ssi_claim_is_joint: [false, true, true] + # 416.1205(b): $2,000 + $1,500 = $3,500 > $3,000. Tested one by one + # against the $2,000 individual limit both spouses would pass. + meets_ssi_resource_test: [true, false, false] + +- name: Case 8, aged couple who are both tax dependents of their son is a joint claim. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + son: + age: 45 + is_tax_unit_head: true + father: + age: 75 + is_tax_unit_dependent: true + social_security_retirement: 500 * 12 + mother: + age: 73 + is_tax_unit_dependent: true + social_security_retirement: 300 * 12 + tax_units: + tax_unit: + members: [son, father, mother] + marital_units: + son_unit: + members: [son] + couple: + members: [father, mother] + households: + household: + members: [son, father, mother] + state_code: FL + output: + ssi_claim_is_joint: [false, true, true] + # $1,491 - ($500 + $300 - $20) = $711, $355.50 each. + ssi: [0, 355.50, 355.50] + +- name: Case 9, spouses who live in different households are not a joint claim. + period: 2026-01 + input: + people: + husband: + age: 70 + wife: + age: 68 + tax_units: + tax_unit: + members: [husband, wife] + marital_units: + couple: + members: [husband, wife] + households: + home: + members: [husband] + state_code: FL + facility: + members: [wife] + state_code: FL + output: + ssi_claim_is_joint: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_if_takes_up.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_if_takes_up.yaml index bb5ab0ecb2f..8f61d6d5820 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_if_takes_up.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_if_takes_up.yaml @@ -38,3 +38,203 @@ # 2025 individual federal benefit rate, no countable income ssi_if_takes_up: 967 ssi: 0 + +# 20 CFR 416.1163(e)(2): 'Your SSI benefit under the deeming rules cannot be +# higher than it would be if deeming did not apply. Therefore, your benefit +# is the lesser of the amount computed under the rules in paragraph (d)(2) of +# this section or the amount remaining after we subtract only your own +# countable income from an individual's Federal benefit rate.' +# 2026 Federal benefit rates: $994 individual, $1,491 couple; the deeming +# threshold is their difference, $497. +- name: Case 5, deemed benefit is the lesser of the couple computation and the benefit without deeming. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + social_security_retirement: 743 * 12 + bank_account_assets: 1_500 + spouse: + age: 64 + social_security_retirement: 100 * 12 + employment_income: 400 * 12 + tax_units: + tax_unit: + members: [claimant, spouse] + marital_units: + marital_unit: + members: [claimant, spouse] + households: + household: + members: [claimant, spouse] + state_code: FL + output: + # Spouse income $100 + $400 = $500 > $497, so deeming applies + # (416.1163(d)(2)). Couple countable income: unearned $743 + $100 - $20 + # = $823; earned ($400 - $65) / 2 = $167.50; total $990.50. + # Couple computation: $1,491 - $990.50 = $500.50. + # Without deeming: $994 - ($743 - $20) = $271. + ssi_countable_income: [990.50, 0] + uncapped_ssi_without_spousal_deeming: [271, 0] + uncapped_ssi: [271, 0] + ssi: [271, 0] + +- name: Case 6, the same couple with lower income and no deeming gets more SSI than Case 5. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + social_security_retirement: 579 * 12 + bank_account_assets: 1_500 + spouse: + age: 64 + social_security_retirement: 78 * 12 + employment_income: 400 * 12 + tax_units: + tax_unit: + members: [claimant, spouse] + marital_units: + marital_unit: + members: [claimant, spouse] + households: + household: + members: [claimant, spouse] + state_code: FL + output: + # Spouse income $78 + $400 = $478 <= $497: no income is deemed + # (416.1163(d)(1)), so SSI is $994 - ($579 - $20) = $435, above the $271 + # of Case 5 although every income is lower. + ssi_income_deemed_from_ineligible_spouse: [0, 0] + ssi: [435, 0] + +- name: Case 7, the benefit without deeming takes the one-third reduction of another person's household. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + social_security_retirement: 400 * 12 + ssi_federal_living_arrangement: ANOTHER_PERSONS_HOUSEHOLD + spouse: + age: 60 + employment_income: 600 * 12 + tax_units: + tax_unit: + members: [claimant, spouse] + marital_units: + marital_unit: + members: [claimant, spouse] + households: + household: + members: [claimant, spouse] + state_code: FL + output: + # 416.1131: the individual rate is reduced by one-third, $994 x 2/3 = + # $662.67; without deeming $662.67 - ($400 - $20) = $282.67. Under + # deeming the couple countable income is $380 + ($600 - $65) / 2 = + # $647.50, which leaves more than $282.67 of the couple rate whether or + # not that rate is reduced, so the benefit is $282.67. + uncapped_ssi_without_spousal_deeming: [282.67, 0] + ssi: [282.67, 0] + +- name: Case 8, a claimant whose own income exceeds the individual rate gets no SSI under deeming. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + social_security_retirement: 1_100 * 12 + spouse: + age: 60 + employment_income: 600 * 12 + tax_units: + tax_unit: + members: [claimant, spouse] + marital_units: + marital_unit: + members: [claimant, spouse] + households: + household: + members: [claimant, spouse] + state_code: FL + output: + # Couple computation: $1,491 - ($1,080 + ($600 - $65) / 2) = $143.50. + # Without deeming: $994 - $1,080 = -$86, so the lesser is -$86 and the + # benefit is $0 (416.1163(e)(2)). Programs that read uncapped_ssi see + # the same shortfall. + uncapped_ssi_without_spousal_deeming: [-86, 0] + uncapped_ssi: [-86, 0] + ssi: [0, 0] + +# 416.1163(g) Examples 3 and 2 (1986: individual rate $336, couple rate $504, +# deeming threshold $168). The examples stop at eligibility; the benefit +# follows from (e)(2). The examples deem July income for September +# (416.1163(e)(1)); with constant income the amounts are the same. Annual +# amounts are twelve times the monthly ones. +- name: Case 9, 416.1163(g) Example 3 (Mr. Smith) pays the lesser amount under 416.1163(e)(2). + period: 1986 + absolute_error_margin: 0.01 + input: + people: + mr_smith: + age: 50 + is_ssi_disabled: true + pension_income: 100 * 12 + mrs_smith: + age: 50 + employment_income: 201 * 12 + tax_units: + tax_unit: + members: [mr_smith, mrs_smith] + marital_units: + marital_unit: + members: [mr_smith, mrs_smith] + households: + household: + members: [mr_smith, mrs_smith] + output: + # Example 3: '$80 ... leaving $68. This gives the couple total countable + # income of $148.' Couple computation $504 - $148 = $356; without + # deeming $336 - $80 = $256; the lesser is $256. + ssi_countable_income: [1_776, 0] # $148 a month + uncapped_ssi_without_spousal_deeming: [3_072, 0] # $256 a month + uncapped_ssi: [3_072, 0] # $256 a month + ssi: [3_072, 0] # $256 a month + +- name: Case 10, 416.1163(g) Example 2 (Mr. Jones) keeps the couple computation when it is lower. + period: 1986 + absolute_error_margin: 0.01 + input: + people: + mr_jones: + age: 50 + is_ssi_disabled: true + mrs_jones: + age: 48 + employment_income: 401 * 12 + pension_income: 252 * 12 + christine: + age: 10 + tax_units: + tax_unit: + members: [mr_jones, mrs_jones, christine] + marital_units: + marital_unit: + members: [mr_jones, mrs_jones] + child_unit: + members: [christine] + households: + household: + members: [mr_jones, mrs_jones, christine] + output: + # Example 2: allocation $168 for Christine, leaving $84 unearned; + # '$64 countable unearned income and $168 countable earned income ... + # ($232)'. Couple computation $504 - $232 = $272; without deeming + # $336 - $0 = $336; the lesser is $272. + ssi_countable_income: [2_784, 0, 0] # $232 a month + ssi: [3_264, 0, 0] # $272 a month diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_state_supplement_three_adult.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_state_supplement_three_adult.yaml new file mode 100644 index 00000000000..935610f75a7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/ssi_state_supplement_three_adult.yaml @@ -0,0 +1,1558 @@ +# State supplement regressions for an eligible couple sharing a household +# and tax unit with a third adult, without explicit marital units. +# Each case cites the state rule and computes its expectation independently. + +# co/ssa/oap/co_oap_three_adult.yaml +# 9 CCR 2503-5 3.533(A)(2)(c): married clients split the $20 disregard. +# 3.534(C) exempts spouses receiving SSI/OAP within the $1,032 OAP limit. +# https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76 +# SSI = (1,491 - (300 + 200 - 20)) / 2 = $505.50 each; third = $614. +# Own OAP incomes = $290/$190/$380. No spouse income is deemed. +# Monthly OAP = 1,032 - 505.50 - 290/190 = $236.50/$336.50; +# third = 1,032 - 614 - 380 = $38, raised to the $189 payment floor. +- name: CO OAP counts only the spouse in a default marital unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + husband: + age: 70 + pension_income: 3_600 + is_tax_unit_head: true + wife: + age: 70 + pension_income: 2_400 + is_tax_unit_spouse: true + third_adult: + age: 70 + pension_income: 4_800 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: CO + output: + co_oap_own_countable_income: [3_480, 2_280, 4_560] + co_oap_deemed_income: [0, 0, 0] + co_oap: [2_838, 4_038, 2_268] + +# ct/dss/ssp/third_adult.yaml +# CT DSS State Supplement page and fact sheet: $2,400 combined assets for a +# married couple; $1,600 for a single person. $1,000 + $1,000 = $2,000 passes; +# the third adult's $1,000 is tested separately. Section 17b-600 in chapter +# 319mm authorizes the program but does not specify these dollar limits. +# https://portal.ct.gov/dss/knowledge-base/articles/cash-assistance/state-supplement-cash-assistance?language=en_US +# https://portal.ct.gov/-/media/departments-and-agencies/dss/fact-sheets-and-issue-briefs/fact-sheets/agedblinddiabledfactsheet.pdf +- name: Connecticut excludes a third adult from the couple resource test + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + ssi_countable_resources: + 2026: 1000 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ssi_countable_resources: + 2026: 1000 + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + ssi_countable_resources: + 2026: 1000 + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: CT + output: + ct_ssp_resource_eligible: [true, true, true] + +# dc/dhcf/ossp/dc_ossp_three_adult.yaml +- name: DC foster-care couple amount excludes a third aged adult in the default marital unit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + dc_ossp_in_adult_foster_care: true + dc_ossp_facility_size: 30 + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dc_ossp_in_adult_foster_care: true + dc_ossp_facility_size: 30 + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + dc_ossp_in_adult_foster_care: true + dc_ossp_facility_size: 30 + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: DC + output: + ssi_claim_is_joint: [true, true, false] + dc_ossp_eligible: [true, true, true] + # DC DHCF's 2026 OSSP table, OS-A (adult foster care, at most 50 beds): + # https://dhcf.dc.gov/service/optional-state-supplement-payment-program + # $1,717 / 2 = $858.50 per spouse; the individual rate is $681. + dc_ossp_payment_amount: [858.5, 858.5, 681] + dc_ossp: [858.5, 858.5, 681] + +# de/dhss/ssp/third_adult.yaml +# POMS SI 01415.058 C.2: certified residential care pays $448 per couple and $140 per individual. With no excess income, each spouse receives $448 / 2 = $224; the third adult receives $140. +# https://secure.ssa.gov/poms.nsf/lnx/0501415058#c2 +- name: Delaware pays a couple rate without including a third resident + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: DE + de_ssp_living_arrangement: CERTIFIED_RESIDENTIAL_CARE_HOME + output: + de_ssp: [224, 224, 140] + +# fl/dcf/oss/third_adult.yaml +# DCF Appendix A-12 (January 2025), OSS Redesign: the couple provider rate is $1,885.80, or $942.90 each; the individual rate is $991.40. Both spouses occupy an ALF on the same track; the third adult does not occupy a qualifying facility. +# https://www.myflfamilies.com/sites/default/files/2024-12/Appendix%20A-12.pdf +- name: Florida tests the two spouses facility placement without a third adult + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2025: 70 + is_tax_unit_head: + 2025: true + fl_oss_program_track: REDESIGN + fl_oss_community_care_type: ALF + wife: + age: + 2025: 70 + is_tax_unit_spouse: + 2025: true + fl_oss_program_track: REDESIGN + fl_oss_community_care_type: ALF + third_adult: + age: + 2025: 70 + is_tax_unit_dependent: + 2025: true + fl_oss_program_track: REDESIGN + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2025: FL + output: + fl_oss_couple_rate_applies: [true, true, false] + fl_oss_provider_rate: [942.9, 942.9, 991.4] + +# hi/dhs/oss/third_adult.yaml +# POMS SI 01415.058 E.2: community care pays $1,995 per eligible couple, so each spouse receives $997.50. The third adult is outside a qualifying care setting and receives $0. +# https://secure.ssa.gov/poms.nsf/lnx/0501415058#e2 +- name: Hawaii compares only spouses when applying the community care couple rate + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + hi_oss_care_facility_type: COMMUNITY_CARE + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + hi_oss_care_facility_type: COMMUNITY_CARE + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: HI + output: + hi_oss_couple_rate_applies: [true, true, false] + hi_oss_payment_amount: [997.5, 997.5, 0] + +# in/fssa/ssp/third_adult.yaml +# Medicaid Policy Manual 5005.05.00: SAPN fills the $52 personal-needs allowance, capped at $22 per person. Each spouse has $30 SSI and no other budgeted income: $52 - $30 = $22 each. The third adult is not SAPN-eligible; that adult's $100 monthly income is outside the couple budget. +# https://www.in.gov/fssa/ompp/files/Medicaid_PM_5000.pdf#page=2 +- name: Indiana combines only spouses budgeted income for SAPN + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + in_ssp_sapn_eligible: true + ssi: 30 + ssi_countable_income: + 2026: 0 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + in_ssp_sapn_eligible: true + ssi: 30 + ssi_countable_income: + 2026: 0 + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + in_ssp_sapn_eligible: false + ssi: 0 + ssi_countable_income: + 2026: 1_200 + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: IN + output: + in_ssp_sapn: [22, 22, 0] + +# ky/dcbs/ssp/third_adult.yaml +# 921 KAR 2:015 Section 9(1)(c): the caretaker standard is $1,606 for an eligible couple both receiving care, or $803 per spouse, and $1,056 for an individual. No countable income reduces these standards. +# https://apps.legislature.ky.gov/law/kar/titles/921/002/015/ +- name: Kentucky recognizes a caretaker couple alongside another aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + ky_ssp_category: CARETAKER + ky_ssp_care_receivers: BOTH + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ky_ssp_category: CARETAKER + ky_ssp_care_receivers: BOTH + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + ky_ssp_category: CARETAKER + ky_ssp_care_receivers: BOTH + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: KY + output: + ky_ssp_claim_type: [COUPLE_BOTH_ELIGIBLE, COUPLE_BOTH_ELIGIBLE, INDIVIDUAL] + ky_ssp_payment_standard: [803, 803, 1056] + ky_ssp: [803, 803, 1056] + +# me/dhhs/ssp/me_ssp_three_adult.yaml +- name: Maine couple amount excludes a third aged adult in the default marital unit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: ME + output: + ssi_claim_is_joint: [true, true, false] + me_ssp_eligible: [true, true, true] + # Maine's 2026 approved state plan, maximum state supplement: + # independent living is $15/couple and $10/individual. + # https://www.medicaid.gov/medicaid/spa/downloads/ME-25-0016.pdf + # $15 / 2 = $7.50 per spouse; the third adult receives $10. + me_ssp: [7.5, 7.5, 10] + +- name: Maine couple category ignores the third adult's care arrangement. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + me_ssp_living_arrangement: ADULT_FOSTER_HOME + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: ME + output: + # The same plan pays $49/individual in adult foster care. + # https://www.medicaid.gov/medicaid/spa/downloads/ME-25-0016.pdf + # The couple remains $15 / 2 = $7.50 each; the third adult gets $49. + me_ssp: [7.5, 7.5, 49] + +# mi/mdhhs/ssp/mi_ssp_three_adult.yaml +- name: Michigan couple amount excludes a third aged adult in the default marital unit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: MI + output: + ssi_claim_is_joint: [true, true, false] + mi_ssp_couple_eligible: [true, true, false] + # Michigan RFT 248, independent living, effective January 1, 2026: + # https://mdhhs-pres-prod.michigan.gov/olmweb/ex/RF/Public/RFT/248.pdf#page=2 + # $21 / 2 = $10.50 per spouse, plus $14 for the individual = $35. + mi_ssp_person: [10.5, 10.5, 14] + mi_ssp: 35 + +# mn/dhs/msa/mn_msa_three_adult.yaml +- name: Minnesota couple income excludes a third aged adult at the same address. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + # The couple forms a separate household at the shared address. + mn_msa_living_arrangement: COUPLE_LIVING_ALONE + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + mn_msa_living_arrangement: COUPLE_LIVING_ALONE + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + mn_msa_living_arrangement: INDIVIDUAL_LIVING_ALONE + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: MN + output: + # CM 0020.21 combines only spouses' income. A separate household within + # a shared address can receive the living-alone standard (PDF pages 3-4). + # https://www.dhs.state.mn.us/main/groups/county_access/documents/pub/mndhs-073585.pdf#page=3 + # Couple: (1,582 - (1,491 - 20)) / 2 = 55.50 each. + # Individual: 1,055 - (994 - 20) = 81. + mn_msa_net_income_eligible: [true, true, true] + mn_msa_person: [55.5, 55.5, 81] + +- name: Minnesota non-SSI resource test excludes the third adult's resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + ssi: 0 + ssi_countable_resources: + 2026: 1_000 + mn_msa_living_arrangement: COUPLE_LIVING_WITH_OTHERS + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ssi: 0 + ssi_countable_resources: + 2026: 1_000 + mn_msa_living_arrangement: COUPLE_LIVING_WITH_OTHERS + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + ssi: 0 + ssi_countable_resources: + 2026: 9_000 + mn_msa_living_arrangement: INDIVIDUAL_LIVING_WITH_OTHERS + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: MN + output: + # Minn. Stat. 256P.02 Subd. 2 limits each assistance unit to $10,000: + # the couple has $2,000; the individual has $9,000. + # https://www.revisor.mn.gov/statutes/cite/256P.02#stat.256P.02.2 + mn_msa_resource_eligible: [true, true, true] + # CM 0020.21, with no countable income: 1,058 / 2 = 529 each; + # the third adult's individual living-with-others standard is 755.33. + # https://www.dhs.state.mn.us/main/groups/county_access/documents/pub/mndhs-073585.pdf#page=4 + mn_msa_person: [529, 529, 755.33] + +- name: Minnesota guardian fee cap uses only the recipient and spouse's gross income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + mn_msa_gross_income: 1_000 + mn_msa_has_guardian: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + mn_msa_gross_income: 500 + mn_msa_has_guardian: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + mn_msa_gross_income: 1_000 + mn_msa_has_guardian: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: MN + output: + # Minn. Stat. 256D.44 Subd. 5(d): min(5% of AU gross income, $100). + # https://www.revisor.mn.gov/statutes/cite/256D.44#stat.256D.44.5 + # Couple: min((1,000 + 500) * .05, 100) = 75; individual: 1,000 * .05 = 50. + mn_msa_guardian_fee: [75, 75, 50] + +# mo/dss/ssp/third_adult.yaml +# DSS Manual 1035.005.00 counts property owned by either spouse. MHABD Appendix J (July 2026) allows $12,441 per couple and $6,220.50 per individual. The couple has $7,000 + $1,000 = $8,000; the third adult has $1,000. +# https://dssmanuals.mo.gov/december-1973-eligibility-requirements/1035-000-00/1035-005-00/ +- name: Missouri identifies co-resident spouses for the SNC resource limit + period: 2026-07 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + mo_ssp_living_arrangement: RCF_LEVEL_I + ssi_countable_resources: + 2026: 7000 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + mo_ssp_living_arrangement: RCF_LEVEL_I + ssi_countable_resources: + 2026: 1000 + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + mo_ssp_living_arrangement: RCF_LEVEL_I + ssi_countable_resources: + 2026: 1000 + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: MO + output: + mo_ssp_resource_eligible: [true, true, true] + +# mt/dphhs/ssp/mt_ssp_three_adult.yaml +# POMS SI 01415.058 H.2.b: certified assisted/group/community care pays +# $193 per eligible couple and $94 per individual; $193 / 2 = $96.50. +# https://secure.ssa.gov/poms.nsf/lnx/0501415058#h2b +# No marital units: only the head and spouse form the eligible couple. +- name: MT couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: MT + output: + mt_ssp: [96.5, 96.5, 94] + +# ne/dhhs/aabd/third_adult.yaml +# 469 NAC 3-006.02B3a(1)(a) and 469-000-211: spouses living independently share the $887 two-person standard, $443.50 each with no shelter costs. The third adult in assisted living uses the $1,236 individual standard, giving $1,236 - $994 SSI = $242. +# https://dhhs.ne.gov/Documents/Title-469-Complete.pdf#page=130 +- name: Nebraska independently budgets the couple despite a third care resident + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + ne_aabd_alternate_living_arrangement: INDEPENDENT + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ne_aabd_alternate_living_arrangement: INDEPENDENT + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + ne_aabd_alternate_living_arrangement: ASSISTED_LIVING_FACILITY + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: NE + output: + ne_aabd_couple_rate_applies: [true, true, false] + ne_aabd_standard_of_need: [443.5, 443.5, 1236] + ne_aabd: [0, 0, 242] + +# nj/njdhs/ssp/nj_ssp_three_adult.yaml +# POMS SI 01415.058 J.2: independent living pays $25.35 per couple +# and $31.25 per individual. $25.35 / 2 = $12.675; federal payment +# rounding (POMS SI 02001.010) rounds each spouse up to $12.68. +# https://secure.ssa.gov/poms.nsf/lnx/0501415058#j2 +# No marital units: only the head and spouse form the eligible couple. +- name: NJ couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: NJ + output: + nj_ssp: [12.68, 12.68, 31.25] + +# POMS SI 01415.025 NY B.2 and SI 01415.058 J.1: the higher variation is +# for someone living ONLY with an ineligible spouse. A third adult rules it +# out; the ordinary individual independent-living supplement is $31.25. +# https://secure.ssa.gov/poms.nsf/lnx/0501415025NY +- name: NJ third adult rules out living only with an ineligible spouse + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 60 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: NJ + output: + nj_ssp_living_arrangement: [LIVING_ALONE_OR_WITH_OTHERS, LIVING_ALONE_OR_WITH_OTHERS, LIVING_ALONE_OR_WITH_OTHERS] + nj_ssp: [31.25, 0, 31.25] + +# nv/dwss/oss/nv_oss_three_adult.yaml +# POMS SI 01415.058 I.2: independent aged living pays +# $74.46 per couple and $36.40 per individual; $74.46 / 2 = $37.23. +# https://secure.ssa.gov/poms.nsf/lnx/0501415058#i2 +# No marital units: only the head and spouse form the eligible couple. +- name: NV couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: NV + output: + nv_oss: [37.23, 37.23, 36.4] + +# ny/otda/ssp/ny_ssp_three_adult.yaml +# 18 NYCRR 398-4.5(a)(1)-(2): an unrelated third adult means living +# with others. The official 2026 chart pays $46 per couple and $23 per +# individual; $46 / 2 = $23 for each spouse. +# https://opwdd.ny.gov/system/files/documents/2025/11/25-inf-06-attachment-1.pdf +# No marital units: only the head and spouse form the eligible couple. +- name: NY couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: NY + output: + ny_ssp_living_arrangement: [LIVING_WITH_OTHERS, LIVING_WITH_OTHERS, LIVING_WITH_OTHERS] + ny_ssp: [23, 23, 23] + +# oh/dbh/rss/third_adult.yaml +# OAC 5160:1-5-01(E)(2) combines spouses in the same RSS living arrangement. OAC 5160:1-3-03.2 applies one $20 exclusion: ($300 + $200 - $20) / 2 = $240 each; the third adult has $400 - $20 = $380. The $1,600 allowable fee plus $200 personal needs gives grants of $1,560, $1,560 and $1,420. +# https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-5-01 +- name: Ohio pools only the RSS couple income + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + oh_rss_eligible: true + ssi: 0 + ssi_unearned_income: + 2026: 3600 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + oh_rss_eligible: true + ssi: 0 + ssi_unearned_income: + 2026: 2400 + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + oh_rss_eligible: true + ssi: 0 + ssi_unearned_income: + 2026: 4800 + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: OH + output: + oh_rss_countable_income: [240, 240, 380] + oh_rss: [1560, 1560, 1420] + +# ok/dhs/ssp/ok_ssp_three_adult.yaml +- name: Oklahoma couple countable income excludes a third aged adult. + period: 2026-07 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + social_security_retirement: + 2026: 1_200 + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: OK + output: + # OKDHS Appendix C-1 Schedule VIII.A: eligible couple standard 1,571; + # individual standard 1,034; maximum $40 per recipient. + # https://oklahoma.gov/content/dam/ok/en/okdhs/documents/searchcenter/okdhsformresults/c-1.pdf#page=3 + # Couple income is SSI 1,491, so (1,571 - 1,491) / 2 = 40 each. + # Third adult: (100 - 20) + SSI (994 - 80) = 994; 1,034 - 994 = 40. + ok_ssp_countable_income: [1_491, 1_491, 994] + ok_ssp: [40, 40, 40] + +# or/odhs/ssp/third_adult.yaml +# OAR 461-155-0630(3) tops up each SSI spouse in community care to the one-person OSIPM standard, applying only the other spouses excess. OAR 461-155-0250(3) sets $994 in 2026. Both spouses are below it: $994 - $800 = $194 and $994 - $900 = $94. The unrelated third adult receives no couple payment. +# https://ch461rules.odhs.oregon.gov/rules/461-155-0630.pdf#page=2 +- name: Oregon excludes another care resident from the spouses community care calculation + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + or_ssp_category: COMMUNITY_CARE + ssi: 100 + or_ssp_countable_income: 800 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + or_ssp_category: COMMUNITY_CARE + ssi: 100 + or_ssp_countable_income: 900 + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + or_ssp_category: COMMUNITY_CARE + ssi: 100 + or_ssp_countable_income: 1500 + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: OR + output: + or_ssp: [194, 94, 0] + +# pa/dhs/ssp/pa_ssp_three_adult.yaml +# PA DHS budget schedule: independent living pays $33.30 per couple +# and $22.10 per individual; $33.30 / 2 = $16.65. +# 55 Pa. Code chapter 299 supplies the individual/couple SSI program rules. +# https://www.pa.gov/content/dam/copapwp-pagov/en/dhs/documents/docs/publications/documents/budget-information/c_259013.pdf +# https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter299/chap299toc.html +# No marital units: only the head and spouse form the eligible couple. +- name: PA couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: PA + output: + pa_ssp: [16.65, 16.65, 22.1] + +# ri/dhs/ssp/ri_ssp_three_adult.yaml +# R.I. Gen. Laws 40-6-27(a)(1)(i),(iii): living-alone state +# standards are $79.38 per couple and $39.92 per individual. +# $79.38 / 2 = $39.69; no federal one-third reduction applies. +# https://webserver.rilegislature.gov/Statutes/TITLE40/40-6/40-6-27.htm +# No marital units: only the head and spouse form the eligible couple. +- name: RI couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: RI + output: + ri_ssp: [39.69, 39.69, 39.92] + +# sd/dss/ssp/sd_ssp_three_adult.yaml +- name: South Dakota couple with only SSI ignores the third adult's other income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + social_security_retirement: + 2026: 1_200 + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: SD + output: + # POMS SI DEN01415.011 B.5.b: a couple with no income other than SSI + # receives $8 and $7. The unrelated individual's $100/month is separate. + # https://secure.ssa.gov/poms.nsf/lnx/0501415011DEN + # Listing that individual first also must not consume the $8 spouse slot. + sd_ssp_independent: [0, 8, 7] + sd_ssp: [0, 8, 7] + +- name: South Dakota joint care entry excludes the third adult's income and resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + ssi_countable_resources: + 2026: 500 + sd_ssp_care_joint_entry: true + sd_ssp_care_certified: true + sd_ssp_care_setting: ADULT_FOSTER_CARE + sd_ssp_approved_care_cost: 1_500 + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ssi_countable_resources: + 2026: 500 + sd_ssp_care_joint_entry: true + sd_ssp_care_certified: true + sd_ssp_care_setting: ADULT_FOSTER_CARE + sd_ssp_approved_care_cost: 1_500 + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + social_security_retirement: + 2026: 1_200 + ssi_countable_resources: + 2026: 3_000 + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: SD + output: + # ARSD 67:46:05:30 tests the entering couple's resources: 500 + 500 < 3,000. + # https://sdlegislature.gov/Rules/Administrative/67:46:05:30 + # ARSD 67:46:04:10(6) and 67:46:06:03 share only spouses' gross income. + # https://sdlegislature.gov/Rules/Administrative/67:46:04:10 + # Each spouse's contribution is SSI 1,491 / 2 = 745.50; + # care payment = approved charge 1,500 - 745.50 = 754.50. + sd_ssp_care: [754.5, 754.5, 0] + sd_ssp: [754.5, 754.5, 0] + +# ut/dws/ssp/ut_dws_ssp_three_adult.yaml +- name: Utah couple amount excludes a third aged adult in the default marital unit. + period: 2026-01 + absolute_error_margin: 0.001 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: UT + output: + ssi_claim_is_joint: [true, true, false] + ut_dws_ssp_eligible: [true, true, true] + # Utah DWS Eligibility Manual 205-10, effective January 1, 2026: + # https://jobs.utah.gov/infosource/eligibilitymanual/200_Program_Eligibility_Requirements/205-10_State_Supplemental_Payments_to_SSI_Recipients_-_General_Information.htm + # Own-household couple $5.75 / 2 = $2.875 each; individual $3.91. + ut_dws_ssp: [2.875, 2.875, 3.91] + ut_ssp: [2.875, 2.875, 3.91] + +- name: Utah household-of-another couple amount excludes the third aged adult. + period: 2026-01 + absolute_error_margin: 0.001 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + ssi_lives_in_another_persons_household: true + ssi_receives_shelter_from_others_in_household: true + ssi_others_pay_all_meals: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ssi_lives_in_another_persons_household: true + ssi_receives_shelter_from_others_in_household: true + ssi_others_pay_all_meals: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + ssi_lives_in_another_persons_household: true + ssi_receives_shelter_from_others_in_household: true + ssi_others_pay_all_meals: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: UT + output: + # The same 205-10 table pays $12.19 to a couple in another's household. + # https://jobs.utah.gov/infosource/eligibilitymanual/200_Program_Eligibility_Requirements/205-10_State_Supplemental_Payments_to_SSI_Recipients_-_General_Information.htm + # $12.19 / 2 = $6.095 per spouse; individual $3.91. + ut_dws_ssp: [6.095, 6.095, 3.91] + ut_ssp: [6.095, 6.095, 3.91] + +# va/dars/ag/va_ag_three_adult.yaml +- name: Virginia initial-month couple excludes the third adult's income and resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + social_security_retirement: + 2026: 24_000 + ssi_countable_resources: + 2026: 1_000 + va_ag_certified: true + va_ag_is_joint_initial_month: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + ssi_countable_resources: + 2026: 1_000 + va_ag_certified: true + va_ag_is_joint_initial_month: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + social_security_retirement: + 2026: 24_000 + ssi_countable_resources: + 2026: 2_000 + va_ag_certified: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: VA + output: + # AG Manual Chapter B section 6: the initial-month AU contains only the + # applicant and spouse; Chapter D uses SSI income exclusions. + # https://townhall.virginia.gov/l/GetFile.cfm?File=C:%5CTownHall%5Cdocroot%5CGuidanceDocs%5C262%5CGDoc_DARS_5346_v2.pdf#page=34 + # Couple resources 1,000 + 1,000 = 2,000 <= 3,000; individual 2,000 <= 2,000. + # Couple: (2,000 - 20) / 2 = 990 each; individual: 2,000 - 20 = 1,980. + va_ag_eligible: [true, true, true] + va_ag_countable_income: [990, 990, 1_980] + # January 2026 rate letter: care 2,130 plus personal needs 87 = 2,217. + # https://www.dss.virginia.gov/media/vdss/licensing/documents/assisted-living-facilities/additional-resources/ARC2026_ag_rate_letter.pdf + # Grants: 2,217 - 990 = 1,227 each; third adult 2,217 - 1,980 = 237. + va_ag: [1_227, 1_227, 237] + +# vt/dcf/aabd/vt_aabd_three_adult.yaml +# POMS SI 01415.058 M.2: independent living pays $105.80 per couple +# and $55.68 per individual; $105.80 / 2 = $52.90. +# https://secure.ssa.gov/poms.nsf/lnx/0501415058#m2 +# No marital units: only the head and spouse form the eligible couple. +- name: VT couple rate excludes a third aged adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: VT + output: + vt_aabd: [52.9, 52.9, 55.68] + +# Vermont AABD rules 2780(A), 2783.1, 2784(C), 2786: one EP grant per +# eligible couple, using the highest level certified on either spouse. +# https://outside.vermont.gov/dept/DCF/Shared%20Documents/ESD/Rules/2700-AABD.pdf#page=44 +# Couple maximum = 1,491 + 105.80 + 183.84 = $1,780.64. With no own +# income, SSI + federal AABD = 1,491 + 105.80 = $1,596.80. EP is +# floor(1,780.64 - 1,596.80) / 2 = $91.50 each. Uncertified third gets $0. +- name: VT essential-person certification and income exclude the third adult + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 70 + is_tax_unit_head: + 2026: true + vt_aabd_ep_level: FULL + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + third_adult: + age: + 2026: 70 + is_tax_unit_dependent: + 2026: true + pension_income: + 2026: 12_000 + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: + 2026: VT + output: + vt_aabd_ep_is_spouse: [false, false, false] + vt_aabd_ep: [91.5, 91.5, 0] + +# wa/dshs/ssp/wa_ssp_three_adult.yaml +- name: Washington historical ineligible-spouse category excludes a third aged adult. + period: 2022-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2022: 40 + is_tax_unit_head: + 2022: true + meets_ssi_disability_criteria: + 2022: true + spouse: + age: + 2022: 40 + is_tax_unit_spouse: + 2022: true + dependent: + age: + 2022: 68 + is_tax_unit_dependent: + 2022: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2022: WA + output: + # WAC 388-474-0012(2)(b) includes an SSI recipient with an ineligible spouse. + # https://app.leg.wa.gov/wac/default.aspx?cite=388-474-0012 + # WSR 20-23-053 set $38.25 for that category and for aged recipients. + # https://lawfilesext.leg.wa.gov/law/wsr/2020/23/20-23-053.htm + # Head qualifies through the actual spouse; the third adult through age. + wa_ssp_payment_category: [STANDARD, NONE, STANDARD] + wa_ssp: [38.25, 0, 38.25] + +# wi/dhs/ssp/wi_ssp_three_adult.yaml +- name: Wisconsin couple amount excludes a third aged adult in the default marital unit. + period: 2026-05 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: WI + output: + ssi_claim_is_joint: [true, true, false] + wi_ssp_eligible: [true, true, true] + # Wisconsin DMS Operations Memo 26-12, effective May 1, 2026: + # https://www.dhs.wisconsin.gov/dms/memos/ops/dms-ops-2026-12.pdf#page=2 + # Eligible couple $145.26 / 2 = $72.63 each; individual $92.16. + wi_ssp_regular: [72.63, 72.63, 92.16] + wi_ssp: [72.63, 72.63, 92.16] + +- name: Wisconsin SSI-E couple amount excludes the third certified adult. + period: 2026-05 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + wi_ssi_e_certified: true + spouse: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + wi_ssi_e_certified: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + wi_ssi_e_certified: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: WI + output: + # The memo's SSI-E rates are $379.90/couple and $105.59/individual. + # https://www.dhs.wisconsin.gov/dms/memos/ops/dms-ops-2026-12.pdf#page=2 + # $379.90 / 2 = $189.95 each; plus regular $72.63 = $262.58 each. + # Third adult: $105.59 + $92.16 = $197.75. + wi_ssi_e: [189.95, 189.95, 105.59] + wi_ssp: [262.58, 262.58, 197.75] + +- name: Wisconsin ineligible-spouse rate applies only to the actual eligible spouse. + period: 2026-05 + absolute_error_margin: 0.01 + input: + people: + head: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + spouse: + age: + 2026: 40 + is_tax_unit_spouse: + 2026: true + dependent: + age: + 2026: 68 + is_tax_unit_dependent: + 2026: true + tax_units: + unit: + members: [head, spouse, dependent] + spm_units: + unit: + members: [head, spouse, dependent] + households: + home: + members: [head, spouse, dependent] + state_code: + 2026: WI + output: + is_ssi_ineligible_spouse: [false, true, false] + wi_ssp_eligible: [true, false, true] + # The memo pays $143.47 to an eligible person with an ineligible spouse. + # https://www.dhs.wisconsin.gov/dms/memos/ops/dms-ops-2026-12.pdf#page=2 + # The third adult has no spouse and keeps the $92.16 individual amount. + wi_ssp_regular: [143.47, 0, 92.16] + wi_ssp: [143.47, 0, 92.16] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/ssp/ak_ssp.yaml b/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/ssp/ak_ssp.yaml index eea223b983a..2d5e660c27c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/ssp/ak_ssp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/ssp/ak_ssp.yaml @@ -117,3 +117,33 @@ # marital_unit.sum = 3_168 + 3_168 = 6_336 # Each gets 6_336 / 2 = 3_168 ak_ssp: [3_168, 3_168] + +# A situation that omits marital units puts the three adults in one marital +# unit. The husband and wife are the SSI joint claim (the tax-unit head and +# spouse) and split the $528 independent-living couple standard; the third +# adult gets the $362 individual standard, the same as with explicit +# marital units. +- name: Case 9, married couple and a third aged adult without marital units. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: 70 + is_tax_unit_head: true + wife: + age: 70 + is_tax_unit_spouse: true + third_adult: + age: 70 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: AK + output: + ak_ssp_claim_type: [COUPLE_BOTH_ELIGIBLE, COUPLE_BOTH_ELIGIBLE, INDIVIDUAL] + ak_ssp: [264, 264, 362] diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/dhr/ssp/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/dhr/ssp/integration.yaml index 84e607ca01e..001de27f8bc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/dhr/ssp/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/dhr/ssp/integration.yaml @@ -513,3 +513,38 @@ al_ssp_payment_category: FOSTER_CARE al_ssp_eligible: [true] al_ssp: [110] + +- name: Case 18, no supplement when spousal deeming leaves no federal SSI. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + social_security_retirement: 1_100 * 12 + al_ssp_payment_category: IHC_LEVEL_A + al_receives_elderly_disabled_medicaid_waiver: true + al_meets_snf_criteria: true + spouse: + age: 60 + employment_income: 600 * 12 + tax_units: + tax_unit: + members: [claimant, spouse] + marital_units: + marital_unit: + members: [claimant, spouse] + spm_units: + spm_unit: + members: [claimant, spouse] + households: + household: + members: [claimant, spouse] + state_code: AL + output: + # 20 CFR 416.1163(e)(2): the couple computation leaves $143.50, but the + # claimant's own countable income ($1,080) exceeds the $994 individual + # rate, so federal SSI is $0 and the SSI-receipt condition fails. + ssi: [0, 0] + al_ssp_eligible: [false, false] + al_ssp: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/ca_capi_three_adult.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/ca_capi_three_adult.yaml new file mode 100644 index 00000000000..38e5b56a930 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/ca_capi_three_adult.yaml @@ -0,0 +1,260 @@ +# Marital units are deliberately omitted. Tax roles identify the actual +# spouses, and the unrelated adult has separate tax and SPM units. +# MPP 49-050.12/.13 and 49-055.32/.4 apply only the actual couple's standard, +# income and SSI/SSP payments. MPP 49-050.26 separates a facility spouse. +# https://www.cdss.ca.gov/Portals/9/CAPI/CAPI_Regulations-Accessible.pdf + +- name: CAPI ignores an unrelated adult's facility residence and payment-standard share. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 72 + is_tax_unit_head: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + spouse: + age: 68 + is_tax_unit_spouse: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + third: + age: 40 + ca_in_medical_care_facility: true + tax_units: + couple: + members: [head, spouse] + other: + members: [third] + spm_units: + couple: + members: [head, spouse] + other: + members: [third] + households: + home: + members: [head, spouse, third] + state_code: CA + output: + # The unrelated facility input does not separate these spouses. Only + # their two shares of the aged couple standard count: 2,098.83 / 2. + ssi_couple_computation_applies: [true, true, false] + ca_capi_payment_standard: [1_049.415, 1_049.415, 0] + ca_capi_person: [1_049.415, 1_049.415, 0] + +- name: CAPI excludes an unrelated blind recipient from the couple's category count. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_tax_unit_head: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + spouse: + age: 60 + is_blind: true + is_tax_unit_spouse: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + third: + age: 70 + is_blind: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + tax_units: + couple: + members: [head, spouse] + other: + members: [third] + spm_units: + couple: + members: [head, spouse] + other: + members: [third] + households: + home: + members: [head, spouse, third] + state_code: CA + living_arrangements_allow_for_food_preparation: false + output: + # One blind spouse: 2,238.44 / 2 = 1,119.22. Only one actual spouse is + # aged or disabled, so the couple food allowance does not apply. The + # unrelated blind, aged recipient takes the higher individual aged + # no-cooking standard: 1,233.94 + 128.87 = 1,362.81. + ca_capi_payment_standard: [1_119.22, 1_119.22, 1_362.81] + ca_capi_person: [1_119.22, 1_119.22, 1_362.81] + +- name: CAPI excludes an unrelated CAPI recipient from a mixed couple's membership count. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 72 + is_tax_unit_head: true + ssi: 994 + ssi_countable_income: 0 + spouse: + age: 68 + is_tax_unit_spouse: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + pension_income: 5_040 + ssi_countable_income: 4_800 + third: + age: 70 + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + tax_units: + couple: + members: [head, spouse] + other: + members: [third] + spm_units: + couple: + members: [head, spouse] + ca_state_supplement: 239.94 + other: + members: [third] + households: + home: + members: [head, spouse, third] + state_code: CA + output: + # The CAPI spouse gets the whole mixed-couple standard, then subtracts + # the pair's income and citizen spouse's SSI/SSP: + # 2,098.83 - 400 - 994 - 239.94 = 464.89. + ca_capi_payment_standard: [0, 2_098.83, 1_233.94] + ca_capi_person: [0, 464.89, 1_233.94] + +- name: CAPI mixed-couple income excludes an unrelated adult's SSI payment. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 72 + is_tax_unit_head: true + ssi: 994 + ca_state_supplement_eligible_person: true + ssi_countable_income: 0 + spouse: + age: 68 + is_tax_unit_spouse: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + pension_income: 5_040 + ssi_countable_income: 4_800 + ca_capi_payment_standard: 2_098.83 + third: + age: 70 + ssi: 500 + ssi_countable_income: 0 + ca_state_supplement_eligible_person: false + tax_units: + couple: + members: [head, spouse] + other: + members: [third] + spm_units: + couple: + members: [head, spouse] + ca_state_supplement: 239.94 + other: + members: [third] + households: + home: + members: [head, spouse, third] + state_code: CA + output: + # Hold the standard and other income fixed to isolate the SSI reader. + ca_capi_person: [0, 464.89, 0] + +- name: CAPI mixed-couple income excludes an unrelated adult's state-supplement eligibility. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 72 + is_tax_unit_head: true + ssi: 994 + ca_state_supplement_eligible_person: true + ssi_countable_income: 0 + spouse: + age: 68 + is_tax_unit_spouse: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + pension_income: 5_040 + ssi_countable_income: 4_800 + ca_capi_payment_standard: 2_098.83 + third: + age: 70 + ssi: 0 + ssi_countable_income: 0 + ca_state_supplement_eligible_person: true + tax_units: + couple: + members: [head, spouse] + other: + members: [third] + spm_units: + couple: + members: [head, spouse] + ca_state_supplement: 239.94 + other: + members: [third] + ca_state_supplement: 200 + households: + home: + members: [head, spouse, third] + state_code: CA + output: + # Only the actual spouse's eligible share of the pair's SSP counts. + ca_capi_person: [0, 464.89, 0] + +- name: CAPI mixed-couple income excludes an unrelated adult's countable income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + head: + age: 72 + is_tax_unit_head: true + ssi: 994 + ca_state_supplement_eligible_person: true + ssi_countable_income: 0 + spouse: + age: 68 + is_tax_unit_spouse: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 39 + pension_income: 5_040 + ssi_countable_income: 4_800 + ca_capi_payment_standard: 2_098.83 + third: + age: 70 + ssi: 0 + ssi_countable_income: 6_000 + ca_state_supplement_eligible_person: false + tax_units: + couple: + members: [head, spouse] + other: + members: [third] + spm_units: + couple: + members: [head, spouse] + ca_state_supplement: 239.94 + other: + members: [third] + households: + home: + members: [head, spouse, third] + state_code: CA + output: + ca_capi_person: [0, 464.89, 0] 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 index 470e7e55d0d..89de3a010ab 100644 --- 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 @@ -400,6 +400,8 @@ members: [spouse, child] state_code: CO output: + # 3.533(A)(2)(c): the declared spouses still split the $20 disregard. + co_oap_own_countable_income: [0, 23_880, 0] co_oap_deemed_income: [5_652, 0, 0] co_oap: [6_732, 0, 0] @@ -433,6 +435,8 @@ members: [spouse] state_code: NY output: + # (1,000 - 65 - 10) / 2 * 12 = $5,550 for the spouse. + co_oap_own_countable_income: [0, 5_550] co_oap_deeming_exempt: [false, true] co_oap_deemed_income: [0, 0] co_oap: [12_384, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa.yaml index 87e06dc698d..a550e32a47d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa.yaml @@ -1677,6 +1677,7 @@ # Both spouses enter on January 10: January 10 through 31. ia_ssa_care_partial_month_days: 22 ia_ssa_care_initial_partial_month: true + ia_ssa_rcf_shares_room_with_spouse: true person2: age: 2026: 70 @@ -3153,7 +3154,7 @@ ia_ssa_smme_countable_income: [2_247.5, 457.5] ia_ssa: [1, 0] -- name: Case 101, without marital units an SMME applicant working above the SGA level in a household of three adults is budgeted alone. +- name: Case 101, an unmarried SMME applicant working above the SGA level in a household of three adults is budgeted alone. period: 2026-04 absolute_error_margin: 0.01 input: @@ -3177,6 +3178,13 @@ person3: age: 2026: 30 + marital_units: + first: + members: [person1] + second: + members: [person2] + third: + members: [person3] tax_units: tax_unit: members: [person1, person2, person3] @@ -3189,8 +3197,9 @@ state_code: 2026: IA output: - # With no marital units, the default unit holds all three people, so - # ia_ssa_has_spouse is false and each member is budgeted alone: + # Explicit single-person marital units establish that these adults are + # unmarried, even though two adults receive inferred tax head/spouse roles. + # Each member is budgeted alone: # (3,277 - 20 - 65) / 2 = 1,596, at least the one-person 1,596. If # person2's 520 pension were deemed it would be 2,106, below the two-person # 2,164. person2's own income is 520 - 20 = 500. @@ -3711,3 +3720,497 @@ ssi: [0, 29, 0, 0, 0] ia_ssa_smme_countable_income: [2_207.5, 2_207.5, 0, 0, 0] ia_ssa: [1, 0, 0, 0, 0] + +- name: Case 111, prior-SSI spouses entering separate facilities retain only the entry-month couple budget. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + social_security_retirement: 14_400 + ssi_countable_resources: 1_000 + ia_ssa_category: + 2026-01: RESIDENTIAL_CARE + 2026-02: RESIDENTIAL_CARE + ia_ssa_care_partial_month_days: + 2026-01: 22 + ia_ssa_care_initial_partial_month: + 2026-01: true + ia_ssa_rcf_received_ssi_before_entry: + 2026-01: true + person2: + age: 70 + social_security_retirement: 20_400 + ssi_countable_resources: 1_500 + ia_ssa_category: + 2026-01: RESIDENTIAL_CARE + 2026-02: RESIDENTIAL_CARE + ia_ssa_care_partial_month_days: + 2026-01: 22 + ia_ssa_care_initial_partial_month: + 2026-01: true + marital_units: + couple: + members: [person1, person2] + tax_units: + first: + members: [person1] + second: + members: [person2] + households: + facility1: + members: [person1] + state_code: IA + facility2: + members: [person2] + state_code: IA + output: + # 6-B Spouses Living Separately retains a couple in the prior-SSI entry + # month even across facilities. (1,200 + 1,700) / 2 = 1,450 care income; + # each gets half the couple home allowance, 745.50. Participation is + # 1,450 - 130 - 745.50 = 574.50, against 22 * 38.47 = 846.34. + # February uses each resident's income and 28 * 38.47 = 1,077.16. + ia_ssa_has_spouse: + 2026-01: [false, false] + ia_ssa_rcf_couple_budget: + 2026-01: [true, true] + 2026-02: [false, false] + ia_ssa_eligible: + 2026-01: [true, true] + ia_ssa_care_countable_income: + 2026-01: [1_450, 1_450] + 2026-02: [1_200, 1_700] + ia_ssa: + 2026-01: [271.84, 271.84] + 2026-02: [7.16, 0] + +- name: Case 112, current individual SSI does not establish prior SSI for spouses entering separate facilities. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + person2: + age: 70 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + marital_units: + couple: + members: [person1, person2] + tax_units: + first: + members: [person1] + second: + members: [person2] + households: + facility1: + members: [person1] + state_code: IA + facility2: + members: [person2] + state_code: IA + output: + ssi: [994, 994] + ia_ssa_rcf_couple_budget: [false, false] + ia_ssa_care_countable_income: [994, 994] + ia_ssa: [1_192.57, 1_192.57] + +- name: Case 113, a third adult does not enter the family-life home couple budget in a default marital unit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + ia_ssa_category: FAMILY_LIFE_HOME + ssi_countable_resources: 1_000 + person2: + age: 70 + ia_ssa_category: FAMILY_LIFE_HOME + ssi_countable_resources: 1_000 + person3: + age: 70 + is_blind: true + social_security_retirement: 120_000 + ia_ssa_category: FAMILY_LIFE_HOME + ia_ssa_dependent_countable_income: 10_000 + ssi_countable_resources: 100_000 + tax_units: + couple: + members: [person1, person2] + other: + members: [person3] + households: + home: + members: [person1, person2, person3] + state_code: IA + output: + # The actual spouses share the $801 federal supplement and $20 top-up; + # the third adult's income, blindness and resources do not enter theirs. + ia_ssa_has_spouse: [true, true, false] + ia_ssa_eligible: [true, true, false] + ia_ssa_care_countable_income: [745.5, 745.5, 10_000] + ia_ssa_federal: [400.5, 400.5, 0] + ia_ssa: [410.5, 410.5, 0] + +- name: Case 114, dependent-person certification and dependent income use only the actual spouses. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + ia_ssa_category: DEPENDENT_PERSON + ia_ssa_dependent_countable_income: 100 + person2: + age: 70 + ia_ssa_dependent_countable_income: 200 + person3: + age: 70 + ia_ssa_category: DEPENDENT_PERSON + ia_ssa_dependent_countable_income: 10_000 + ssi_countable_resources: 100_000 + tax_units: + couple: + members: [person1, person2] + other: + members: [person3] + households: + home: + members: [person1, person2, person3] + state_code: IA + output: + # The certified spouse establishes the couple category. Its dependent + # income is max(100, 200), not the unrelated adult's 10,000. + ia_ssa_federal: [259, 259, 0] + ia_ssa: [259, 259, 0] + +- name: Case 115, in-home care pools only the actual spouse's income costs and deductions. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + social_security_retirement: 18_000 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + ia_ssa_care_additional_deductions: 100 + person2: + age: 70 + social_security_retirement: 6_000 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + ia_ssa_care_additional_deductions: 50 + person3: + age: 70 + social_security_retirement: 120_000 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + ia_ssa_care_additional_deductions: 10_000 + tax_units: + couple: + members: [person1, person2] + other: + members: [person3] + households: + home: + members: [person1, person2, person3] + state_code: IA + output: + # Couple participation = 2,000 - 1,491 - 100 - 50 = 359; approved care + # costs total 800, leaving 441 split equally. The third adult is alone. + ia_ssa_care_countable_income: [2_000, 2_000, 10_000] + ia_ssa_ihhrc_participation: [359, 359, 0] + ia_ssa_ihhrc_amount: [220.5, 220.5, 400] + ia_ssa: [220.5, 220.5, 400] + +- name: Case 116, an Iowa couple with separate tax units still pools in-home care income and resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + social_security_retirement: 18_000 + ssi_countable_resources: 1_000 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + person2: + age: 70 + social_security_retirement: 6_000 + ssi_countable_resources: 1_000 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + marital_units: + couple: + members: [person1, person2] + tax_units: + first: + members: [person1] + second: + members: [person2] + households: + home: + members: [person1, person2] + state_code: IA + output: + is_tax_unit_spouse: [false, false] + ia_ssa_has_spouse: [true, true] + ia_ssa_eligible: [true, true] + ia_ssa_care_countable_income: [2_000, 2_000] + ia_ssa_ihhrc_participation: [509, 509] + ia_ssa: [145.5, 145.5] + +- name: Case 117, an unrelated adult in a medical facility does not stop Iowa SMME spouse income pooling. + period: 2026-04 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 50 + meets_ssi_disability_criteria: + 2026: true + employment_income: + 2026: 30_000 + ia_ssa_category: SMME + ia_ssa_smme_coverage_eligible: true + person2: + age: + 2026: 70 + social_security_retirement: + 2026: 16_800 + ia_ssa_category: SMME + ia_ssa_smme_coverage_eligible: true + person3: + age: + 2026: 70 + social_security_retirement: + 2026: 120_000 + ssi_lives_in_medical_treatment_facility: true + ia_ssa_category: SMME + ia_ssa_smme_coverage_eligible: true + tax_units: + couple: + members: [person1, person2] + other: + members: [person3] + households: + home: + members: [person1, person2, person3] + state_code: + 2026: IA + output: + # SMME combines 1,400 - 20 + (2,500 - 65) / 2 = 2,597.50. The unrelated + # adult's medical stay and income neither stop deeming nor enter the sum. + ia_ssa_smme_spousal_deeming_applies: [true, false, false] + ia_ssa_smme_countable_income: [2_597.5, 2_597.5, 9_980] + ia_ssa: [1, 1, 1] + +- name: Case 118, retained low-income residential-care entry uses couple SSI despite separate current facilities. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + social_security_retirement: 2_100 + ia_ssa_category: + 2026-01: RESIDENTIAL_CARE + 2026-02: RESIDENTIAL_CARE + ia_ssa_care_partial_month_days: + 2026-01: 22 + ia_ssa_care_initial_partial_month: + 2026-01: true + ia_ssa_rcf_received_ssi_before_entry: + 2026-01: true + person2: + age: 70 + social_security_retirement: 1_200 + ia_ssa_category: + 2026-01: RESIDENTIAL_CARE + 2026-02: RESIDENTIAL_CARE + ia_ssa_care_partial_month_days: + 2026-01: 22 + ia_ssa_care_initial_partial_month: + 2026-01: true + marital_units: + couple: + members: [person1, person2] + tax_units: + first: + members: [person1] + second: + members: [person2] + households: + facility1: + members: [person1] + state_code: IA + facility2: + members: [person2] + state_code: IA + output: + # https://hhs.iowa.gov/media/3987/download?inline=#page=69 + # Manual 6-B PDF p.69: the entry budget retains couple SSI of + # 1,491 - (175 + 100 - 20) = 1,236, so care income is + # (175 + 100 + 1,236) / 2 = 755.50. The half-couple home allowance + # 745.50 plus personal needs 130 leaves no client participation. + # Federal SSI's static current households yield individual amounts; + # Iowa's entry budget must not sum those amounts as couple SSI. + ssi: + 2026-01: [839, 914] + ia_ssa_rcf_couple_budget: + 2026-01: [true, true] + 2026-02: [false, false] + ia_ssa_care_countable_income: + 2026-01: [755.5, 755.5] + 2026-02: [1_014, 1_014] + ia_ssa: + 2026-01: [846.34, 846.34] + 2026-02: [193.16, 193.16] + +- name: Case 119, prior SSI does not make a co-resident parent and dependent adult child an RCF couple. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 70 + person_id: + 2026: 1 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + ia_ssa_rcf_received_ssi_before_entry: true + person2: + age: + 2026: 66 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + is_tax_unit_dependent: + 2026: true + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + tax_units: + family: + members: [person1, person2] + households: + facility: + members: [person1, person2] + state_code: IA + output: + # Both adults meet the age condition and one received SSI before entry, + # but the default two-person marital unit does not establish marriage. + # SSI rejects this parent/dependent-child pair; sharing one household + # cannot activate Iowa's exception for separated spouses. + ia_ssa_has_spouse: [false, false] + ssi_claim_is_joint: [false, false] + ia_ssa_rcf_couple_budget: [false, false] + ia_ssa_care_countable_income: [994, 994] + ia_ssa: [1_192.57, 1_192.57] + +- name: Case 120, separated parent and dependent adult child retain individual RCF budgets with omitted marital units. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 96 + person_id: + 2026: 1 + ssi_countable_resources: + 2026: 1_800 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + ia_ssa_rcf_received_ssi_before_entry: true + child: + age: + 2026: 66 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + is_tax_unit_dependent: + 2026: true + ssi_countable_resources: + 2026: 1_800 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + tax_units: + family: + members: [parent, child] + households: + first_facility: + members: [parent] + state_code: IA + second_facility: + members: [child] + state_code: IA + output: + # A named parent remains a parent across households. Neither person's + # resources enter the other's individual $2,000 resource test. + ia_ssa_has_spouse: [false, false] + ssi_claim_is_joint: [false, false] + ia_ssa_rcf_couple_budget: [false, false] + ia_ssa_eligible: [true, true] + ia_ssa_care_countable_income: [994, 994] + ia_ssa: [1_192.57, 1_192.57] + +- name: Case 121, explicit singleton marital units give the same separated parent and child RCF payments. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: + 2026: 96 + person_id: + 2026: 1 + ssi_countable_resources: + 2026: 1_800 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + ia_ssa_rcf_received_ssi_before_entry: true + child: + age: + 2026: 66 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + is_tax_unit_dependent: + 2026: true + ssi_countable_resources: + 2026: 1_800 + ia_ssa_category: RESIDENTIAL_CARE + ia_ssa_care_initial_partial_month: true + marital_units: + parent: + members: [parent] + child: + members: [child] + tax_units: + family: + members: [parent, child] + households: + first_facility: + members: [parent] + state_code: IA + second_facility: + members: [child] + state_code: IA + output: + ia_ssa_has_spouse: [false, false] + ssi_claim_is_joint: [false, false] + ia_ssa_rcf_couple_budget: [false, false] + ia_ssa_eligible: [true, true] + ia_ssa_care_countable_income: [994, 994] + ia_ssa: [1_192.57, 1_192.57] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.yaml index 568acb1b1ac..a0d9f8aa916 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.yaml @@ -145,15 +145,20 @@ - name: Case 7, SSI-eligible minor with SSI-eligible adult spouse — minor fails §4(1)(c) age test. period: 2026-01 + # Explicit roles avoid inferring the minor spouse as a tax dependent. input: people: person1: age: 50 + is_tax_unit_head: + 2026: true is_ssi_aged_blind_disabled: true meets_ssi_resource_test: true ky_ssp_category: CARETAKER person2: age: 17 + is_tax_unit_spouse: + 2026: true is_ssi_aged_blind_disabled: true meets_ssi_resource_test: true ky_ssp_category: CARETAKER diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/dta/ssp/ma_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/dta/ssp/ma_state_supplement.yaml index 37a46e30513..7ad97b775b5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/dta/ssp/ma_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/dta/ssp/ma_state_supplement.yaml @@ -19,3 +19,33 @@ meets_ssi_resource_test: false output: ma_state_supplement: 0 + +# A situation that omits marital units puts the three adults in one marital +# unit. The husband and wife are the SSI joint claim (the tax-unit head and +# spouse); the third adult is an individual. 106 CMR 327.330, full cost of +# living, aged: $100.86 each for a couple and $128.82 for an individual, +# the same as with explicit marital units. +- name: Married couple and a third aged adult without marital units. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: 70 + is_tax_unit_head: true + wife: + age: 70 + is_tax_unit_spouse: true + third_adult: + age: 70 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [husband, wife, third_adult] + households: + household: + members: [husband, wife, third_adult] + state_code: MA + output: + ssi_claim_is_joint: [true, true, false] + ma_state_supplement: [100.86, 100.86, 128.82] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/ssp/mo_ssp.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/ssp/mo_ssp.yaml index d7b94b2f51d..d3f3a7a6313 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/ssp/mo_ssp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/ssp/mo_ssp.yaml @@ -632,3 +632,109 @@ # A citizen would receive min($1,300 - $1,200, $156) = $100 plus the PNA. mo_ssp_eligible: false mo_ssp: 0 + +# RSMo 208.010.1 and DSS Manual 1035.005.00 retain the combined property +# test for spouses separated to obtain medical or nursing care. +# https://revisor.mo.gov/main/OneSection.aspx?section=208.010 +# https://dssmanuals.mo.gov/december-1973-eligibility-requirements/1035-000-00/1035-005-00/ +# MHABD Appendix J (effective July 2026): SNC couple limit $12,441, +# SNF grant cap $390 plus $50 personal needs allowance. +# https://dssmanuals.mo.gov/wp-content/uploads/2022/07/mhabd-appendix-j.pdf +# $7,000 + $1,000 = $8,000 passes; $1,000 + $15,000 = $16,000 fails. +# $2,000 facility charge - $1,200 pension = $800 need; min($800, $390) +# + $50 = $440 for the eligible claimant. SSI is calculated naturally. +- name: MO medical separation retains the couple resource cap and SNC payment + period: 2026-07 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: + 2026: 75 + is_tax_unit_head: + 2026: true + ssi_countable_resources: + 2026: 7000 + pension_income: + 2026: 14400 + mo_ssp_living_arrangement: SNF_OR_ICF_NON_MEDICAID + mo_snc_facility_base_charge: 2000 + community_spouse: + age: + 2026: 73 + is_tax_unit_spouse: + 2026: true + ssi_countable_resources: + 2026: 1000 + tax_units: + joint_return: + members: [claimant, community_spouse] + marital_units: + declared_marriage: + members: [claimant, community_spouse] + spm_units: + claimant_unit: + members: [claimant] + community_unit: + members: [community_spouse] + households: + nursing_facility: + members: [claimant] + state_code: + 2026: MO + community_home: + members: [community_spouse] + state_code: + 2026: MO + output: + mo_ssp_resource_eligible: [true, true] + mo_ssp_eligible: [true, false] + mo_ssp: [440, 0] + +- name: MO medical separation retains resources held by the community spouse + period: 2026-07 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: + 2026: 75 + is_tax_unit_head: + 2026: true + ssi_countable_resources: + 2026: 1000 + pension_income: + 2026: 14400 + mo_ssp_living_arrangement: SNF_OR_ICF_NON_MEDICAID + mo_snc_facility_base_charge: 2000 + community_spouse: + age: + 2026: 73 + is_tax_unit_spouse: + 2026: true + ssi_countable_resources: + 2026: 15000 + tax_units: + joint_return: + members: [claimant, community_spouse] + marital_units: + declared_marriage: + members: [claimant, community_spouse] + spm_units: + claimant_unit: + members: [claimant] + community_unit: + members: [community_spouse] + households: + nursing_facility: + members: [claimant] + state_code: + 2026: MO + community_home: + members: [community_spouse] + state_code: + 2026: MO + output: + mo_ssp_resource_eligible: [false, true] + mo_ssp_eligible: [false, false] + mo_ssp: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml index 98ed9ddbb9c..1cd03dd11b5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_pre_one_claimant.yaml @@ -7,14 +7,17 @@ age: 66 taxable_pension_income: 10_000 rent: 4_800 + ssi: 0 person2: age: 70 taxable_pension_income: 15_000 rent: 7_200 + ssi: 0 person3: age: 40 taxable_interest_income: 5_000 rent: 3_000 + ssi: 0 tax_units: tax_unit1: members: [person1] @@ -27,6 +30,7 @@ members: [person1, person2, person3] state_name: MT output: + # SSI is pinned at 0 to isolate the credit and claimant-limit arithmetic. # 2024 Schedule 2EC. Line 18 = 10_000 + 15_000 + 5_000 = 30_000 for the # whole household; line 20 = 17_400; line 21 = 0.05; line 22 = 870. # person1: line 25 = 720; line 27 = max(720 - 870, 0) = 0. diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/odhs/ssp/or_ssp.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/odhs/ssp/or_ssp.yaml index 66d110843ce..3ad1e367c13 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/odhs/ssp/or_ssp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/odhs/ssp/or_ssp.yaml @@ -600,3 +600,53 @@ # 994 - 180.42 = 813.58. or_ssp_countable_income: 180.42 or_ssp: 813.58 + +# OAR 461-155-0630(3) pays married SSI recipients who each receive community +# care services, including at different facilities. OAR 461-001-0000(62) +# defines spouse by legal marriage. Hub ruling: +# us-or:ssp-community-care-spouses-separate-facilities. +# https://ch461rules.odhs.oregon.gov/rules/461-155-0630.pdf#page=2 +# https://ch461rules.odhs.oregon.gov/rules/461-001-0000.pdf#page=15 +# OAR 461-140-0040(5)(e)(D) excludes income inaccessible due to incapacity. +# https://ch461rules.odhs.oregon.gov/rules/461-140-0040.pdf#page=4 +# SSI $994 - inaccessible $194/$94 = countable income $800/$900. Neither +# spouse has excess over $994, so payments are $994 - $800 = $194 and +# $994 - $900 = $94, under the one-person OSIPM standard. +- name: Review OR married SSI recipients in separate community-care facilities + period: 2026-10 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + or_ssp_category: COMMUNITY_CARE + or_ssp_inaccessible_income: 194 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + or_ssp_category: COMMUNITY_CARE + or_ssp_inaccessible_income: 94 + tax_units: + joint_tax_unit: + members: [husband, wife] + marital_units: + declared_marriage: + members: [husband, wife] + households: + husband_foster_home: + members: [husband] + state_code: + 2026: OR + wife_foster_home: + members: [wife] + state_code: + 2026: OR + output: + ssi: [994, 994] + or_ssp_countable_income: [800, 900] + or_ssp: [194, 94] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/ssp/sd_ssp.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/ssp/sd_ssp.yaml index 9d34f6f7e1d..5f858156351 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/ssp/sd_ssp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/ssp/sd_ssp.yaml @@ -543,3 +543,132 @@ sd_ssp_care: 2026-01: 771.6 2026-02: 1_166.48 + +# ARSD 67:46:04:10(6) shares income equally for spouses entering care in the +# same month, whatever their current room or household. ARSD 67:46:06:03 +# imports that income rule into the payment; 67:46:05:30 sets a $3,000 joint +# resource limit when both spouses begin long-term care in the same month. +# https://sdlegislature.gov/Rules/Administrative/67:46:04:10 +# https://sdlegislature.gov/Rules/Administrative/67:46:06:03 +# https://sdlegislature.gov/Rules/Administrative/67:46:05:30 +# ($1,800 + $1,200) / 2 = $1,500 income each; $2,000 approved care cost +# - $1,500 = $500 payment each. $1,600 + $1,600 = $3,200 resources exceed +# the $3,000 joint limit, so neither spouse receives a payment in that case. +- name: Review SD joint care entry shares income across separate facility rooms + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + pension_income: + 2026: 21600 + ssi_countable_resources: + 2026: 500 + sd_ssp_care_joint_entry: true + sd_ssp_care_certified: true + sd_ssp_care_setting: ADULT_FOSTER_CARE + sd_ssp_approved_care_cost: 2000 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + pension_income: + 2026: 14400 + ssi_countable_resources: + 2026: 500 + sd_ssp_care_joint_entry: true + sd_ssp_care_certified: true + sd_ssp_care_setting: ADULT_FOSTER_CARE + sd_ssp_approved_care_cost: 2000 + tax_units: + joint_tax_unit: + members: + - husband + - wife + marital_units: + declared_marriage: + members: + - husband + - wife + households: + husband_care_room: + members: + - husband + state_code: + 2026: SD + wife_care_room: + members: + - wife + state_code: + 2026: SD + output: + sd_ssp_care: + - 500 + - 500 + ssi: + - 0 + - 0 +- name: Review SD joint entry resources exceed couple limit across facility households + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + pension_income: + 2026: 21600 + ssi_countable_resources: + 2026: 1600 + sd_ssp_care_joint_entry: true + sd_ssp_care_certified: true + sd_ssp_care_setting: ADULT_FOSTER_CARE + sd_ssp_approved_care_cost: 2000 + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + pension_income: + 2026: 14400 + ssi_countable_resources: + 2026: 1600 + sd_ssp_care_joint_entry: true + sd_ssp_care_certified: true + sd_ssp_care_setting: ADULT_FOSTER_CARE + sd_ssp_approved_care_cost: 2000 + tax_units: + joint_tax_unit: + members: + - husband + - wife + marital_units: + declared_marriage: + members: + - husband + - wife + households: + husband_care_room: + members: + - husband + state_code: + 2026: SD + wife_care_room: + members: + - wife + state_code: + 2026: SD + output: + sd_ssp_care: + - 0 + - 0 + ssi: + - 0 + - 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag.yaml index 0f21c77ff63..89f8ab4b928 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag.yaml @@ -404,3 +404,131 @@ va_ag: 2026-01: 558 2026-02: 1_223 + +# AG Manual Chapter B section 6 (PDF pp. 34-35) tests marriage/co-residence +# at the first moment of the application month; entering separate facilities +# later in that month retains the joint budget recorded by the input. +# https://townhall.virginia.gov/l/GetFile.cfm?File=C:%5CTownHall%5Cdocroot%5CGuidanceDocs%5C262%5CGDoc_DARS_5346_v2.pdf#page=34 +# Joint countable income = ($1,200 + $1,200 - $20) / 2 = $1,190 each; +# $2,130 maintenance + $87 personal needs - $1,190 = $1,027 grant each. +# Joint resources $1,600 + $1,600 = $3,200 exceed the $3,000 couple limit. +- name: Review VA spouses enter separate facilities during joint initial month + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + pension_income: + 2026: 14400 + ssi_countable_resources: + 2026: 1000 + va_ag_certified: true + va_ag_is_joint_initial_month: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + pension_income: + 2026: 14400 + ssi_countable_resources: + 2026: 1000 + va_ag_certified: true + va_ag_is_joint_initial_month: true + tax_units: + joint_tax_unit: + members: + - husband + - wife + marital_units: + declared_marriage: + members: + - husband + - wife + households: + husband_facility: + members: + - husband + state_code: + 2026: VA + wife_facility: + members: + - wife + state_code: + 2026: VA + output: + va_ag: + - 1027 + - 1027 + ssi: + - 0 + - 0 + va_ag_eligible: + - true + - true + va_ag_countable_income: + - 1190 + - 1190 +- name: Review VA joint initial-month resources exceed couple limit across facility + households + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + husband: + age: + 2026: 72 + is_tax_unit_head: + 2026: true + pension_income: + 2026: 14400 + ssi_countable_resources: + 2026: 1600 + va_ag_certified: true + va_ag_is_joint_initial_month: true + wife: + age: + 2026: 70 + is_tax_unit_spouse: + 2026: true + pension_income: + 2026: 14400 + ssi_countable_resources: + 2026: 1600 + va_ag_certified: true + va_ag_is_joint_initial_month: true + tax_units: + joint_tax_unit: + members: + - husband + - wife + marital_units: + declared_marriage: + members: + - husband + - wife + households: + husband_facility: + members: + - husband + state_code: + 2026: VA + wife_facility: + members: + - wife + state_code: + 2026: VA + output: + va_ag: + - 0 + - 0 + ssi: + - 0 + - 0 + va_ag_eligible: + - false + - false diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_209b_ssi_recipient_income_eligible_for_medicaid.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_209b_ssi_recipient_income_eligible_for_medicaid.py index da396f055d4..fecbb5a1ce1 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_209b_ssi_recipient_income_eligible_for_medicaid.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_209b_ssi_recipient_income_eligible_for_medicaid.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class is_209b_ssi_recipient_income_eligible_for_medicaid(Variable): @@ -28,8 +31,8 @@ def formula(person, period, parameters): medical_expenses = person("medicaid_medically_needy_medical_expenses", period) # SSI financial responsibility rules (42 CFR 435.602): the unit is # the individual or the married couple, not the tax filing unit. - income_after_spenddown = person.marital_unit.sum( - personal_income - medical_expenses + income_after_spenddown = _ssi_couple_total( + person, period, personal_income - medical_expenses ) # The limit is per person (Missouri's blind standard can differ diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py index b4745bda5ad..e3023a1fd25 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class is_optional_senior_or_disabled_asset_eligible(Variable): @@ -19,8 +23,8 @@ def formula(person, period, parameters): # the individual or the married couple, not the tax filing unit. personal_assets = person("ssi_countable_resources", period) # $ # Inputs are stored as float32, so compare whole cents with the limit. - assets = np.round(person.marital_unit.sum(personal_assets), 2) - is_couple = person.marital_unit.nb_persons() == 2 + assets = np.round(_ssi_couple_total(person, period, personal_assets), 2) + is_couple = _ssi_spouse_index(person, period) >= 0 state = person.household("state_code_str", period) # Parameters diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_income_eligible.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_income_eligible.py index 859510b9a7b..b9167e50ffb 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_income_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_income_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class is_optional_senior_or_disabled_income_eligible(Variable): @@ -29,7 +32,7 @@ def formula(person, period, parameters): ) # SSI financial responsibility rules (42 CFR 435.602): the unit is # the individual or the married couple, not the tax filing unit. - income = person.marital_unit.sum(personal_income) + income = _ssi_couple_total(person, period, personal_income) income_limit = person( "medicaid_optional_senior_or_disabled_income_limit", period ) diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medicaid_optional_senior_or_disabled_income_limit.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medicaid_optional_senior_or_disabled_income_limit.py index 6e676553384..3f2a4b3a3a9 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medicaid_optional_senior_or_disabled_income_limit.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medicaid_optional_senior_or_disabled_income_limit.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class medicaid_optional_senior_or_disabled_income_limit(Variable): @@ -25,7 +28,7 @@ def formula(person, period, parameters): # Non-MAGI Medicaid follows the SSI financial responsibility rules # (42 CFR 435.602): the unit is the individual, or the married # couple living together, whatever their filing status. - is_couple = person.marital_unit.nb_persons() == 2 + is_couple = _ssi_spouse_index(person, period) >= 0 state = person.household("state_code_str", period) p = parameters( period diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py index 01e6b76825e..b9b3dfc3b98 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class is_medically_needy_for_medicaid(Variable): @@ -39,7 +43,6 @@ def formula(person, period, parameters): # SSI financial responsibility rules (42 CFR 435.602): the unit is # the individual or the married couple, plus parents for a child, # not the tax filing unit. - marital_unit = person.marital_unit # 42 CFR 435.831(d) deducts expenses of financially responsible # relatives. For a child these are the ineligible parents whose # income SSI deems (20 CFR 416.1165); split their expenses among the @@ -54,13 +57,15 @@ def formula(person, period, parameters): parental_expense_share = where( eligible_child, parental_expenses / max_(eligible_children, 1), 0 ) - income = marital_unit.sum( - personal_income - medical_expenses - parental_expense_share + income = _ssi_couple_total( + person, + period, + personal_income - medical_expenses - parental_expense_share, ) - assets = marital_unit.sum(personal_assets) + assets = _ssi_couple_total(person, period, personal_assets) ma = parameters(period).gov.hhs.medicaid mn = ma.eligibility.categories.medically_needy - is_joint = marital_unit.nb_persons() == 2 + is_joint = _ssi_spouse_index(person, period) >= 0 state = person.household("state_code_str", period) income_limit = MONTHS_IN_YEAR * where( is_joint, diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.py b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.py index a692202dac3..d5958cdec9f 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.py +++ b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.py @@ -2,6 +2,9 @@ from policyengine_us.variables.gov.hhs.medicaid.income._apply_medicaid_optional_senior_or_disabled_exclusions import ( _apply_medicaid_optional_senior_or_disabled_exclusions, ) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse( @@ -39,19 +42,18 @@ def formula(person, period, parameters): # deeming methodology: the spouse's income counts without the # FBR-differential threshold or ineligible-child allocations. is_mo = state == "MO" - marital_unit = person.marital_unit ineligible_spouse = person("is_ssi_ineligible_spouse", period) spouse_gross_earned = ( - marital_unit.sum(ineligible_spouse * individual_earned) + _ssi_couple_total(person, period, ineligible_spouse * individual_earned) - ineligible_spouse * individual_earned ) spouse_gross_unearned = ( - marital_unit.sum(ineligible_spouse * individual_unearned) + _ssi_couple_total(person, period, ineligible_spouse * individual_unearned) - ineligible_spouse * individual_unearned ) spouse_earned = where(is_mo, spouse_gross_earned, spouse_earned) spouse_unearned = where(is_mo, spouse_gross_unearned, spouse_unearned) - has_ineligible_spouse = marital_unit.sum(ineligible_spouse) > 0 + has_ineligible_spouse = _ssi_couple_total(person, period, ineligible_spouse) > 0 deeming_applies = deeming_applies | (is_mo & has_ineligible_spouse) alone_countable = _apply_medicaid_optional_senior_or_disabled_exclusions( diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py index 3bf485dfcba..4c537a0d75e 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py +++ b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py @@ -2,6 +2,9 @@ from policyengine_us.variables.gov.hhs.medicaid.income._apply_medicaid_optional_senior_or_disabled_exclusions import ( _apply_medicaid_optional_senior_or_disabled_exclusions, ) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class medicaid_optional_senior_or_disabled_countable_income(Variable): @@ -93,7 +96,7 @@ def formula(person, period, parameters): ) premiums = where( couple_computation, - person.marital_unit.sum(own_premiums), + _ssi_couple_total(person, period, own_premiums), own_premiums, ) deducts_premiums = p.deducts_health_insurance_premiums[state].astype(bool) diff --git a/policyengine_us/variables/gov/ssa/ssi/_ssi_living_arrangement_amount.py b/policyengine_us/variables/gov/ssa/ssi/_ssi_living_arrangement_amount.py new file mode 100644 index 00000000000..ebffcf05966 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/_ssi_living_arrangement_amount.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( + SSIFederalLivingArrangement, +) + + +def ssi_living_arrangement_amount(person, period, parameters, base_amount): + """Adjust a federal benefit rate for the person's living arrangement.""" + p = parameters(period).gov.ssa.ssi.amount + arrangement = person("ssi_federal_living_arrangement", period) + + # 20 CFR § 416.1131: One-third reduction for another person's + # household. Applies to the applicable FBR (individual or couple + # based on deeming). + is_another_household = ( + arrangement == SSIFederalLivingArrangement.ANOTHER_PERSONS_HOUSEHOLD + ) + base_amount = where( + is_another_household, + base_amount * (1 - p.one_third_reduction_rate), + base_amount, + ) + + # 42 USC § 1382(e)(1)(B), 20 CFR § 416.414: Medical treatment + # facility, $30/month per person. + is_medical_facility = ( + arrangement == SSIFederalLivingArrangement.MEDICAL_TREATMENT_FACILITY + ) + return where(is_medical_facility, p.medical_facility, base_amount) diff --git a/policyengine_us/variables/gov/ssa/ssi/_ssi_state_supplement.py b/policyengine_us/variables/gov/ssa/ssi/_ssi_state_supplement.py index 5e0c84da3f3..12f89b15a03 100644 --- a/policyengine_us/variables/gov/ssa/ssi/_ssi_state_supplement.py +++ b/policyengine_us/variables/gov/ssa/ssi/_ssi_state_supplement.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( SSIFederalLivingArrangement, ) @@ -18,7 +21,7 @@ def ssi_state_supplement( SI 02001.005 and SI 02001.010. Callers apply state-specific eligibility. """ joint = person("ssi_couple_computation_applies", period) - couple_total = person.marital_unit.sum(couple_rate / 2) + couple_total = _ssi_couple_total(person, period.this_year, couple_rate / 2) standard = where(joint, couple_total / 2, individual_rate) countable = person("ssi_countable_income", period) federal_amount = person("ssi_amount_if_eligible", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/README.md b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/README.md index 2458846818d..38751797a67 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/README.md +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/README.md @@ -34,6 +34,13 @@ The implementation follows these steps according to the regulations: 5. **Calculate deemed income amount** - The deemed amount is the difference between the combined countable income and the individual's countable income +6. **Limit the benefit to the amount without deeming** (§416.1163(e)(2)) + - The benefit is the lesser of the couple computation and the individual Federal benefit rate less only the individual's own countable income (`uncapped_ssi_without_spousal_deeming`), applied in `uncapped_ssi` before the zero floor + +## Who is an ineligible spouse + +An ineligible spouse is "someone who lives with you as your husband or wife and is not eligible for SSI benefits" (§416.1160(d)). The spouse is found by the co-resident spouse test in `eligibility/resources/deemed/_ssi_spouses.py`, which resource deeming and joint claims also use, not by tax-unit head and spouse roles. For income, eligibility here means aged, blind or disabled status: a spouse who is aged, blind or disabled but fails the citizenship or qualified-noncitizen condition is still treated as part of a joint claim, because state supplements and Medicaid read the joint claim as the SSI couple methodology. The resource test treats that spouse as an ineligible spouse (`_ssi_resource_couple`). + ## Exclusions Applied Income exclusions are applied in the following order: diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse.py index 2be85271634..3dc7de68f08 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class ssi_earned_income_deemed_from_ineligible_spouse(Variable): @@ -11,18 +14,20 @@ class ssi_earned_income_deemed_from_ineligible_spouse(Variable): def formula(person, period, parameters): # First, determine unearned income from the ineligible spouse. - # This is (a) in the law. - ineligible_spouse = person("is_ssi_ineligible_spouse", period) + # This is (a) in the law. The spouse is the co-resident husband or + # wife (416.1160(d)), whatever their tax-unit role. + spouse = _ssi_spouse_index(person, period) + safe_spouse = np.maximum(spouse, 0) + has_ineligible_spouse = (spouse >= 0) & person( + "is_ssi_ineligible_spouse", period + )[safe_spouse] unearned_income = person("ssi_unearned_income", period) - marital_unit = person.marital_unit - ineligible_spousal_unearned_income = ( - marital_unit.sum(ineligible_spouse * unearned_income) - - ineligible_spouse * unearned_income + ineligible_spousal_unearned_income = where( + has_ineligible_spouse, unearned_income[safe_spouse], 0 ) earned_income = person("ssi_earned_income", period) - ineligible_spousal_earned_income = ( - marital_unit.sum(ineligible_spouse * earned_income) - - ineligible_spouse * earned_income + ineligible_spousal_earned_income = where( + has_ineligible_spouse, earned_income[safe_spouse], 0 ) # Next, (b) determine and subtract allocations for ineligible children. diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse.py index 93257349be8..536866b9aaf 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class ssi_unearned_income_deemed_from_ineligible_spouse(Variable): @@ -14,12 +17,16 @@ class ssi_unearned_income_deemed_from_ineligible_spouse(Variable): def formula(person, period, parameters): # First, determine unearned income from the ineligible spouse. - # This is (a) in the law. - ineligible_spouse = person("is_ssi_ineligible_spouse", period) + # This is (a) in the law. The spouse is the co-resident husband or + # wife (416.1160(d)), whatever their tax-unit role. + spouse = _ssi_spouse_index(person, period) + safe_spouse = np.maximum(spouse, 0) + has_ineligible_spouse = (spouse >= 0) & person( + "is_ssi_ineligible_spouse", period + )[safe_spouse] unearned_income = person("ssi_unearned_income", period) - ineligible_spousal_unearned_income = ( - person.marital_unit.sum(ineligible_spouse * unearned_income) - - ineligible_spouse * unearned_income + ineligible_spousal_unearned_income = where( + has_ineligible_spouse, unearned_income[safe_spouse], 0 ) # Next, (b) determine and subtract allocations for ineligible children. diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_earned_income.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_earned_income.py index 4e813069180..ea43e26017a 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_earned_income.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_earned_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class ssi_marital_earned_income(Variable): @@ -6,13 +9,14 @@ class ssi_marital_earned_income(Variable): entity = Person label = "Total SSI earned income for a marital unit" definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1802#p-416.1802(b)(1)" + ) def formula(person, period, parameters): + # 416.1802(b)(1): with an eligible spouse 'we will count your combined + # income'. Combine the two spouses, not every member of the unit. couple_computation = person("ssi_couple_computation_applies", period) earned_income = person("ssi_earned_income", period) - - return where( - couple_computation, - person.marital_unit.sum(earned_income), - earned_income, - ) + spouse = np.maximum(_ssi_spouse_index(person, period), 0) + return earned_income + where(couple_computation, earned_income[spouse], 0) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_unearned_income.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_unearned_income.py index 0fde0dee8bd..e4e786b7f4d 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_unearned_income.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/marital/ssi_marital_unearned_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class ssi_marital_unearned_income(Variable): @@ -6,13 +9,14 @@ class ssi_marital_unearned_income(Variable): entity = Person label = "Total SSI unearned income for a marital unit" definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1802#p-416.1802(b)(1)" + ) def formula(person, period, parameters): + # 416.1802(b)(1): with an eligible spouse 'we will count your combined + # income'. Combine the two spouses, not every member of the unit. couple_computation = person("ssi_couple_computation_applies", period) unearned_income = person("ssi_unearned_income", period) - - return where( - couple_computation, - person.marital_unit.sum(unearned_income), - unearned_income, - ) + spouse = np.maximum(_ssi_spouse_index(person, period), 0) + return unearned_income + where(couple_computation, unearned_income[spouse], 0) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_spouses.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_spouses.py index fbd5f09ede5..ba5fb0f0b65 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_spouses.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_spouses.py @@ -67,3 +67,44 @@ def _ssi_spouse_index(person, period): """The row of each person's co-resident spouse, or -1.""" first, second = co_resident_parent_indices(person, period) return _ssi_established_spouse_indices(person, period, first, second) + + +def _ssi_eligible_apart_from_means(person, period): + """Aged, blind or disabled and meeting SSI's citizenship condition. + + 416.1160(d) makes an ineligible spouse one who 'is not eligible for SSI + benefits'. The resource test runs before eligibility is known, so this + tests the conditions that do not depend on it: aged, blind or disabled + status, and citizenship or qualified-noncitizen status (as is_ssi_eligible + requires). + """ + immigration_status = person("immigration_status", period) + is_citizen = immigration_status == immigration_status.possible_values.CITIZEN + return person("is_ssi_aged_blind_disabled", period) & ( + is_citizen | person("is_ssi_qualified_noncitizen", period) + ) + + +def _ssi_resource_couple(person, period): + """Whether 416.1205(b) combines a joint claim's resources. + + A joint claim pairs two aged, blind or disabled spouses. When one of them + fails the citizenship condition, they are not an eligible couple + (416.1801(c)), so the other's resources are tested with the spouse's + deemed under 416.1202(a), with the ineligible-spouse exclusions. + """ + eligible = _ssi_eligible_apart_from_means(person, period) + spouse = np.maximum(_ssi_spouse_index(person, period), 0) + return person("ssi_claim_is_joint", period) & eligible & eligible[spouse] + + +def _ssi_couple_total(person, period, values): + """Each person's value plus their co-resident spouse's, if they have one. + + This equals the marital-unit sum for units of one or two people, and + adds only the spouse when a unit holds more people, as the default unit + of a situation that omits marital units can. ``period`` is the year of + the spouse test. + """ + spouse = _ssi_spouse_index(person, period) + return values + where(spouse >= 0, values[np.maximum(spouse, 0)], 0) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.py index 8a0c3c52e50..18e184d3091 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/is_ssi_spousal_resource_deeming_applies.py @@ -1,5 +1,6 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_resource_couple, _ssi_spouse_index, ) @@ -22,13 +23,15 @@ def formula(person, period, parameters): # resources of the spouse' against the couple limit. Eligibility is # not known before the resource test, and 416.1205(b) tests an # eligible couple's combined resources against the same limit, so a - # spouse's resources count either way when the claim is not already - # joint. The spouse is identified as a parent's spouse is in parental + # spouse's resources count either way when the couple's are not + # already combined. The spouse is identified as a parent's spouse is in parental # deeming. 416.1167 retains an absent person's household membership: # enter a temporarily absent spouse in the household they remain in. + # A joint claim whose other member fails the citizenship condition + # is not an eligible couple (416.1801(c)), so it deems too. spouse = _ssi_spouse_index(person, period) return ( person("is_ssi_aged_blind_disabled", period) - & ~person("ssi_claim_is_joint", period) + & ~_ssi_resource_couple(person, period) & (spouse >= 0) ) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_spouse.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_spouse.py index 6def145a7ea..08d6bbe113f 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_spouse.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_spouse.py @@ -1,5 +1,6 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_eligible_apart_from_means, _ssi_spouse_index, ) @@ -26,11 +27,7 @@ def formula(person, period, parameters): # A spouse who fails SSI's citizenship or qualified-noncitizen # condition is 'not eligible for SSI benefits' (416.1160) whatever # their age or disability, so keeps the ineligible-spouse exclusions. - immigration_status = person("immigration_status", period) - could_be_eligible = person("is_ssi_aged_blind_disabled", period) & ( - (immigration_status == immigration_status.possible_values.CITIZEN) - | person("is_ssi_qualified_noncitizen", period) - ) + could_be_eligible = _ssi_eligible_apart_from_means(person, period) resources = where( could_be_eligible[spouse], person("ssi_countable_resources", period)[spouse], diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py index 8a593952788..67fde62db37 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_resource_couple, + _ssi_spouse_index, +) class meets_ssi_resource_test(Variable): @@ -25,9 +29,12 @@ def formula(person, period, parameters): "ssi_resources_deemed_from_ineligible_parent", ], ) + # 416.1205(b): an eligible couple's combined resources. The couple + # is two spouses, so add the spouse rather than the marital unit. + spouse = np.maximum(_ssi_spouse_index(person, period), 0) countable_resources = where( - joint_claim, - person.marital_unit.sum(personal_resources), + _ssi_resource_couple(person, period), + personal_resources + personal_resources[spouse], personal_resources + deemed_resources, ) # 416.1205(a): an individual living with an ineligible spouse counts diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_eligible_spouse.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_eligible_spouse.py index 5c74f441fb7..f60bef2b143 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_eligible_spouse.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_eligible_spouse.py @@ -10,6 +10,8 @@ class is_ssi_eligible_spouse(Variable): reference = "https://www.law.cornell.edu/uscode/text/42/1382c#b" def formula(person, period, parameters): - both_aged_blind_disabled = person("ssi_marital_both_eligible", period) + # This label follows the tax-unit spouse role within a joint claim. + # A married couple with no tax-unit spouse role has no member + # carrying the eligible-spouse label. spouse = person("is_tax_unit_spouse", period) - return spouse & both_aged_blind_disabled + return spouse & person("ssi_claim_is_joint", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_spouse.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_spouse.py index 7f1f4b45070..bce250cdc5c 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_spouse.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_spouse.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class is_ssi_ineligible_spouse(Variable): @@ -7,18 +10,18 @@ class is_ssi_ineligible_spouse(Variable): label = "Is an SSI-ineligible spouse" unit = USD definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/42/1382c#b" + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1382c#b", + "https://www.ecfr.gov/current/title-20/section-416.1160#p-416.1160(d)", + "https://www.ecfr.gov/current/title-20/section-416.1806", + ) def formula(person, period, parameters): - # Check if person is a spouse (either tax unit spouse or head) - is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) - - # Check SSI eligibility status - eligible = person("is_ssi_aged_blind_disabled", period) - eligible_spouse = person("is_ssi_eligible_spouse", period) - - # A person is an ineligible spouse if they: - # 1. Are a spouse (either tax unit spouse or head) - # 2. Are not SSI eligible themselves - # 3. Are not an SSI eligible spouse - return is_head_or_spouse & ~eligible_spouse & ~eligible + # 416.1160(d): 'Ineligible spouse means someone who lives with you as + # your husband or wife and is not eligible for SSI benefits.' The + # spouse test is the one joint claims and resource deeming use, so a + # spouse who is not a tax-unit head or spouse still counts. Like + # ssi_claim_is_joint, this tests aged, blind or disabled status only. + abd = person("is_ssi_aged_blind_disabled", period) + spouse = _ssi_spouse_index(person, period) + return ~abd & (spouse >= 0) & abd[np.maximum(spouse, 0)] diff --git a/policyengine_us/variables/gov/ssa/ssi/ssi_amount_if_eligible.py b/policyengine_us/variables/gov/ssa/ssi/ssi_amount_if_eligible.py index 480f7e72483..8bc3c8a92f2 100644 --- a/policyengine_us/variables/gov/ssa/ssi/ssi_amount_if_eligible.py +++ b/policyengine_us/variables/gov/ssa/ssi/ssi_amount_if_eligible.py @@ -1,6 +1,6 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( - SSIFederalLivingArrangement, +from policyengine_us.variables.gov.ssa.ssi._ssi_living_arrangement_amount import ( + ssi_living_arrangement_amount, ) @@ -14,11 +14,11 @@ class ssi_amount_if_eligible(Variable): def formula(person, period, parameters): p = parameters(period).gov.ssa.ssi.amount - arrangement = person("ssi_federal_living_arrangement", period) # 20 CFR 416.414(b): Benefit rate depends on couple computation: # 1. Couple computation applies → couple rate / 2 per person - # 2. No couple computation, deeming → couple rate (capped in ssi) + # 2. No couple computation, deeming → couple rate (limited by + # 416.1163(e)(2) in uncapped_ssi) # 3. No couple computation, no deeming → individual rate # # ssi_couple_computation_applies is false when only one spouse @@ -48,23 +48,4 @@ def formula(person, period, parameters): person("is_child", period.this_year), p.individual, base_amount ) - is_medical_facility = ( - arrangement == SSIFederalLivingArrangement.MEDICAL_TREATMENT_FACILITY - ) - - # 20 CFR § 416.1131: One-third reduction for another person's - # household. Applies to the applicable FBR (individual or couple - # based on deeming). - is_another_household = ( - arrangement == SSIFederalLivingArrangement.ANOTHER_PERSONS_HOUSEHOLD - ) - vtr_rate = p.one_third_reduction_rate - base_amount = where( - is_another_household, - base_amount * (1 - vtr_rate), - base_amount, - ) - - # 42 USC § 1382(e)(1)(B), 20 CFR § 416.414: Medical treatment - # facility, $30/month per person. - return where(is_medical_facility, p.medical_facility, base_amount) + return ssi_living_arrangement_amount(person, period, parameters, base_amount) diff --git a/policyengine_us/variables/gov/ssa/ssi/ssi_claim_is_joint.py b/policyengine_us/variables/gov/ssa/ssi/ssi_claim_is_joint.py index 7f90b60888e..25189916d81 100644 --- a/policyengine_us/variables/gov/ssa/ssi/ssi_claim_is_joint.py +++ b/policyengine_us/variables/gov/ssa/ssi/ssi_claim_is_joint.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class ssi_claim_is_joint(Variable): @@ -6,10 +9,22 @@ class ssi_claim_is_joint(Variable): entity = Person label = "SSI claim is joint" definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1382c#b", + "https://www.ecfr.gov/current/title-20/section-416.1801", + "https://www.ecfr.gov/current/title-20/section-416.1802", + "https://www.ecfr.gov/current/title-20/section-416.1806", + ) def formula(person, period, parameters): - abd_person = person("is_ssi_aged_blind_disabled", period) - is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) - eligible_person = abd_person & is_head_or_spouse - - return person.marital_unit.sum(eligible_person) > 1 + # 416.1801(c): an eligible spouse is the spouse of another person + # eligible for SSI who lives in the same household with that person. + # Spouses are found by the test resource deeming uses (416.1806: + # husband or wife, living together), not by tax-unit head and spouse + # roles, which go to the two oldest adults of a tax unit when roles + # are not supplied. Both must be aged, blind or disabled. The + # citizenship condition is not applied here: state supplements and + # Medicaid read this as the SSI couple methodology. + abd = person("is_ssi_aged_blind_disabled", period) + spouse = _ssi_spouse_index(person, period) + return abd & (spouse >= 0) & abd[np.maximum(spouse, 0)] diff --git a/policyengine_us/variables/gov/ssa/ssi/ssi_couple_computation_applies.py b/policyengine_us/variables/gov/ssa/ssi/ssi_couple_computation_applies.py index 9befe7a063f..a960bb7ea94 100644 --- a/policyengine_us/variables/gov/ssa/ssi/ssi_couple_computation_applies.py +++ b/policyengine_us/variables/gov/ssa/ssi/ssi_couple_computation_applies.py @@ -2,6 +2,9 @@ from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( SSIFederalLivingArrangement, ) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class ssi_couple_computation_applies(Variable): @@ -31,6 +34,7 @@ def formula(person, period, parameters): in_facility = ( arrangement == SSIFederalLivingArrangement.MEDICAL_TREATMENT_FACILITY ) - one_in_facility = person.marital_unit.sum(in_facility) == 1 + spouse = np.maximum(_ssi_spouse_index(person, period.this_year), 0) + one_in_facility = in_facility != in_facility[spouse] return joint_claim & ~one_in_facility diff --git a/policyengine_us/variables/gov/ssa/ssi/ssi_if_takes_up.py b/policyengine_us/variables/gov/ssa/ssi/ssi_if_takes_up.py index 7e6a85efd5c..7a51025bf3b 100644 --- a/policyengine_us/variables/gov/ssa/ssi/ssi_if_takes_up.py +++ b/policyengine_us/variables/gov/ssa/ssi/ssi_if_takes_up.py @@ -17,23 +17,7 @@ def formula(person, period, parameters): if parameters(period).gov.ssa.ssi.abolish_ssi: return 0 - uncapped = person("uncapped_ssi", period) - - # Apply floor: SSI cannot be negative - benefit = max_(0, uncapped) - - # Apply cap when spousal deeming: cannot exceed individual FBR - # per 20 CFR §416.1163 - # This cap is necessary when spouse's gross income is just above $483: - # - Deeming applies (uses couple FBR) - # - After exclusions, countable may be low - # - Benefit could exceed individual FBR without this cap - deeming_applies = person("is_ssi_spousal_deeming_applies", period.this_year) - p = parameters(period).gov.ssa.ssi.amount - capped_benefit = min_(benefit, p.individual) - - return where( - deeming_applies, - capped_benefit, - benefit, - ) + # Apply floor: SSI cannot be negative. Under spousal deeming, + # uncapped_ssi already applies 20 CFR 416.1163(e)(2), which also keeps + # the benefit at or below the individual Federal benefit rate. + return max_(0, person("uncapped_ssi", period)) diff --git a/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py b/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py index 04b751a49b7..b9541abddde 100644 --- a/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py +++ b/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py @@ -9,8 +9,23 @@ class uncapped_ssi(Variable): documentation = "Maximum SSI, less countable income (can be below zero)." definition_period = MONTH defined_for = "is_ssi_eligible" + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1382#b", + "https://www.ecfr.gov/current/title-20/section-416.1163#p-416.1163(e)(2)", + ) def formula(person, period, parameters): amount = person("ssi_amount_if_eligible", period) countable_income = person("ssi_countable_income", period) - return amount - countable_income + uncapped = amount - countable_income + # 20 CFR 416.1163(e)(2): 'Your SSI benefit under the deeming rules + # cannot be higher than it would be if deeming did not apply. + # Therefore, your benefit is the lesser of the amount computed under + # the rules in paragraph (d)(2) of this section or the amount + # remaining after we subtract only your own countable income from an + # individual's Federal benefit rate.' Taking the lesser before the + # zero floor keeps this consistent with the payment for programs + # that read the shortfall below zero. + without_deeming = person("uncapped_ssi_without_spousal_deeming", period) + deeming_applies = person("is_ssi_spousal_deeming_applies", period.this_year) + return where(deeming_applies, min_(uncapped, without_deeming), uncapped) diff --git a/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi_without_spousal_deeming.py b/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi_without_spousal_deeming.py new file mode 100644 index 00000000000..de6ab525097 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi_without_spousal_deeming.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi._ssi_living_arrangement_amount import ( + ssi_living_arrangement_amount, +) + + +class uncapped_ssi_without_spousal_deeming(Variable): + value_type = float + entity = Person + label = "Uncapped SSI without spousal deeming" + unit = USD + documentation = ( + "Individual federal benefit rate less only the person's own countable " + "income, the amount SSI under spousal deeming cannot exceed (can be " + "below zero)." + ) + definition_period = MONTH + defined_for = "is_ssi_eligible" + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1163#p-416.1163(e)(2)", + ) + + def formula(person, period, parameters): + # 416.1163(e)(2): 'the amount remaining after we subtract only your + # own countable income from an individual's Federal benefit rate'. + # The rate takes the same living-arrangement adjustments as the rate + # under deeming. + p = parameters(period).gov.ssa.ssi.amount + amount = ssi_living_arrangement_amount(person, period, parameters, p.individual) + # Countable income adds income deemed from an ineligible spouse to the + # person's own countable income; remove it. + own_countable_income = person("ssi_countable_income", period) - person( + "ssi_income_deemed_from_ineligible_spouse", period + ) + return amount - own_countable_income diff --git a/policyengine_us/variables/gov/states/_spouses.py b/policyengine_us/variables/gov/states/_spouses.py new file mode 100644 index 00000000000..902b2be46c2 --- /dev/null +++ b/policyengine_us/variables/gov/states/_spouses.py @@ -0,0 +1,61 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person._parent_links import ( + _parent_indices, + co_resident_parent_indices, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) + + +def _state_retained_pair(person, year): + """A two-person marriage, excluding known parent/dependent pairs. + + Resolve parent links within the marital unit, including across households + and tax units. A link naming a co-resident keeps that identity when the + same id is reused by a spouse in another household. The existing parent + resolver keeps its ambiguous-id fallback. + As in SSI's spouse test, a possible default unit cannot make someone a + spouse of the person who claims them as a tax dependent. + """ + first, second = _parent_indices(person, year, person.marital_unit) + home_first, home_second = co_resident_parent_indices(person, year) + marital_unit = person.marital_unit.reference_entity.members_entity_id + + def prefer_co_resident(slot, home): + in_unit = np.where(marital_unit[np.maximum(home, 0)] == marital_unit, home, -1) + return np.where(home >= 0, in_unit, slot) + + first = prefer_co_resident(first, home_first) + second = prefer_co_resident(second, home_second) + parent_link = person.marital_unit.any((first >= 0) | (second >= 0)) + tax_unit = person.tax_unit.reference_entity.members_entity_id + same_tax_unit = person.marital_unit.min(tax_unit) == person.marital_unit.max( + tax_unit + ) + dependent_count = person.marital_unit.sum(person("is_tax_unit_dependent", year)) + claims = (person.marital_unit.count == 1) & same_tax_unit & (dependent_count == 1) + return (person.marital_unit.nb_persons() == 2) & ~parent_link & ~claims + + +def state_couple_total(person, year, values): + """Spouse totals for state rules that retain a declared marriage apart. + + A two-person marital unit declares the spouses unless parent/dependent + facts rule them out, even when they occupy different households. + Larger default groups instead use SSI's identified + spouse pair, so an unrelated adult cannot enter the spouse's budget. + Callers apply their own medical-separation, entry-month or care conditions; + this helper does not change federal SSI's co-residence test. + """ + return where( + _state_retained_pair(person, year), + person.marital_unit.sum(values), + _ssi_couple_total(person, year, values), + ) + + +def state_is_married(person, year): + """Declared spouses, including apart, or the identified default-group pair.""" + return _state_retained_pair(person, year) | (_ssi_spouse_index(person, year) >= 0) diff --git a/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp.py b/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp.py index a7fe59d890d..8baaf19c27f 100644 --- a/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp.py +++ b/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ak_ssp(Variable): @@ -25,8 +28,11 @@ def formula(person, period): "ak_ssp_eligible", period ) claim_type_values = claim_type.possible_values + couple_supplement = _ssi_couple_total( + person, period.this_year, state_supplement + ) return where( claim_type == claim_type_values.COUPLE_BOTH_ELIGIBLE, - person.marital_unit.sum(state_supplement) / 2, + couple_supplement / 2, state_supplement, ) diff --git a/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp_claim_type.py b/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp_claim_type.py index 21f29e2aca3..116a3c128c2 100644 --- a/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp_claim_type.py +++ b/policyengine_us/variables/gov/states/ak/dpa/ssp/ak_ssp_claim_type.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class AKSSPClaimType(Enum): @@ -21,54 +24,23 @@ class ak_ssp_claim_type(Variable): def formula(person, period, parameters): joint_claim = person("ssi_claim_is_joint", period.this_year) - marital_unit_size = person.marital_unit.sum( - person("age", period.this_year) >= 0 - ) - living_arrangement = person("ak_ssp_living_arrangement", period) - living_arrangement_values = living_arrangement.possible_values + # The spouse is the co-resident husband or wife the SSI joint claim + # uses, so a third adult in a default marital unit is not counted. + spouse = _ssi_spouse_index(person, period.this_year) + has_spouse = spouse >= 0 + partner = np.where(has_spouse, spouse, np.arange(person.count)) # Both spouses must share the same living arrangement to be - # treated as a joint couple claim. New enum values added to - # AKSSPLivingArrangement must be appended to this check. - shared_living_arrangement = ( - ( - person.marital_unit.sum( - living_arrangement == living_arrangement_values.INDEPENDENT - ) - == marital_unit_size - ) - | ( - person.marital_unit.sum( - living_arrangement == living_arrangement_values.HOUSEHOLD_OF_ANOTHER - ) - == marital_unit_size - ) - | ( - person.marital_unit.sum( - living_arrangement == living_arrangement_values.ASSISTED_LIVING - ) - == marital_unit_size - ) - | ( - person.marital_unit.sum( - living_arrangement == living_arrangement_values.MEDICAID_FACILITY - ) - == marital_unit_size - ) - ) + # treated as a joint couple claim. + living_arrangement = person("ak_ssp_living_arrangement", period) + shared_living_arrangement = living_arrangement == living_arrangement[partner] is_eligible_individual = person("is_ssi_aged_blind_disabled", period.this_year) # COUPLE_ONE_ELIGIBLE requires exactly one spouse to be # individually ABD-eligible. If both spouses qualify but they # are not jointly claiming, they should not be misclassified # as a one-eligible couple. - spouse_is_eligible = ( - person.marital_unit.sum(is_eligible_individual.astype(int)) - - is_eligible_individual.astype(int) - ) > 0 + spouse_is_eligible = has_spouse & is_eligible_individual[partner] one_eligible_couple = ( - ~joint_claim - & is_eligible_individual - & ~spouse_is_eligible - & (marital_unit_size == 2) + ~joint_claim & is_eligible_individual & has_spouse & ~spouse_is_eligible ) couple_both_eligible = joint_claim & shared_living_arrangement return select( diff --git a/policyengine_us/variables/gov/states/ca/cdss/capi/_ca_capi_ssi_ssp_standard.py b/policyengine_us/variables/gov/states/ca/cdss/capi/_ca_capi_ssi_ssp_standard.py index 41408ff9b62..d49ce296040 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/capi/_ca_capi_ssi_ssp_standard.py +++ b/policyengine_us/variables/gov/states/ca/cdss/capi/_ca_capi_ssi_ssp_standard.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( SSIFederalLivingArrangement, ) @@ -23,7 +27,9 @@ def ca_capi_couple_computation(person, period): only the California facility input marks it. """ in_facility = ca_capi_in_title_xix_facility(person, period) - one_in_facility = person.marital_unit.sum(in_facility) == 1 + one_in_facility = ( + _ssi_couple_total(person, period.this_year, in_facility.astype(int)) == 1 + ) return person("ssi_couple_computation_applies", period) & ~one_in_facility @@ -35,11 +41,15 @@ def ca_capi_spouse_ssi_ssp(person, period): among the unit's members eligible for it; a citizen disabled child's supplement in the unit is then attributed to the spouse. """ - spouse = ~person("ca_capi_eligible_person", period.this_year) - spouse_ssi = person.marital_unit.sum(person("ssi", period) * spouse) + spouse_index = _ssi_spouse_index(person, period.this_year) + safe_spouse = np.maximum(spouse_index, 0) + spouse = (spouse_index >= 0) & ~person("ca_capi_eligible_person", period.this_year)[ + safe_spouse + ] + spouse_ssi = person("ssi", period)[safe_spouse] * spouse ssp_eligible = person("ca_state_supplement_eligible_person", period) - spouse_ssp_share = person.marital_unit.sum(ssp_eligible * spouse) / max_( - person.spm_unit.sum(ssp_eligible), 1 + spouse_ssp_share = ( + ssp_eligible[safe_spouse] * spouse / max_(person.spm_unit.sum(ssp_eligible), 1) ) spouse_ssp = spouse_ssp_share * person.spm_unit("ca_state_supplement", period) return spouse_ssi + spouse_ssp @@ -95,9 +105,13 @@ def ca_capi_ssi_ssp_standard(person, period, parameters): ) joint = ca_capi_couple_computation(person, period) - blind_count = where(joint, person.marital_unit.sum(blind), 2 * blind) + blind_count = where( + joint, _ssi_couple_total(person, period.this_year, blind.astype(int)), 2 * blind + ) aged_or_disabled_count = where( - joint, person.marital_unit.sum(aged_or_disabled), 2 * aged_or_disabled + joint, + _ssi_couple_total(person, period.this_year, aged_or_disabled.astype(int)), + 2 * aged_or_disabled, ) two_blind = (blind_count > 1) * p.blind.married.two_blind one_blind = (blind_count == 1) * p.blind.married.one_blind @@ -135,5 +149,9 @@ def ca_capi_ssi_ssp_standard(person, period, parameters): ], default=max_(community_couple / 2, fbr.couple / 2), ) - couple = where(joint, person.marital_unit.sum(couple_share), 2 * couple_share) + couple = where( + joint, + _ssi_couple_total(person, period.this_year, couple_share), + 2 * couple_share, + ) return individual, couple diff --git a/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_payment_standard.py b/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_payment_standard.py index a7a86fb37f2..1c4a1fd3871 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_payment_standard.py +++ b/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_payment_standard.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.ca.cdss.capi._ca_capi_ssi_ssp_standard import ( ca_capi_couple_computation, ca_capi_spouse_ssi_ssp, @@ -26,8 +29,10 @@ def formula(person, period, parameters): individual, couple = ca_capi_ssi_ssp_standard(person, period, parameters) p = parameters(period).gov.states.ca.cdss.capi.payment_standard_offset joint = ca_capi_couple_computation(person, period) - capi_members = person.marital_unit.sum( - person("ca_capi_eligible_person", period.this_year) + capi_members = _ssi_couple_total( + person, + period.this_year, + person("ca_capi_eligible_person", period.this_year).astype(int), ) # Each member of a CAPI couple takes one-half (MPP 49-050.12, 49-055.4). both_capi = joint & (capi_members > 1) diff --git a/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_person.py b/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_person.py index 8c21e8748a4..3c26fea316d 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_person.py +++ b/policyengine_us/variables/gov/states/ca/cdss/capi/ca_capi_person.py @@ -2,6 +2,9 @@ from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.ca.cdss.capi._ca_capi_ssi_ssp_standard import ( ca_capi_couple_computation, ca_capi_in_title_xix_facility, @@ -67,9 +70,11 @@ def formula(person, period, parameters): # can still pay such a spouse SSI that SSA would not (issue 10481). capi_eligible = person("ca_capi_eligible_person", period.this_year) joint = ca_capi_couple_computation(person, period) - couple_with_spouse = joint & ~(person.marital_unit.sum(capi_eligible) > 1) + couple_with_spouse = joint & ~( + _ssi_couple_total(person, period.this_year, capi_eligible.astype(int)) > 1 + ) spouse_ssi_ssp = ca_capi_spouse_ssi_ssp(person, period) - couple_income = person.marital_unit.sum(countable_income) + couple_income = _ssi_couple_total(person, period.this_year, countable_income) # A spouse counts as an eligible spouse only while receiving SSI/SSP # (MPP 49-005(e)(3), 49-050.13). A spouse paid neither is an # ineligible spouse (MPP 49-005(i)(2)), whose income is deemed. diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_spouses.py b/policyengine_us/variables/gov/states/co/ssa/oap/_spouses.py new file mode 100644 index 00000000000..d518191f4ec --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_spouses.py @@ -0,0 +1,19 @@ +from policyengine_us.variables.gov.states._spouses import ( + state_couple_total, + state_is_married, +) + + +def co_oap_couple_total(person, year, values): + """OAP spouses' values, including declared spouses living apart. + + 9 CCR 2503-5 3.534(B)-(C) does not require spouses to share a + household. A two-person marital unit therefore retains both spouses; + larger default groups use SSI's identified spouse pair instead. + """ + return state_couple_total(person, year, values) + + +def co_oap_is_married(person, year): + """Marriage for OAP's shared $20 disregard (3.533(A)(2)(c)).""" + return state_is_married(person, year) 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 index a1c81d3db4d..ca6eeb4bedd 100644 --- 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 @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._spouses import ( + co_oap_couple_total, +) from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( cents, ssi_paid, @@ -74,10 +77,8 @@ def formula(person, period, parameters): # 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 + return co_oap_couple_total(person, year, 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. 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 index 227011917f2..7a8e846d960 100644 --- 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 @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._spouses import ( + co_oap_couple_total, +) from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( PRECISION, ssi_paid, @@ -42,7 +45,9 @@ def formula(person, period, parameters): 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 + spouse_of_client = ( + co_oap_couple_total(person, year, eligible.astype(int)) - eligible > 0 + ) candidate = spouse_of_client & within_limit exempt = candidate & (receives_ssi | receives_oap) # Medicaid decides only for a candidate not exempt otherwise. Skipping 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 index eefc5ab45f7..54d04d0358e 100644 --- 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 @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._spouses import ( + co_oap_is_married, +) from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( cents, ssi_paid, @@ -32,7 +35,7 @@ def formula(person, period, parameters): ) # 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 + married = co_oap_is_married(person, period.this_year) 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 diff --git a/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py b/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py index f4e5cf330f0..ce3eca91f88 100644 --- a/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py +++ b/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ct_ssp_resource_eligible(Variable): @@ -19,15 +22,17 @@ def formula(person, period, parameters): personal_resources = person("ssi_countable_resources", period.this_year) is_joint_claim = person("ssi_claim_is_joint", period.this_year) has_ineligible_spouse = ( - person.marital_unit.sum( - person("is_ssi_ineligible_spouse", period.this_year) + _ssi_couple_total( + person, + period.this_year, + person("is_ssi_ineligible_spouse", period.this_year).astype(int), ) > 0 ) applies_couple_limit = is_joint_claim | has_ineligible_spouse resources = where( applies_couple_limit, - person.marital_unit.sum(personal_resources), + _ssi_couple_total(person, period.this_year, personal_resources), personal_resources, ) limit = where( diff --git a/policyengine_us/variables/gov/states/dc/dhcf/ossp/dc_ossp_payment_amount.py b/policyengine_us/variables/gov/states/dc/dhcf/ossp/dc_ossp_payment_amount.py index ad11e748446..0f5e9b57519 100644 --- a/policyengine_us/variables/gov/states/dc/dhcf/ossp/dc_ossp_payment_amount.py +++ b/policyengine_us/variables/gov/states/dc/dhcf/ossp/dc_ossp_payment_amount.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class dc_ossp_payment_amount(Variable): @@ -17,17 +21,13 @@ def formula(person, period, parameters): living_arrangement = person("dc_ossp_living_arrangement", period) eligible = person("dc_ossp_eligible", period) joint_claim = person("ssi_claim_is_joint", period.this_year) - both_eligible = person.marital_unit.sum(eligible) == 2 + both_eligible = ( + _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 + ) # Couple rate requires joint SSI claim, both spouses OSSP-eligible, # and both in the same arrangement category. - is_os_a = living_arrangement == living_arrangement.possible_values.OS_A - is_os_b = living_arrangement == living_arrangement.possible_values.OS_B - is_os_g = living_arrangement == living_arrangement.possible_values.OS_G - both_same = ( - (person.marital_unit.sum(is_os_a) == 2) - | (person.marital_unit.sum(is_os_b) == 2) - | (person.marital_unit.sum(is_os_g) == 2) - ) + spouse = _ssi_spouse_index(person, period.this_year) + both_same = living_arrangement == living_arrangement[np.maximum(spouse, 0)] is_couple = joint_claim & both_eligible & both_same p = parameters(period).gov.states.dc.dhcf.ossp.payment diff --git a/policyengine_us/variables/gov/states/de/dhss/ssp/de_ssp.py b/policyengine_us/variables/gov/states/de/dhss/ssp/de_ssp.py index 6126735db21..7c9f51fc1f1 100644 --- a/policyengine_us/variables/gov/states/de/dhss/ssp/de_ssp.py +++ b/policyengine_us/variables/gov/states/de/dhss/ssp/de_ssp.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class de_ssp(Variable): @@ -17,13 +20,15 @@ def formula(person, period, parameters): p = parameters(period).gov.states.de.dhss.ssp.amount joint_claim = person("ssi_claim_is_joint", period.this_year) eligible = person("de_ssp_eligible", period) - is_couple = joint_claim & (person.marital_unit.sum(eligible) == 2) + is_couple = joint_claim & ( + _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 + ) per_person_amount = where(is_couple, p.couple / 2, p.individual) uncapped_ssi = person("uncapped_ssi", period) reduction = max_(0, -uncapped_ssi) supplement = max_(0, per_person_amount - reduction) return where( is_couple, - person.marital_unit.sum(supplement) / 2, + _ssi_couple_total(person, period.this_year, supplement) / 2, supplement, ) diff --git a/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_couple_rate_applies.py b/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_couple_rate_applies.py index d228149a17b..57fcaf13bcc 100644 --- a/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_couple_rate_applies.py +++ b/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_couple_rate_applies.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class fl_oss_couple_rate_applies(Variable): @@ -20,20 +23,30 @@ def formula(person, period, parameters): # couple with mismatched or missing tracks has no valid # couple rate. joint_claim = person("ssi_claim_is_joint", period) - unit_size = person.marital_unit.nb_persons() both_ssi_eligible = ( - person.marital_unit.sum(person("is_ssi_eligible", period)) == unit_size + _ssi_couple_total( + person, period.this_year, person("is_ssi_eligible", period).astype(int) + ) + == 2 ) living_arrangement = person("fl_oss_living_arrangement", period) in_facility = living_arrangement != living_arrangement.possible_values.NONE - both_in_facility = person.marital_unit.sum(in_facility) == unit_size + both_in_facility = ( + _ssi_couple_total(person, period.this_year, in_facility.astype(int)) == 2 + ) program_track = person("fl_oss_program_track", period) TRACK = program_track.possible_values both_on_redesign = ( - person.marital_unit.sum(program_track == TRACK.REDESIGN) == unit_size + _ssi_couple_total( + person, period.this_year, (program_track == TRACK.REDESIGN).astype(int) + ) + == 2 ) both_on_protected = ( - person.marital_unit.sum(program_track == TRACK.PROTECTED) == unit_size + _ssi_couple_total( + person, period.this_year, (program_track == TRACK.PROTECTED).astype(int) + ) + == 2 ) both_on_same_track = both_on_redesign | both_on_protected return joint_claim & both_ssi_eligible & both_in_facility & both_on_same_track diff --git a/policyengine_us/variables/gov/states/hi/dhs/oss/hi_oss_couple_rate_applies.py b/policyengine_us/variables/gov/states/hi/dhs/oss/hi_oss_couple_rate_applies.py index 28b6a32b3f5..5c99c9daede 100644 --- a/policyengine_us/variables/gov/states/hi/dhs/oss/hi_oss_couple_rate_applies.py +++ b/policyengine_us/variables/gov/states/hi/dhs/oss/hi_oss_couple_rate_applies.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.hi.dhs.oss.hi_oss_living_arrangement import ( HIOSSLivingArrangement, ) @@ -19,15 +22,36 @@ def formula(person, period, parameters): joint_claim = person("ssi_claim_is_joint", period.this_year) is_eligible = person("is_ssi_eligible", period) both_ssi_eligible = ( - person.marital_unit.sum(is_eligible) == person.marital_unit.nb_persons() + _ssi_couple_total(person, period.this_year, is_eligible.astype(int)) == 2 ) la = person("hi_oss_living_arrangement", period) LA = HIOSSLivingArrangement - unit_size = person.marital_unit.nb_persons() both_same_facility = ( - (person.marital_unit.sum(la == LA.COMMUNITY_CARE) == unit_size) - | (person.marital_unit.sum(la == LA.DOMICILIARY_CARE_I) == unit_size) - | (person.marital_unit.sum(la == LA.DOMICILIARY_CARE_II) == unit_size) - | (person.marital_unit.sum(la == LA.MEDICAID_INSTITUTION) == unit_size) + ( + _ssi_couple_total( + person, period.this_year, (la == LA.COMMUNITY_CARE).astype(int) + ) + == 2 + ) + | ( + _ssi_couple_total( + person, period.this_year, (la == LA.DOMICILIARY_CARE_I).astype(int) + ) + == 2 + ) + | ( + _ssi_couple_total( + person, period.this_year, (la == LA.DOMICILIARY_CARE_II).astype(int) + ) + == 2 + ) + | ( + _ssi_couple_total( + person, + period.this_year, + (la == LA.MEDICAID_INSTITUTION).astype(int), + ) + == 2 + ) ) return joint_claim & both_ssi_eligible & both_same_facility diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_smme_spouse_income.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_smme_spouse_income.py index 58014e0debf..d7025ab87d9 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_smme_spouse_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_smme_spouse_income.py @@ -1,16 +1,18 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_spouses import ( + ia_ssa_spouse_value, +) def ia_ssa_smme_spouse_is_not_ineligible(person, period): - """Whether the other member of a two-person marital unit is not an + """Whether the co-resident spouse is not an ineligible spouse under 8-E p.12: a person SSI treats as aged, blind or disabled, or an SMME applicant working above the SGA level, who receives full Medicaid under a group that does not rely on State Supplementary Assistance (6-B PDF page 76), such as MEPD (8-F PDF page 114) or 1619(b) (8-F PDF page 88). For an - applicant whom SSI does not count, any such member is the spouse. - Callers gate on ia_ssa_has_spouse, so the marital-unit sum less the - person's own value is the spouse's.""" + applicant whom SSI does not count, that person's spouse is still found + by marriage rather than SSI eligibility or tax-unit role.""" abd = person("is_ssi_aged_blind_disabled", period.this_year) sga_applicant = ( person("meets_ssi_disability_criteria", period.this_year) @@ -18,7 +20,7 @@ def ia_ssa_smme_spouse_is_not_ineligible(person, period): & person("ia_ssa_smme_coverage_eligible", period) ) status = abd | sga_applicant - return person.marital_unit.sum(status) - status > 0 + return ia_ssa_spouse_value(person, period, status) def ia_ssa_smme_spouse_is_sga_applicant(person, period): @@ -26,13 +28,12 @@ def ia_ssa_smme_spouse_is_sga_applicant(person, period): an SMME applicant working above the SGA level whose SMME income combines the couple's income. SSI deems from that applicant, but under 8-E p.12 the applicant is not an ineligible spouse, so the couple is combined. - The applicant's flag requires ia_ssa_has_spouse, so the marital-unit sum - less the person's own value is the spouse's.""" + Unrelated household adults never enter this spouse test.""" applicant = person("ia_ssa_smme_spousal_deeming_applies", period) & person( "ia_ssa_smme_coverage_eligible", period ) abd = person("is_ssi_aged_blind_disabled", period.this_year) - return abd & (person.marital_unit.sum(applicant) - applicant > 0) + return abd & ia_ssa_spouse_value(person, period, applicant) def ia_ssa_smme_spouse_income(person, period): @@ -41,8 +42,7 @@ def ia_ssa_smme_spouse_income(person, period): first (20 CFR 416.1163(b)(3)). A spouse who is not an ineligible spouse is combined as for an eligible couple, with no allocations and after the spouse's own student earned income exclusion (20 CFR 416.1112(c)(3)). - Callers gate on ia_ssa_has_spouse, so the marital-unit sum less the - person's own value is the spouse's. As in SSI's own deeming, support and + Only the identified co-resident spouse contributes. As in SSI's own deeming, support and alimony the spouse pays are not deducted (20 CFR 416.1161(a)(10), 8-E p.12; issue 10454), children are found through the applicant's tax unit, no allocation is skipped for a child who receives public @@ -53,9 +53,11 @@ def ia_ssa_smme_spouse_income(person, period): unearned = person("ssi_unearned_income", period) exclusion = person("ssi_blind_or_disabled_working_student_exclusion", period) not_ineligible = ia_ssa_smme_spouse_is_not_ineligible(person, period) - spouse_exclusion = (person.marital_unit.sum(exclusion) - exclusion) * not_ineligible - spouse_earned = max_(person.marital_unit.sum(earned) - earned - spouse_exclusion, 0) - spouse_unearned = person.marital_unit.sum(unearned) - unearned + spouse_exclusion = ia_ssa_spouse_value(person, period, exclusion) * not_ineligible + spouse_earned = max_( + ia_ssa_spouse_value(person, period, earned) - spouse_exclusion, 0 + ) + spouse_unearned = ia_ssa_spouse_value(person, period, unearned) allocations = ( add(person.tax_unit, period, ["ssi_ineligible_child_allocation"]) * ~not_ineligible diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_spouses.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_spouses.py new file mode 100644 index 00000000000..e4fae3d4c37 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/_ia_ssa_spouses.py @@ -0,0 +1,10 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) + + +def ia_ssa_spouse_value(person, period, values): + """The co-resident spouse's value, excluding unrelated household adults.""" + spouse = _ssi_spouse_index(person, period.this_year) + return where(spouse >= 0, values[np.maximum(spouse, 0)], np.zeros_like(values)) diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa.py index b6529263b3b..f6f1301741c 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa.py @@ -1,4 +1,10 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_spouses import ( + ia_ssa_spouse_value, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_smme_spouse_income import ( ia_ssa_smme_spouse_is_sga_applicant, ) @@ -68,7 +74,9 @@ def formula(person, period, parameters): # living expenses from the previous living arrangement, but do not # disregard an amount that is over the SSI standard". The model follows # the rule's fixed amount (ia_ssa.yaml Cases 52-53). - has_spouse = person("ia_ssa_has_spouse", period) + has_spouse = person("ia_ssa_has_spouse", period) | person( + "ia_ssa_rcf_couple_budget", period + ) entry = where(has_spouse, federal.amount.couple, federal.amount.individual) entry = where( person("ia_ssa_rcf_couple_budget", period), entry / 2, entry @@ -95,10 +103,12 @@ def formula(person, period, parameters): if source not in excluded: has_other_income = has_other_income | (person(source, period) > 0) has_other_income = where( - joint, person.marital_unit.any(has_other_income), has_other_income + joint, + has_other_income | ia_ssa_spouse_value(person, period, has_other_income), + has_other_income, ) flh = category == c.FAMILY_LIFE_HOME - recipients = person.marital_unit.sum(flh) + recipients = _ssi_couple_total(person, period.this_year, flh.astype(int)) top_up = ( ~has_other_income * receives_ssi diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_countable_income.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_countable_income.py index 087810af14f..db306fbb4f0 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_countable_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_countable_income.py @@ -1,4 +1,11 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import ( + state_couple_total, + state_is_married, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -11,7 +18,7 @@ class ia_ssa_care_countable_income(Variable): definition_period = MONTH unit = USD defined_for = StateCode.IA - documentation = "Care income without the $20 general exclusion: SSI countable income plus SSI. In-home care counts an adult's spouse's income. An eligible family-life home couple, and a residential care couple in the month of entry, split the couple's income. After the month of entry, a married resident counts own income plus the SSI individual amount from it. A child in in-home care receives income deemed from parents under the Iowa method. Interest and dividends are excluded. First-month deeming from an ineligible spouse, and deductions absent from the federal SSI income inputs, are not modeled." + documentation = "Care income without the $20 general exclusion: SSI countable income plus SSI. In-home care counts an adult's spouse's income. An eligible family-life home couple, and a residential care couple in the month of entry, split the couple's income. A retained prior-SSI entry couple now living apart uses the SSI couple amount calculated from its combined income. After the month of entry, a married resident counts own income plus the SSI individual amount from it. A child in in-home care receives income deemed from parents under the Iowa method. Interest and dividends are excluded. First-month deeming from an ineligible spouse, and deductions absent from the federal SSI income inputs, are not modeled." reference = ( # 441 IAC 52.1(3)"a" "https://www.legis.iowa.gov/docs/iac/chapter/441.52.pdf#page=1", @@ -84,9 +91,8 @@ def formula(person, period, parameters): # income is below the SSI rate "must now apply for and accept SSI # benefits" (p.61). The model therefore assumes SSA pays each spouse as # an individual from that month, and credits that amount even to a - # resident who does not take up SSI. Federal SSI has no input for - # spouses who live apart and keeps the couple or deemed amount, so the - # SSI individual amount is computed here from the resident's own income + # resident who does not take up SSI. The SSI individual amount is + # computed here from the resident's own income # (ia_ssa.yaml Cases 57, 58, 60 and 73). individual_ssi = max_( ssi.amount.individual @@ -99,11 +105,61 @@ def formula(person, period, parameters): / MONTHS_IN_YEAR, 0, ) - separated = rcf & spouse & ~person("ia_ssa_care_initial_partial_month", period) + separated = ( + rcf + & state_is_married(person, period.this_year) + & ~person("ia_ssa_care_initial_partial_month", period) + ) benefit = where(separated, individual_ssi, person("ssi", period)) - earned = where(combine, person.marital_unit.sum(earned), earned) - unearned = where(combine, person.marital_unit.sum(unearned), unearned) - benefit = where(combine, person.marital_unit.sum(benefit), benefit) + # RCF's prior-SSI entry budget retains the marriage across facilities; + # other care paths pool only with the spouse living in the home. + earned = where( + combine, + where( + rcf, + state_couple_total(person, period.this_year, earned), + _ssi_couple_total(person, period.this_year, earned), + ), + earned, + ) + unearned = where( + combine, + where( + rcf, + state_couple_total(person, period.this_year, unearned), + _ssi_couple_total(person, period.this_year, unearned), + ), + unearned, + ) + benefit = where( + combine, + where( + rcf, + state_couple_total(person, period.this_year, benefit), + _ssi_couple_total(person, period.this_year, benefit), + ), + benefit, + ) + # 6-B's entry-month example (PDF p.69) retains couple SSI of 1,236 + # for pensions of 175 and 100: 1,491 - (275 - 20). Current separate + # households make federal SSI individual immediately in this static + # model, so pooling those payments would overstate entry-month care + # income. Reconstruct only this retained Iowa entry budget, as the + # individual RCF amount above is reconstructed after entry. This uses + # the regular RCF SSI rate and the income inputs already used for + # care; it does not change federal timing or the documented gaps. + retained_entry_ssi = max_( + ssi.amount.couple + - _apply_ssi_exclusions( + earned * MONTHS_IN_YEAR, + unearned * MONTHS_IN_YEAR, + parameters, + period, + ) + / MONTHS_IN_YEAR, + 0, + ) + benefit = where(rcf & combine & ~spouse, retained_entry_ssi, benefit) income = ( _apply_ssi_exclusions( earned * MONTHS_IN_YEAR, diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_initial_partial_month.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_initial_partial_month.py index 9ebe3315ce4..d4adba7bf90 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_initial_partial_month.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_care_initial_partial_month.py @@ -8,7 +8,7 @@ class ia_ssa_care_initial_partial_month(Variable): definition_period = MONTH defined_for = StateCode.IA default_value = False - documentation = "Set for a first month of in-home health-related care service that is less than a full month, or for the month a resident enters a residential care facility after living at home earlier that month. In-home care then requires no client participation. Residential care deducts the SSI benefit rate for one person, or for a couple when a spouse lived in the home. For a married resident, the month also sets the couple budget (ia_ssa_rcf_couple_budget) when either spouse received SSI or both spouses enter that month; later months budget each spouse alone. Key the entry to its month and enter the covered days in ia_ssa_care_partial_month_days." + documentation = "Set for a first month of in-home health-related care service that is less than a full month, or for the month a resident enters a residential care facility after living at home earlier that month. In-home care then requires no client participation. Residential care deducts the SSI benefit rate for one person, or for a couple when a spouse lived in the home. For a married resident, the month also sets the couple budget (ia_ssa_rcf_couple_budget) when either spouse received SSI before entry or both spouses enter the same room that month; later months budget each spouse alone. Spouses in separate facilities must record prior receipt in ia_ssa_rcf_received_ssi_before_entry; same-room entry without prior SSI requires ia_ssa_rcf_shares_room_with_spouse. Key the entry to its month and enter the covered days in ia_ssa_care_partial_month_days." reference = ( # 441 IAC 52.1(3)"a"(6) "https://www.legis.iowa.gov/docs/iac/chapter/441.52.pdf#page=2", diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_category.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_category.py index c85f31d07de..d437b3e39d3 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_category.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_category.py @@ -30,6 +30,10 @@ class ia_ssa_category(Variable): resources, which 441 IAC 51.5(2) counts with the recipient's; the model tests only the recipient's and spouse's resources. For residential care, set ia_ssa_care_initial_partial_month in the month - of entry; a married resident is budgeted alone from the next month. + of entry; a married resident is budgeted alone from the next month. If + spouses enter separate facilities, ia_ssa_rcf_received_ssi_before_entry + records prior SSI receipt for their entry-month couple budget. Without + prior SSI, the same-month couple entry rule also requires + ia_ssa_rcf_shares_room_with_spouse. NONE also excludes the blind allowance; otherwise it may accompany care. """ diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py index 9bdb207b59b..e3390ac1274 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import state_couple_total +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ia_ssa_eligible(Variable): @@ -33,16 +37,14 @@ def formula(person, period, parameters): # resident lives apart from the spouse after the month of entry (6-B # p.60), so only the couple-budget entry month pools resources # (ia_ssa.yaml Case 61). - # NOTE: The spouse who stays at home still pools resources with the - # resident after the month of entry, though p.12 would test that - # spouse alone. Federal SSI in the model has no input for spouses who - # live apart and keeps them a couple, and that spouse's SSA income - # comes from federal SSI, so this belongs with the federal treatment of - # spouses living apart. - couple = person("ia_ssa_has_spouse", period) & ( - community | person("ia_ssa_rcf_couple_budget", period) + entry_couple = person("ia_ssa_rcf_couple_budget", period) + couple = (person("ia_ssa_has_spouse", period) & community) | entry_couple + combined = where( + entry_couple, + state_couple_total(person, period.this_year, resource), + _ssi_couple_total(person, period.this_year, resource), ) - resource = where(couple, person.marital_unit.sum(resource), resource) + resource = where(couple, combined, resource) resource = resource + person("ia_ssa_resources_deemed_from_parents", period) # 6-B Resources prints "$2,000 for an individual" and "$3,000 for a # married couple living together" under 20 CFR 416.1202; these equal diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_federal.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_federal.py index 920d3952c4f..7b4aef0dc93 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_federal.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_federal.py @@ -1,4 +1,10 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_spouses import ( + ia_ssa_spouse_value, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi._ssi_state_supplement import ( ssi_state_supplement, ) @@ -31,14 +37,22 @@ def formula(person, period, parameters): # not apply as part of a joint claim (issue 10243). joint = person("ssi_couple_computation_applies", period) blind = person("is_blind", period.this_year) - blind_count = person.marital_unit.sum(blind) + blind_count = _ssi_couple_total(person, period.this_year, blind.astype(int)) dependent = person("ia_ssa_dependent_countable_income", period) - dependent = where(joint, person.marital_unit.max(dependent), dependent) + dependent = where( + joint, + max_(dependent, ia_ssa_spouse_value(person, period, dependent)), + dependent, + ) recipient_income = person("ssi_countable_income", period) + person( "ssi", period ) group_income = ( - where(joint, person.marital_unit.sum(recipient_income), recipient_income) + where( + joint, + _ssi_couple_total(person, period.this_year, recipient_income), + recipient_income, + ) + dependent ) need = ( @@ -60,7 +74,9 @@ def formula(person, period, parameters): # couple applies the couple level (integration.yaml Cases 11-12), # whatever the other spouse's category. certified = category == c.DEPENDENT_PERSON - certified = where(joint, person.marital_unit.any(certified), certified) + certified = where( + joint, certified | ia_ssa_spouse_value(person, period, certified), certified + ) dp = certified & (dependent < p.dependent_amount) & (group_income < need) # 6-B: the family-life home standard is "$1,156", and the maximum # supplement is "$162 ($1,156 - $994 SSI standard)". SSA pays at most @@ -108,7 +124,7 @@ def formula(person, period, parameters): + 2 * blind * p.blind_amount ) recipient = (individual_rate > 0) & ~medical & person("ia_ssa_eligible", period) - count = person.marital_unit.sum(recipient) + count = _ssi_couple_total(person, period.this_year, recipient.astype(int)) payment = ssi_state_supplement( person, period, diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_has_spouse.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_has_spouse.py index a5b4b549cd7..2dcb0111977 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_has_spouse.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_has_spouse.py @@ -7,7 +7,7 @@ class ia_ssa_has_spouse(Variable): value_type = bool entity = Person - label = "Iowa SSA spouse in the marital unit" + label = "Iowa SSA spouse living with the person" definition_period = MONTH defined_for = StateCode.IA reference = ( @@ -17,10 +17,7 @@ class ia_ssa_has_spouse(Variable): def formula(person, period, parameters): # 6-B Resources p.12: "When the applicant is living with a spouse, - # count the spouse's resources." A two-person marital unit is a couple - # only when SSI's spouse test pairs its members, so the default unit of - # a situation without marital units does not pair a parent with an - # adult child the parent claims (ia_ssa.yaml Case 65). A default unit - # of three or more people budgets each member alone. - couple_unit = person.marital_unit.nb_persons() == 2 - return couple_unit & (_ssi_spouse_index(person, period) >= 0) + # count the spouse's resources." SSI's spouse lookup also identifies + # the actual couple in a default marital unit with unrelated adults, + # and excludes a parent and the adult child they claim as a dependent. + return _ssi_spouse_index(person, period.this_year) >= 0 diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_amount.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_amount.py index 0c695d18a33..03ccbb0dc88 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_amount.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_amount.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ia_ssa_ihhrc_amount(Variable): @@ -30,7 +33,7 @@ def formula(person, period, parameters): # A couple's approved costs share one participation; others are alone. total_cost = where( person("ia_ssa_has_spouse", period), - person.marital_unit.sum(covered_cost), + _ssi_couple_total(person, period.this_year, covered_cost), covered_cost, ) payable = max_(total_cost - person("ia_ssa_ihhrc_participation", period), 0) diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_participation.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_participation.py index 08130590781..f3b6cbaceed 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_participation.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_ihhrc_participation.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ia_ssa_ihhrc_participation(Variable): @@ -28,7 +31,11 @@ def formula(person, period, parameters): deductions = max_(person("ia_ssa_care_additional_deductions", period), 0) has_spouse = person("ia_ssa_has_spouse", period) maintenance = where(has_spouse, ssi.amount.couple, ssi.amount.individual) - deductions = where(has_spouse, person.marital_unit.sum(deductions), deductions) + deductions = where( + has_spouse, + _ssi_couple_total(person, period.this_year, deductions), + deductions, + ) # 441 IAC 177.2(1)"f"(3) disregards "The amount of the SSI standard for # a dependent plus any established unmet medical needs for each # dependent living in the home. Any income of the dependent shall be diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_couple_budget.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_couple_budget.py index 25173adcd27..f4b4539c380 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_couple_budget.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_couple_budget.py @@ -1,4 +1,11 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import ( + state_couple_total, + state_is_married, +) +from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_spouses import ( + ia_ssa_spouse_value, +) class ia_ssa_rcf_couple_budget(Variable): @@ -23,8 +30,7 @@ def formula(person, period, parameters): # spouses separately beginning in the month of entry." 441 IAC 51.4(3) # makes "Partners in a marriage who both enter the same room of the # residential care facility in the same month" a couple for "the - # initial month"; spouses who both enter in the same month are taken - # to share a room (ia_ssa.yaml Cases 57, 59 and 60). A resident with an + # initial month"; sharing a room is an explicit input. A resident with an # ineligible spouse is budgeted alone (ia_ssa.yaml Case 73); SSI's # first-month deemed income from that spouse (6-B p.60) is not modeled. # NOTE: A spouse who enters after the other spouse's month of entry @@ -36,10 +42,38 @@ def formula(person, period, parameters): category = person("ia_ssa_category", period) rcf = category == category.possible_values.RESIDENTIAL_CARE entering = rcf & person("ia_ssa_care_initial_partial_month", period) - couple = person("ssi_couple_computation_applies", period) & person( - "ia_ssa_has_spouse", period + year = period.this_year + # The prior-SSI entry-month rule retains the declared marriage even + # when the spouses have moved to separate facilities. Ordinary Iowa + # spouse pooling still requires co-residence (ia_ssa_has_spouse). + couple = state_is_married(person, year) & ( + state_couple_total( + person, year, person("is_ssi_aged_blind_disabled", year).astype(int) + ) + == 2 ) + # Keep the existing SSI computation guard for spouses still living + # together. The state entry exception supplies only the spouse link + # federal SSI drops when the spouses occupy separate households. A + # rejected co-resident pair (such as a parent and adult child in the + # default marital unit) does not establish that the people live apart. + household = person.household.reference_entity.members_entity_id + apart = state_couple_total(person, year, household) != 2 * household + couple = couple & (apart | person("ssi_couple_computation_applies", period)) receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) - couple_received_ssi = person.marital_unit.any(receives_ssi) - both_enter = person.marital_unit.sum(entering) == 2 - return entering & couple & (couple_received_ssi | both_enter) + # Separated spouses need an explicit pre-entry receipt fact. For + # co-resident legacy situations, current receipt still approximates + # pre-entry receipt; a newly awarded current-month payment cannot + # establish prior receipt for spouses now in separate households. + receives_ssi = person("ia_ssa_rcf_received_ssi_before_entry", period) | ( + person("ia_ssa_has_spouse", period) & receives_ssi + ) + couple_received_ssi = ( + state_couple_total(person, year, receives_ssi.astype(int)) > 0 + ) + both_enter = state_couple_total(person, year, entering.astype(int)) == 2 + room = person("ia_ssa_rcf_shares_room_with_spouse", period) + same_room = person("ia_ssa_has_spouse", period) & ( + room | ia_ssa_spouse_value(person, period, room) + ) + return entering & couple & (couple_received_ssi | (both_enter & same_room)) diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_received_ssi_before_entry.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_received_ssi_before_entry.py new file mode 100644 index 00000000000..8e5f15ce31a --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_received_ssi_before_entry.py @@ -0,0 +1,12 @@ +from policyengine_us.model_api import * + + +class ia_ssa_rcf_received_ssi_before_entry(Variable): + value_type = bool + entity = Person + label = "Iowa SSA residential care resident received SSI before entry" + definition_period = MONTH + defined_for = StateCode.IA + default_value = False + documentation = "Receipt of SSI before the residential-care entry recorded in this month, rather than an individual SSI award after the spouses separate. Receipt by either spouse establishes the prior-SSI entry condition for the couple." + reference = "https://hhs.iowa.gov/media/3987/download?inline=#page=60" diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_shares_room_with_spouse.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_shares_room_with_spouse.py new file mode 100644 index 00000000000..5f1adf73fc5 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_rcf_shares_room_with_spouse.py @@ -0,0 +1,12 @@ +from policyengine_us.model_api import * + + +class ia_ssa_rcf_shares_room_with_spouse(Variable): + value_type = bool + entity = Person + label = "Iowa SSA residential care resident shares a room with the spouse" + definition_period = MONTH + defined_for = StateCode.IA + default_value = False + documentation = "Both spouses occupy the same room of the residential care facility. Reporting this fact for either co-resident spouse applies it to both. Without prior SSI receipt, the entry-month couple budget requires this fact as well as entry by both spouses in that month." + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.51.pdf#page=1" diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_countable_income.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_countable_income.py index 031a5593f34..7de641454fa 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_countable_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_countable_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_smme_spouse_income import ( ia_ssa_smme_spouse_income, ia_ssa_smme_spouse_is_sga_applicant, @@ -94,11 +97,13 @@ def formula(person, period, parameters): # combined income, not SSI's deeming from the applicant (Cases 98 and # 102). applicant = deemed & person("ia_ssa_smme_coverage_eligible", period) - combined = person.marital_unit.sum(where(applicant, income, 0)) + combined = _ssi_couple_total( + person, period.this_year, where(applicant, income, 0) + ) income = where( ia_ssa_smme_spouse_is_sga_applicant(person, period), combined, income ) # NOTE: For an eligible couple, ssi_countable_income takes off only # half of a student spouse's earned income exclusion (issue 10456). joint = person("ssi_couple_computation_applies", period) - return where(joint, person.marital_unit.sum(income), income) + return where(joint, _ssi_couple_total(person, period.this_year, income), income) diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_spousal_deeming_applies.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_spousal_deeming_applies.py index b9c434b3e4a..000a75ae777 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_spousal_deeming_applies.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_smme_spousal_deeming_applies.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_spouses import ( + ia_ssa_spouse_value, +) from policyengine_us.variables.gov.states.ia.hhs.ssa._ia_ssa_smme_spouse_income import ( ia_ssa_smme_spouse_income, ia_ssa_smme_spouse_is_not_ineligible, @@ -53,9 +56,8 @@ def formula(person, period, parameters): # the same household" with the spouse. A spouse in a residential care # facility after the month of entry, whom ia_ssa_care_countable_income # budgets alone, is still treated as a household member here. - apart = person.marital_unit.any( - person("ssi_lives_in_medical_treatment_facility", period) - ) + medical = person("ssi_lives_in_medical_treatment_facility", period) + apart = medical | ia_ssa_spouse_value(person, period, medical) # 8-E p.12: "An ineligible spouse is a spouse who is not eligible for # SSI or non-MAGI-related Medicaid, or State Supplementary Assistance." # Those groups are for aged, blind or disabled people (8-F PDF pages diff --git a/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_sapn.py b/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_sapn.py index a70a664f6df..33545090117 100644 --- a/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_sapn.py +++ b/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_sapn.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class in_ssp_sapn(Variable): @@ -21,7 +24,9 @@ def formula(person, period, parameters): p = parameters(period).gov.states["in"].fssa.ssp joint_claim = person("ssi_claim_is_joint", period.this_year) eligible = person("in_ssp_sapn_eligible", period) - both_eligible = person.marital_unit.sum(eligible) == 2 + both_eligible = ( + _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 + ) is_couple = joint_claim & both_eligible ssi_monthly = person("ssi", period) countable_income = person("ssi_countable_income", period) @@ -33,7 +38,9 @@ def formula(person, period, parameters): min_(pna - ssi_monthly - countable_income, max_individual), 0, ) - couple_budgeted = person.marital_unit.sum(ssi_monthly + countable_income) + couple_budgeted = _ssi_couple_total( + person, period.this_year, ssi_monthly + countable_income + ) couple_amount = max_( min_(2 * pna - couple_budgeted, max_couple), 0, diff --git a/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.py b/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.py index 6f67eb9df34..5f939328efd 100644 --- a/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.py +++ b/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_claim_type.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class KYSSPClaimType(Enum): @@ -32,13 +36,13 @@ def formula(person, period, parameters): category = person("ky_ssp_category", period) in_qualifying_category = category != category.possible_values.NONE ssp_categorically_eligible = is_ssi_eligible & is_adult & in_qualifying_category - eligible_count = person.marital_unit.sum(ssp_categorically_eligible) - marital_unit_size = person.marital_unit.nb_persons() + eligible_count = _ssi_couple_total( + person, period.this_year, ssp_categorically_eligible.astype(int) + ) + married = _ssi_spouse_index(person, period.this_year) >= 0 both_eligible = (eligible_count == 2) & ssp_categorically_eligible one_eligible_couple = ( - (eligible_count == 1) - & ssp_categorically_eligible - & (marital_unit_size == 2) + (eligible_count == 1) & ssp_categorically_eligible & married ) return select( [both_eligible, one_eligible_couple], diff --git a/policyengine_us/variables/gov/states/ma/dta/ssp/ma_maximum_state_supplement.py b/policyengine_us/variables/gov/states/ma/dta/ssp/ma_maximum_state_supplement.py index f2d5556496d..6a419742aab 100644 --- a/policyengine_us/variables/gov/states/ma/dta/ssp/ma_maximum_state_supplement.py +++ b/policyengine_us/variables/gov/states/ma/dta/ssp/ma_maximum_state_supplement.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ma_maximum_state_supplement(Variable): @@ -82,14 +85,9 @@ def formula(person, period, parameters): [monthly_aged_rate, monthly_blind_rate, monthly_disabled_rate] ) - # 9. Summation across the marital unit. If we have a couple, we might want - # the total combined amount, then split it. Or we might directly keep it as per-person. - # Here, they sum for the entire marital unit, then later they do dividing if needed. - combined_amount_for_marital_unit = person.marital_unit.sum(monthly_rate) - - # 10. Finally, the code divides by 2 if it's a joint claim, presumably so that - # each person sees their half. If it's not joint, it just returns the full amount. - # This "per-person" approach might be confusing if the state has a special "couple rate" - # that isn't exactly double or half. But that's how the code is set up now. - divisor = where(joint_claim, 2, 1) - return combined_amount_for_marital_unit / divisor + # 9. For a joint claim, combine the two spouses' amounts and split + # them evenly; otherwise keep the person's own amount. Combining the + # spouses rather than the marital unit keeps a third adult in a + # default marital unit out of the couple's amount. + couple_amount = _ssi_couple_total(person, period.this_year, monthly_rate) + return where(joint_claim, couple_amount / 2, monthly_rate) diff --git a/policyengine_us/variables/gov/states/ma/dta/ssp/ma_state_supplement.py b/policyengine_us/variables/gov/states/ma/dta/ssp/ma_state_supplement.py index 784a66aef7b..a15e7e1a531 100644 --- a/policyengine_us/variables/gov/states/ma/dta/ssp/ma_state_supplement.py +++ b/policyengine_us/variables/gov/states/ma/dta/ssp/ma_state_supplement.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ma_state_supplement(Variable): @@ -19,8 +22,11 @@ def formula(person, period, parameters): meets_resource_test = person("meets_ssi_resource_test", period) eligible = abd & meets_resource_test joint_claim = person("ssi_claim_is_joint", period.this_year) + couple_supplement = _ssi_couple_total( + person, period.this_year, state_supplement + ) return where( joint_claim, - person.marital_unit.sum(state_supplement) / 2, + couple_supplement / 2, state_supplement * eligible, ) diff --git a/policyengine_us/variables/gov/states/me/dhhs/ssp/me_ssp.py b/policyengine_us/variables/gov/states/me/dhhs/ssp/me_ssp.py index 10b931b902a..a1975edb950 100644 --- a/policyengine_us/variables/gov/states/me/dhhs/ssp/me_ssp.py +++ b/policyengine_us/variables/gov/states/me/dhhs/ssp/me_ssp.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class me_ssp(Variable): @@ -18,13 +22,14 @@ class me_ssp(Variable): def formula(person, period, parameters): joint_claim = person("ssi_claim_is_joint", period.this_year) eligible = person("me_ssp_eligible", period) - both_eligible = person.marital_unit.sum(eligible) == 2 + both_eligible = ( + _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 + ) category = person("me_ssp_payment_category", period) # Both spouses must share the same payment category to be treated # as a joint couple claim. - shared_category = person.marital_unit.max(category) == person.marital_unit.min( - category - ) + spouse = _ssi_spouse_index(person, period.this_year) + shared_category = category == category[np.maximum(spouse, 0)] couple_applies = joint_claim & both_eligible & shared_category return where( couple_applies, diff --git a/policyengine_us/variables/gov/states/mi/mdhhs/ssp/mi_ssp_couple_eligible.py b/policyengine_us/variables/gov/states/mi/mdhhs/ssp/mi_ssp_couple_eligible.py index f356ddf099f..f668519bc8c 100644 --- a/policyengine_us/variables/gov/states/mi/mdhhs/ssp/mi_ssp_couple_eligible.py +++ b/policyengine_us/variables/gov/states/mi/mdhhs/ssp/mi_ssp_couple_eligible.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) from policyengine_us.variables.gov.states.mi.mdhhs.ssp.mi_ssp_living_arrangement import ( MISSPLivingArrangement, ) @@ -19,22 +23,12 @@ class mi_ssp_couple_eligible(Variable): def formula(person, period, parameters): joint_claim = person("ssi_claim_is_joint", period.this_year) eligible = person("mi_ssp_eligible", period) - # Summing the same variable used in defined_for is safe here; if a - # future contributor swaps to summing a different variable across - # the marital unit, the defined_for filter could silently halve - # the result. - both_eligible = person.marital_unit.sum(eligible) == 2 + both_eligible = ( + _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 + ) category = person("mi_ssp_payment_category", period) - qualifying_arrangements = [ - value - for value in MISSPLivingArrangement - if value != MISSPLivingArrangement.NONE - ] - both_same_arrangement = ( - sum( - person.marital_unit.sum(category == value) == 2 - for value in qualifying_arrangements - ) - > 0 + spouse = _ssi_spouse_index(person, period.this_year) + both_same_arrangement = (category != MISSPLivingArrangement.NONE) & ( + category == category[np.maximum(spouse, 0)] ) return joint_claim & both_eligible & both_same_arrangement diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py index 27de6fc0bc3..ef759e6bc79 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class mn_msa_gross_income_eligible(Variable): @@ -21,9 +24,16 @@ def formula(person, period, parameters): p = parameters(period).gov.states.mn.dhs.msa.eligibility.income_limit ssi_individual_fbr = parameters(period).gov.ssa.ssi.amount.individual categorically_eligible = person("is_ssi_aged_blind_disabled", period.this_year) - both_eligible = person.marital_unit.sum(categorically_eligible) == 2 + both_eligible = ( + _ssi_couple_total( + person, period.this_year, categorically_eligible.astype(int) + ) + == 2 + ) gross = person("mn_msa_gross_income", period) - au_income = where(both_eligible, person.marital_unit.sum(gross), gross) + au_income = where( + both_eligible, _ssi_couple_total(person, period.this_year, gross), gross + ) cap = ssi_individual_fbr * where( both_eligible, p.couple_fbr_multiplier, p.individual_fbr_multiplier ) diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py index bd8a807be09..ceefa00797f 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -55,8 +58,8 @@ def formula(person, period, parameters): # SSI track: count FBR as unearned, apply $20 once at AU level for # couples (matches mn_msa_person.py SSI track formula). couple_ssi_countable = max_( - person.marital_unit.sum(ssi_fbr) - + person.marital_unit.sum(raw_unearned) + _ssi_couple_total(person, period.this_year, ssi_fbr) + + _ssi_couple_total(person, period.this_year, raw_unearned) - disregard, 0, ) @@ -83,8 +86,8 @@ def formula(person, period, parameters): "ssi_unearned_income_deemed_from_ineligible_parent", ], ) - couple_earned = person.marital_unit.sum(annual_earned) - couple_unearned = person.marital_unit.sum(annual_unearned) + couple_earned = _ssi_couple_total(person, period.this_year, annual_earned) + couple_unearned = _ssi_couple_total(person, period.this_year, annual_unearned) couple_non_ssi_countable = ( _apply_ssi_exclusions(couple_earned, couple_unearned, parameters, period) / MONTHS_IN_YEAR @@ -98,11 +101,13 @@ def formula(person, period, parameters): ) # For couples both ssi_countable and non_ssi_countable are already - # AU-aggregate (computed via marital_unit.sum); the couple standard + # aggregated over the recipient and spouse; the couple standard # is the AU total, so compare directly. countable = where(receives_ssi, ssi_countable, non_ssi_countable) special_needs = person("mn_msa_special_needs_total", period) need_standard = person("mn_msa_assistance_standard", period) + where( - is_couple, person.marital_unit.sum(special_needs), special_needs + is_couple, + _ssi_couple_total(person, period.this_year, special_needs), + special_needs, ) return countable <= need_standard diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py index dcbab010738..70214720794 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class mn_msa_resource_eligible(Variable): @@ -25,5 +28,7 @@ def formula(person, period, parameters): p = parameters(period).gov.states.mn.dhs.msa.eligibility receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) countable_resources = person("ssi_countable_resources", period.this_year) - unit_resources = person.marital_unit.sum(countable_resources) + unit_resources = _ssi_couple_total( + person, period.this_year, countable_resources + ) return receives_ssi | (unit_resources <= p.asset_limit.non_ssi_track) diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py b/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py index 4019169214c..844b39ab6ff 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -30,7 +33,7 @@ def formula(person, period, parameters): # The SSI track substitutes the federal SSI FBR for the recipient's # post-disregard SSI payment so MSA tops up against the FBR rather # than the federal payment. FLA-D recipients are exempt from the $20 - # general disregard. Couples are computed at the marital unit then + # general disregard. Couples are computed using only the two spouses then # split 50/50 to avoid wasting disregards on asymmetric income. arrangement = person("mn_msa_payment_category", period) LA = arrangement.possible_values @@ -43,7 +46,9 @@ def formula(person, period, parameters): need_standard = standard + special_needs # Couple income is tested at the assistance-unit level, but modeled # special needs remain attached to the spouse with the allowance. - couple_special_needs = person.marital_unit.sum(special_needs) + couple_special_needs = _ssi_couple_total( + person, period.this_year, special_needs + ) special_needs_share = special_needs / max_(couple_special_needs, 1) ssi = person("ssi", period) receives_ssi = ssi > 0 @@ -60,11 +65,11 @@ def formula(person, period, parameters): ) disregard = where(is_medicaid_facility, 0, p.general) - # SSI track: aggregate FBR + unearned at marital unit for couples, + # SSI track: aggregate FBR + unearned for the two spouses, # apply $20 once, then split supplement 50/50. couple_ssi_countable = max_( - person.marital_unit.sum(ssi_fbr) - + person.marital_unit.sum(raw_unearned) + _ssi_couple_total(person, period.this_year, ssi_fbr) + + _ssi_couple_total(person, period.this_year, raw_unearned) - disregard, 0, ) @@ -97,8 +102,8 @@ def formula(person, period, parameters): "ssi_unearned_income_deemed_from_ineligible_parent", ], ) - couple_earned = person.marital_unit.sum(annual_earned) - couple_unearned = person.marital_unit.sum(annual_unearned) + couple_earned = _ssi_couple_total(person, period.this_year, annual_earned) + couple_unearned = _ssi_couple_total(person, period.this_year, annual_unearned) couple_non_ssi_countable_annual = _apply_ssi_exclusions( couple_earned, couple_unearned, parameters, period ) diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py index 514d50a00b3..0bfaf98b1bc 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class mn_msa_guardian_fee(Variable): @@ -20,7 +23,7 @@ def formula(person, period, parameters): # monthly income up to a maximum of $100 per month. Per CM 0017, # for SSI recipients count the full SSI FBR as gross unearned # income (even when the actual SSI check is less). Aggregate to - # the marital unit before applying the cap. + # the recipient and spouse before applying the cap. # Minnesota DHS correspondence (2026-10-19) reports that approved # guardian fees are $100 in practice regardless of income. # PolicyEngine follows the statute's 5%/$100 cap, which governs. @@ -28,7 +31,7 @@ def formula(person, period, parameters): has_guardian = person("mn_msa_has_guardian", period) guardian_is_county_staff = person("mn_msa_guardian_is_county_staff", period) gross = person("mn_msa_gross_income", period) - unit_gross = person.marital_unit.sum(gross) + unit_gross = _ssi_couple_total(person, period.this_year, gross) unit_fee = min_(unit_gross * p.rate, p.max_amount) # Minn. Stat. § 256D.44 Subd. 5(d) allows no fee for county agency staff. return has_guardian * ~guardian_is_county_staff * unit_fee diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_payment_category.py b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_payment_category.py index 308dbcba87c..10f67c30c98 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_payment_category.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_payment_category.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.mn.dhs.msa.mn_msa_living_arrangement import ( MNMSALivingArrangement, ) @@ -36,7 +39,12 @@ def formula(person, period, parameters): ) apply_housing_standard = where( is_couple, - person.marital_unit.any(housing_assistance_eligible), + ( + _ssi_couple_total( + person, period.this_year, housing_assistance_eligible.astype(int) + ) + > 0 + ), housing_assistance_eligible, ) housing_standard_arrangement = select( diff --git a/policyengine_us/variables/gov/states/mo/dss/ssp/eligibility/mo_ssp_resource_eligible.py b/policyengine_us/variables/gov/states/mo/dss/ssp/eligibility/mo_ssp_resource_eligible.py index 5ec42a8f935..08a3ea8582f 100644 --- a/policyengine_us/variables/gov/states/mo/dss/ssp/eligibility/mo_ssp_resource_eligible.py +++ b/policyengine_us/variables/gov/states/mo/dss/ssp/eligibility/mo_ssp_resource_eligible.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import ( + state_couple_total, + state_is_married, +) class mo_ssp_resource_eligible(Variable): @@ -27,14 +31,15 @@ def formula(person, period, parameters): ) # The couple limit applies to property of either spouse when the - # claimant is married and living with the spouse (§ 1035.005.00), so - # the test follows the claimant's marital unit rather than the SPM unit. - # Spouses separated to receive medical or nursing home care also use - # the couple limit; that separation is not modeled. + # claimant is married and living with the spouse (§ 1035.005.00). + # RSMo 208.010.1 also treats spouses separated to obtain medical or + # nursing care as living together, retaining the declared marriage. personal_resources = person("ssi_countable_resources", period.this_year) - married = person.marital_unit.nb_persons() == 2 + married = state_is_married(person, period.this_year) countable_resources = where( - married, person.marital_unit.sum(personal_resources), personal_resources + married, + state_couple_total(person, period.this_year, personal_resources), + personal_resources, ) p = parameters(period).gov.states.mo.dss.ssp.eligibility.resource_limit diff --git a/policyengine_us/variables/gov/states/ne/dhhs/aabd/ne_aabd_couple_rate_applies.py b/policyengine_us/variables/gov/states/ne/dhhs/aabd/ne_aabd_couple_rate_applies.py index f69afb0d78d..ad89d42c1d6 100644 --- a/policyengine_us/variables/gov/states/ne/dhhs/aabd/ne_aabd_couple_rate_applies.py +++ b/policyengine_us/variables/gov/states/ne/dhhs/aabd/ne_aabd_couple_rate_applies.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class ne_aabd_couple_rate_applies(Variable): @@ -21,12 +24,16 @@ def formula(person, period): # is_ssi_eligible is used (not ne_aabd_eligible) to avoid a # circular dependency on the benefit calculation. joint_claim = person("ssi_claim_is_joint", period.this_year) - unit_size = person.marital_unit.nb_persons() both_ssi_eligible = ( - person.marital_unit.sum(person("is_ssi_eligible", period)) == unit_size + _ssi_couple_total( + person, period.this_year, person("is_ssi_eligible", period).astype(int) + ) + == 2 ) living_arrangement = person("ne_aabd_living_arrangement", period) LA = living_arrangement.possible_values is_independent = living_arrangement == LA.INDEPENDENT - both_independent = person.marital_unit.sum(is_independent) == unit_size + both_independent = ( + _ssi_couple_total(person, period.this_year, is_independent.astype(int)) == 2 + ) return joint_claim & both_ssi_eligible & both_independent diff --git a/policyengine_us/variables/gov/states/nj/njdhs/ssp/nj_ssp_living_arrangement.py b/policyengine_us/variables/gov/states/nj/njdhs/ssp/nj_ssp_living_arrangement.py index f652d78b1c9..5166fa07988 100644 --- a/policyengine_us/variables/gov/states/nj/njdhs/ssp/nj_ssp_living_arrangement.py +++ b/policyengine_us/variables/gov/states/nj/njdhs/ssp/nj_ssp_living_arrangement.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.states.nj.njdhs.ssp.nj_ssp_care_setting import ( NJSSPCareSetting, ) @@ -42,10 +45,15 @@ def formula(person, period, parameters): other_medical = ( person("ssi_lives_in_medical_treatment_facility", period) & ~medical ) - ineligible_spouse = person.marital_unit.any( - person("is_ssi_ineligible_spouse", period.this_year) + ineligible_spouse = ( + _ssi_couple_total( + person, + period.this_year, + person("is_ssi_ineligible_spouse", period.this_year).astype(int), + ) + > 0 ) - no_others = person.household.nb_persons() == person.marital_unit.nb_persons() + no_others = person.household.nb_persons() == 2 return select( [ (care == NJSSPCareSetting.EXCLUDED) | other_medical, diff --git a/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_living_arrangement.py b/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_living_arrangement.py index 116ef0272ff..23fdfded77e 100644 --- a/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_living_arrangement.py +++ b/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_living_arrangement.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( SSIFederalLivingArrangement, ) @@ -65,7 +68,10 @@ def formula(person, period, parameters): & (care == NYSSPCareSetting.NONE) ) housemates = person.household.sum(housemate) - housemate - spouse = person.marital_unit.sum(housemate) - housemate + spouse = ( + _ssi_couple_total(person, period.this_year, housemate.astype(int)) + - housemate + ) alone = housemates == where(joint, spouse, 0) minor = person("ny_ssp_is_minor", period.this_year) return select( diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py index ff1cde2f8e8..914e9d08d38 100644 --- a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class oh_rss(Variable): @@ -32,7 +35,7 @@ def formula(person, period, parameters): # SSI reform carries over to RSS. limits = parameters(period).gov.ssa.ssi.eligibility.resources.limit eligible = person("oh_rss_eligible", period) - couple = person.marital_unit.sum(eligible) == 2 + couple = _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 # NOTE: When only one spouse qualifies, 5160:1-5-01(E)(3) applies the # individual standard, so the model tests own resources against # $2,000. Ohio's Medicaid resource rules, 5160:1-3-05.1(B)(9)(b) and @@ -42,7 +45,9 @@ def formula(person, period, parameters): # the SSI resource test both combine the couple's resources, so a # couple above $3,000 fails the Medicaid condition. resources = person("ssi_countable_resources", period.this_year) - resources = where(couple, person.marital_unit.sum(resources), resources) + resources = where( + couple, _ssi_couple_total(person, period.this_year, resources), resources + ) limit = where(couple, limits.couple, limits.individual) payment = max_( p.allowable_fee diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py index 8988542e5be..bffd9507540 100644 --- a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -19,7 +22,7 @@ class oh_rss_countable_income(Variable): def formula(person, period, parameters): # RSS does not deem income from an ineligible spouse or parent. eligible = person("oh_rss_eligible", period) - couple = person.marital_unit.sum(eligible) == 2 + couple = _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 earned = max_( person("ssi_earned_income", period) - person("ssi_blind_or_disabled_working_student_exclusion", period), @@ -32,8 +35,12 @@ def formula(person, period, parameters): - person("dividend_income", period), 0, ) - earned = where(couple, person.marital_unit.sum(earned), earned) - unearned = where(couple, person.marital_unit.sum(unearned), unearned) + earned = where( + couple, _ssi_couple_total(person, period.this_year, earned), earned + ) + unearned = where( + couple, _ssi_couple_total(person, period.this_year, unearned), unearned + ) # NOTE: PolicyEngine's federal SSI still counts interest and dividends # (#10079), so for an SSI recipient with that income the SSI counted # here is understated and RSS is overstated by the same amount. @@ -41,7 +48,11 @@ def formula(person, period, parameters): # status, so a spouse who declines SSI is still budgeted jointly # (#10138). federal_ssi = person("ssi", period) - federal_ssi = where(couple, person.marital_unit.sum(federal_ssi), federal_ssi) + federal_ssi = where( + couple, + _ssi_couple_total(person, period.this_year, federal_ssi), + federal_ssi, + ) # Ohio applies the same $20 general, $65 earned and one-half earned # income exclusions (5160:1-3-03.2(D)(2)(l), (E)(5), (E)(7)), so the SSI # exclusion parameters are used and an SSI reform carries over to RSS. diff --git a/policyengine_us/variables/gov/states/ok/dhs/ssp/_ok_ssp_income.py b/policyengine_us/variables/gov/states/ok/dhs/ssp/_ok_ssp_income.py index b6b072a38e3..a738c6f9464 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/ssp/_ok_ssp_income.py +++ b/policyengine_us/variables/gov/states/ok/dhs/ssp/_ok_ssp_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -69,16 +72,16 @@ def ok_ssp_income(person, period, parameters, *, combined): allocation = ~eligible * person("ok_ssp_spouse_child_allocation", period) earned = max_(earned - max_(allocation - unearned, 0), 0) unearned = max_(unearned - allocation, 0) - earned = person.marital_unit.sum(earned) - unearned = person.marital_unit.sum(unearned) + earned = _ssi_couple_total(person, period.this_year, earned) + unearned = _ssi_couple_total(person, period.this_year, unearned) # 317:35-5-42(l)(2) excludes an ineligible spouse's SSI. Its exclusion # of "any income that was counted or excluded in figuring the amount # of that payment" is not modeled; the spouse's other income stays # counted. - benefit = person.marital_unit.sum(benefit * eligible) + benefit = _ssi_couple_total(person, period.this_year, benefit * eligible) # 317:35-5-42(b)(8)(A) counts the one-third value once, for the # individual or the eligible couple, never for an ineligible spouse. - shelter = person.marital_unit.sum(shelter * eligible) + shelter = _ssi_couple_total(person, period.this_year, shelter * eligible) else: unearned = unearned + ism countable = ( diff --git a/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp.py b/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp.py index 83cccfe156d..288b98c1645 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp.py +++ b/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class ok_ssp(Variable): @@ -23,7 +27,7 @@ def formula(person, period, parameters): individual_standard = ssi.individual + p.maximum_payment couple_standard = ssi.couple + 2 * p.maximum_payment eligible = person("ok_ssp_eligible", period) - joint = person.marital_unit.sum(eligible) == 2 + joint = _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 # NOTE: OAC 340:15-1-2 defines an ineligible spouse as one who is not # aged, blind or disabled, and is silent on an aged, blind or disabled # spouse who is not eligible. 340:15-1-4(10) applies 317:35-5-42, whose @@ -37,7 +41,12 @@ def formula(person, period, parameters): arrangement == arrangement.possible_values.MEDICAL_TREATMENT_FACILITY ) apart = in_facility | person("is_incarcerated", period) - ineligible_spouse = person.marital_unit.sum(~eligible & ~apart) > 0 + spouse = _ssi_spouse_index(person, period.this_year) + ineligible_spouse = ( + (spouse >= 0) + & ~eligible[np.maximum(spouse, 0)] + & ~apart[np.maximum(spouse, 0)] + ) own = person("ok_ssp_own_countable_income", period) combined = person("ok_ssp_countable_income", period) standard = where( @@ -51,7 +60,8 @@ def formula(person, period, parameters): # spouse the odd dollar without naming which, so person-level amounts # follow member order while the couple total does not. first_share = np.floor(gap / 2) - first = person.marital_unit.reference_entity.members_position == 0 + position = person.marital_unit.reference_entity.members_position + first = position < position[np.maximum(spouse, 0)] payment = where(joint, where(first, first_share, gap - first_share), gap) # NOTE: 340:15-1-5(c)(6) tests a blind or disabled minor's income plus # deemed parental income against the SSP standard for an individual, diff --git a/policyengine_us/variables/gov/states/or/odhs/ssp/or_ssp.py b/policyengine_us/variables/gov/states/or/odhs/ssp/or_ssp.py index 80da0a570b1..fde81e3f807 100644 --- a/policyengine_us/variables/gov/states/or/odhs/ssp/or_ssp.py +++ b/policyengine_us/variables/gov/states/or/odhs/ssp/or_ssp.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import state_couple_total class or_ssp(Variable): @@ -40,9 +41,14 @@ def formula(person, period, parameters): # Community care and SSI restoration amounts depend on the SSI payment, # so those paths require modeled SSI rather than reported receipt. care = (category == c.COMMUNITY_CARE) & (ssi > 0) - couple = person.marital_unit.sum(care) == 2 + # OAR 461-155-0630(3) applies when each spouse receives SSI and + # community-based care services, including in separate facilities. + # Hub ruling: us-or:ssp-community-care-spouses-separate-facilities. + couple = state_couple_total(person, period.this_year, care.astype(int)) == 2 own_excess = max_(income - standard, 0) - spouse_excess = person.marital_unit.sum(own_excess) - own_excess + spouse_excess = ( + state_couple_total(person, period.this_year, own_excess) - own_excess + ) couple_payment = max_(standard - income - spouse_excess, 0) interim = (category == c.DISABILITY_INTERIM) | (category == c.NATURALIZATION) restoring_ssi = ( diff --git a/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_care.py b/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_care.py index ca8c5d309fb..ee557fa57f1 100644 --- a/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_care.py +++ b/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_care.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import state_couple_total class sd_ssp_care(Variable): @@ -22,11 +23,22 @@ class sd_ssp_care(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.sd.dss.ssp resources = person("ssi_countable_resources", period.this_year) - # Joint entry applies only when both spouses are flagged. + # ARSD 67:46:04:10(6) and 67:46:05:30 preserve a joint budget when + # both spouses enter long-term care in the same month, including + # placement in different rooms or households. joint_entry = ( - person.marital_unit.sum(person("sd_ssp_care_joint_entry", period)) > 1 + state_couple_total( + person, + period.this_year, + person("sd_ssp_care_joint_entry", period).astype(int), + ) + > 1 + ) + resources = where( + joint_entry, + state_couple_total(person, period.this_year, resources), + resources, ) - resources = where(joint_entry, person.marital_unit.sum(resources), resources) limit = where(joint_entry, p.joint_entry_resource_limit, p.resource_limit) immigration = person("immigration_status", period.this_year) citizenship = (immigration == immigration.possible_values.CITIZEN) | person( @@ -73,9 +85,7 @@ def formula(person, period, parameters): # assisted living or adult foster care is not modeled. 67:46:04:10(7) # and 67:46:05:42(7) count the couple's combined income and resources # in that month; this formula uses the entering spouse's own. - shared_income = person.marital_unit.sum(own_income) / ( - person.marital_unit.nb_persons() - ) + shared_income = state_couple_total(person, period.this_year, own_income) / 2 income = where(joint_entry, shared_income, own_income) net_income = max_(income - person("sd_ssp_care_income_deductions", period), 0) # 67:46:06:09: income is not applied in a partial month unless the diff --git a/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_independent.py b/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_independent.py index 804b695e93b..83927c85334 100644 --- a/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_independent.py +++ b/policyengine_us/variables/gov/states/sd/dss/ssp/sd_ssp_independent.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class sd_ssp_independent(Variable): @@ -53,10 +57,14 @@ def formula(person, period, parameters): # ARSD 67:12:14:03(2) reads per person ("The person is receiving other # income"). couple = person("ssi_claim_is_joint", period.this_year) & ( - person.marital_unit.sum(qualifies) > 1 + _ssi_couple_total(person, period.this_year, qualifies.astype(int)) > 1 ) - other_income = where(couple, person.marital_unit.sum(own_income), own_income) - first = person.marital_unit.reference_entity.members_position == 0 + other_income = where( + couple, _ssi_couple_total(person, period.this_year, own_income), own_income + ) + spouse = _ssi_spouse_index(person, period.this_year) + position = person.marital_unit.reference_entity.members_position + first = position < position[np.maximum(spouse, 0)] amount = where( couple, where(first, p.couple_first_amount, p.couple_second_amount), diff --git a/policyengine_us/variables/gov/states/ut/dws/ssp/ut_dws_ssp.py b/policyengine_us/variables/gov/states/ut/dws/ssp/ut_dws_ssp.py index 2f4d5ad757a..ee91515985c 100644 --- a/policyengine_us/variables/gov/states/ut/dws/ssp/ut_dws_ssp.py +++ b/policyengine_us/variables/gov/states/ut/dws/ssp/ut_dws_ssp.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) from policyengine_us.variables.gov.ssa.ssi.eligibility.status.ssi_federal_living_arrangement import ( SSIFederalLivingArrangement, ) @@ -21,7 +24,7 @@ class ut_dws_ssp(Variable): "https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/ut.html", ) documentation = """ - Spouses are the members of a marital unit. Set + Couple amounts include only the recipient and their co-resident spouse. Set ssi_lives_in_another_persons_household, ssi_receives_shelter_from_others_in_household and ssi_others_pay_all_meals for both spouses to place a couple in the household of another. @@ -33,7 +36,7 @@ def formula(person, period, parameters): # receives half. A person whose spouse does not qualify receives the # single person amount. eligible = person("ut_dws_ssp_eligible", period) - couple = person.marital_unit.sum(eligible) == 2 + couple = _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 # 205-10 pays couples "who live in the household of another" and # couples "who live alone or with others in their own household". SSA # paid the same rows by federal living arrangement codes B and A diff --git a/policyengine_us/variables/gov/states/va/dars/ag/va_ag_countable_income.py b/policyengine_us/variables/gov/states/va/dars/ag/va_ag_countable_income.py index 314011cd7d3..c5164013ec4 100644 --- a/policyengine_us/variables/gov/states/va/dars/ag/va_ag_countable_income.py +++ b/policyengine_us/variables/gov/states/va/dars/ag/va_ag_countable_income.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import state_couple_total from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -20,8 +21,16 @@ class va_ag_countable_income(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.va.dars.ag + # Chapter B section 6 tests co-residence at the first moment of the + # application month; subsequent placement in separate facilities does + # not end the joint initial-month budget recorded by this input. joint = ( - person.marital_unit.sum(person("va_ag_is_joint_initial_month", period)) == 2 + state_couple_total( + person, + period.this_year, + person("va_ag_is_joint_initial_month", period).astype(int), + ) + == 2 ) # NOTE: AG Manual Chapter H's exclusion list (p. 370) does not name the # SSI student earned income exclusion, while Chapter D (p. 123) @@ -36,8 +45,12 @@ def formula(person, period, parameters): - add(person, period, p.excluded_unearned_sources), 0, ) - earned = where(joint, person.marital_unit.sum(earned), earned) - unearned = where(joint, person.marital_unit.sum(unearned), unearned) + earned = where( + joint, state_couple_total(person, period.this_year, earned), earned + ) + unearned = where( + joint, state_couple_total(person, period.this_year, unearned), unearned + ) non_ssi_countable = ( _apply_ssi_exclusions( earned * MONTHS_IN_YEAR, unearned * MONTHS_IN_YEAR, parameters, period diff --git a/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py b/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py index acc268b6f8e..878cb7898ff 100644 --- a/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py +++ b/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states._spouses import state_couple_total class va_ag_eligible(Variable): @@ -14,11 +15,20 @@ def formula(person, period, parameters): immigration = person("immigration_status", period.this_year) citizen = immigration == immigration.possible_values.CITIZEN immigrant = person("is_ssi_qualified_noncitizen", period.this_year) + # Chapter B section 6 preserves the joint initial-month resource + # budget when the spouses subsequently enter separate facilities. joint = ( - person.marital_unit.sum(person("va_ag_is_joint_initial_month", period)) == 2 + state_couple_total( + person, + period.this_year, + person("va_ag_is_joint_initial_month", period).astype(int), + ) + == 2 ) resources = person("ssi_countable_resources", period.this_year) - resources = where(joint, person.marital_unit.sum(resources), resources) + resources = where( + joint, state_couple_total(person, period.this_year, resources), resources + ) limits = parameters(period).gov.ssa.ssi.eligibility.resources.limit receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) resource_eligible = receives_ssi | ( diff --git a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep.py b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep.py index 19a5268f52e..05f9690c703 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep.py +++ b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class vt_aabd_ep(Variable): @@ -36,9 +40,15 @@ def formula(person, period, parameters): # different levels for each spouse; use the highest level certified for # either spouse. certified = abd_eligible & valid_level - person_eligible = abd_eligible & person.marital_unit.any(certified) - rate = person.marital_unit.max(where(certified, p.ep.rate[level], 0)) - count = person.marital_unit.sum(person_eligible) + person_eligible = abd_eligible & ( + _ssi_couple_total(person, period.this_year, certified.astype(int)) > 0 + ) + rates = where(certified, p.ep.rate[level], 0) + spouse_index = _ssi_spouse_index(person, period.this_year) + rate = max_( + rates, where(spouse_index >= 0, rates[np.maximum(spouse_index, 0)], 0) + ) + count = _ssi_couple_total(person, period.this_year, person_eligible.astype(int)) couple = count > 1 # Rule 2784(B)-(C): the individual maximum is the combined couple # payment, the another-household maximum is the combined independent diff --git a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_countable_income.py b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_countable_income.py index 1cc1084e95b..fb8e1764152 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_countable_income.py +++ b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_countable_income.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, + _ssi_spouse_index, +) class vt_aabd_ep_countable_income(Variable): @@ -40,13 +44,15 @@ def formula(person, period, parameters): # reading ssi_earned_income; the ssi term below keeps SSA's treatment. gross_earned = add(person, period, ssi_income.sources.earned) earned = in_household * max_(gross_earned - p.employment_expense, 0) - abd_earned = person.marital_unit.sum(earned * abd) - other_earned = person.marital_unit.sum(earned * ~abd) + abd_earned = _ssi_couple_total(person, period.this_year, earned * abd) + other_earned = _ssi_couple_total(person, period.this_year, earned * ~abd) # NOTE: Rule 2786 withholds the $20 exclusion from VA pension only; with # no pension-specific input, all veterans benefits are treated as pension. veterans = max_(person("veterans_benefits", period), 0) - unearned = person.marital_unit.sum( - in_household * max_(person("ssi_unearned_income", period) - veterans, 0) + unearned = _ssi_couple_total( + person, + period.this_year, + in_household * max_(person("ssi_unearned_income", period) - veterans, 0), ) general = exclusions.general remaining_general = max_(general - unearned, 0) @@ -58,11 +64,18 @@ def formula(person, period, parameters): abd_earned = max_(abd_earned - remaining_general_abd - exclusions.earned, 0) * ( 1 - exclusions.earned_share ) - needs_based = person.marital_unit.sum( + needs_based = _ssi_couple_total( + person, + period.this_year, in_household - * (veterans + person("ssi", period) + person("vt_aabd_federal", period)) + * (veterans + person("ssi", period) + person("vt_aabd_federal", period)), ) - additional = person.marital_unit.max( - max_(person("vt_aabd_ep_additional_countable_income", period), 0) + additional_income = max_( + person("vt_aabd_ep_additional_countable_income", period), 0 + ) + spouse_index = _ssi_spouse_index(person, period.this_year) + additional = max_( + additional_income, + where(spouse_index >= 0, additional_income[np.maximum(spouse_index, 0)], 0), ) return unearned + abd_earned + other_earned + needs_based + additional diff --git a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_is_spouse.py b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_is_spouse.py index c9ea361fbc5..b3e23c5b969 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_is_spouse.py +++ b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_is_spouse.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class vt_aabd_ep_is_spouse(Variable): @@ -12,10 +15,9 @@ class vt_aabd_ep_is_spouse(Variable): def formula(person, period, parameters): abd = person("is_ssi_aged_blind_disabled", period.this_year) - # A marital unit has at most two members, so a non-ABD member of an - # ABD person's marital unit is that person's spouse. + # Only a co-resident spouse can be an ineligible-spouse essential person. return ( abd - & person.marital_unit.any(~abd) + & (_ssi_couple_total(person, period.this_year, (~abd).astype(int)) > 0) & ~person("vt_aabd_ep_nonspouse_essential_person", period) ) diff --git a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_resources_eligible.py b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_resources_eligible.py index b05591f9e52..6067bb5caab 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_resources_eligible.py +++ b/policyengine_us/variables/gov/states/vt/dcf/aabd/vt_aabd_ep_resources_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class vt_aabd_ep_resources_eligible(Variable): @@ -26,4 +29,11 @@ def formula(person, period, parameters): arrangement == la.HOUSEHOLD_OF_ANOTHER ) # Rule 2785 tests the assistance group's combined resources once. - return person.marital_unit.any((receives | passes_state_test) & in_household) + return ( + _ssi_couple_total( + person, + period.this_year, + ((receives | passes_state_test) & in_household).astype(int), + ) + > 0 + ) diff --git a/policyengine_us/variables/gov/states/wa/dshs/ssp/wa_ssp_payment_category.py b/policyengine_us/variables/gov/states/wa/dshs/ssp/wa_ssp_payment_category.py index bb0a06b522c..589798c1d24 100644 --- a/policyengine_us/variables/gov/states/wa/dshs/ssp/wa_ssp_payment_category.py +++ b/policyengine_us/variables/gov/states/wa/dshs/ssp/wa_ssp_payment_category.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) class WAStateSupplementaryPaymentCategory(Enum): @@ -43,12 +46,9 @@ def formula(person, period, parameters): "is_ssi_disabled", period.this_year ) - marital_unit = person.marital_unit - ssi_recipient_count = marital_unit.sum(base_eligible) + spouse = _ssi_spouse_index(person, period.this_year) has_ineligible_spouse = ( - (marital_unit.nb_persons() == 2) - & (ssi_recipient_count == 1) - & base_eligible + (spouse >= 0) & base_eligible & ~base_eligible[np.maximum(spouse, 0)] ) in_standard_category = aged | blind | disabled_qualifies | has_ineligible_spouse diff --git a/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssi_e.py b/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssi_e.py index b011abddf89..952ced28647 100644 --- a/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssi_e.py +++ b/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssi_e.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class wi_ssi_e(Variable): @@ -37,5 +40,5 @@ def formula(person, period, parameters): & person("wi_ssp_eligible", period) & ~household_of_another ) - both = person.marital_unit.sum(certified) == 2 + both = _ssi_couple_total(person, period.this_year, certified.astype(int)) == 2 return certified * where(both, p.couple / 2, p.individual) diff --git a/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssp_regular.py b/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssp_regular.py index 05b9a4b6fcc..5f76f16ef30 100644 --- a/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssp_regular.py +++ b/policyengine_us/variables/gov/states/wi/dhs/ssp/wi_ssp_regular.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_couple_total, +) class wi_ssp_regular(Variable): @@ -13,13 +16,16 @@ class wi_ssp_regular(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.wi.dhs.ssp eligible = person("wi_ssp_eligible", period) - eligible_couple = person.marital_unit.sum(eligible) == 2 + eligible_couple = ( + _ssi_couple_total(person, period.this_year, eligible.astype(int)) == 2 + ) # The one-eligible-spouse rate requires an SSI-ineligible spouse. A spouse # who is SSI-eligible but excluded from the state payment leaves the # recipient on the individual rate. ineligible_spouse = person("is_ssi_ineligible_spouse", period.this_year) has_ineligible_spouse = ( - person.marital_unit.sum(ineligible_spouse) - ineligible_spouse + _ssi_couple_total(person, period.this_year, ineligible_spouse.astype(int)) + - ineligible_spouse ) > 0 arrangement = person("ssi_federal_living_arrangement", period) other_household = (