diff --git a/changelog.d/ssi-ineligible-child-allocation-scope.fixed.md b/changelog.d/ssi-ineligible-child-allocation-scope.fixed.md new file mode 100644 index 00000000000..e4bae6e01ed --- /dev/null +++ b/changelog.d/ssi-ineligible-child-allocation-scope.fixed.md @@ -0,0 +1 @@ +Limit SSI spousal-deeming child allocations to the couple's co-resident children. Scope parental allocations to co-resident children, align SSI and Medicaid recipient immigration rules, preserve state Medicaid allocation age ceilings and child definitions, recognize marriage across households and historical student age limits, and retain monthly allocations through deeming thresholds, exclusions and benefit caps. Use full-time secondary-school attendance, including explicit K-12 enrollment at age 18, rather than tax dependency for Wisconsin's dependent 18-year-old allocation condition, with enrollment proxying expected completion before 19. Preserve annual SSI and Medicaid income inputs while applying allocations, exclusions, floors, and caps monthly. Keep Colorado and Texas couple-budget consumers, New York SSP, Virginia AG, Oklahoma SSP, Colorado OAP, and New Hampshire SSP on those monthly interfaces. diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/categories/senior_or_disabled/income/deeming/ineligible_child_age_limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/categories/senior_or_disabled/income/deeming/ineligible_child_age_limit.yaml new file mode 100644 index 00000000000..038f0873f20 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/categories/senior_or_disabled/income/deeming/ineligible_child_age_limit.yaml @@ -0,0 +1,70 @@ +description: States limit parental Medicaid income allocations to ineligible children below these ages. These overrides constrain the shared SSI allocation definition; states absent from this parameter retain the existing shared allocation behavior. +# The dates mark the shared Medicaid income model's 2015 coverage baseline, +# not an assertion that the states changed their allocation rules in 2015. +# Sources were checked for the current rules in October 2026. +metadata: + unit: year + period: year + label: Medicaid parental allocation ineligible child age limit + breakdown: + - state_code + reference: + - title: California Code of Regulations, title 22, section 50030 - Child + href: https://www.cdss.ca.gov/shd/res/pdf/50101.pdf + - title: Florida ESS Manual 2240.0606.03 - Dependent in Deeming + href: https://ffic.myflfamilies.com/manual/2200.pdf#page=40 + - title: Florida ESS Manual 2640.0212.02 - Determine if Parent has Income to Deem + href: https://ffic.myflfamilies.com/manual/2600.pdf#page=61 + - title: Idaho IDAPA 16.03.05.451.03 - Ineligible Child + href: https://adminrules.idaho.gov/rules/current/16/160305.pdf#page=37 + - title: Louisiana Medicaid Eligibility Manual I-1400 - Ineligible Child + href: https://ldh.la.gov/assets/medicaid/MedicaidEligibilityPolicy/I-1400.pdf#page=4 + - title: Michigan BEM 540 - Allocation to Non-SSI-Related Children + href: https://mdhhs-pres-prod.michigan.gov/olmweb/ex/BP/Public/BEM/540.pdf#page=2 + - title: Missouri DSS 0805.020.15 - Deeming Parental Income, step 1 + href: https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-020-00/0805-020-15/ + - title: Montana Combined Medicaid Manual 603-1 - Ineligible Child Allocation + href: https://prod-dphhs.mt.gov/assets/hcsd/mamanual/CMA603.1.pdf + - title: North Carolina MA-2260 IV.A - Financial Eligibility Requirements + href: https://policies.ncdhhs.gov/wp-content/uploads/MA-2260-Financial-Elig-Requirements-PLA.pdf#page=4 + - title: New Jersey Medicaid Only Manual 10:71-5.5 - Deeming of Income + href: https://nj.gov/humanservices/notices/documents/rules-and-regulations/NJAC%2010_71%20%20MEDICAID%20ONLY.PDF#page=247 + - title: New York Medicaid Reference Guide - Allocation + href: https://www.health.ny.gov/health_care/medicaid/reference/mrg/income.pdf + - title: Ohio Administrative Code 5160:1-3-03.3(B)(2) and (8) - Medicaid deeming of income + href: https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-3-03.3 + - title: Texas MEPD G-2312 - Parent-to-Child Deeming, step 2 + href: https://fhb.hhs.texas.gov/handbooks/medicaid-elderly-people-disabilities-handbook/g-2300-special-income-noninstitutional-budgets + - title: Virginia Medicaid Manual M0530.002 B.1-B.2 - Nonblind Nondisabled Child + href: https://www.dmas.virginia.gov/media/mghpujpo/m05-1-1-26.pdf#page=57 + - title: Wisconsin Medicaid Eligibility Handbook 15.1.2 - Special Financial Tests + href: https://www.emhandbooks.wisconsin.gov/meh-ebd/policy_files/15/15.1.htm + +CA: + 2015-01-01: 21 +FL: + 2015-01-01: 21 +ID: + 2015-01-01: 21 +LA: + 2015-01-01: 21 +MI: + 2015-01-01: 18 +MO: + 2015-01-01: 18 +MT: + 2015-01-01: 21 +NC: + 2015-01-01: 21 +NJ: + 2015-01-01: 18 +NY: + 2015-01-01: 18 +OH: + 2015-01-01: 18 +TX: + 2015-01-01: 21 +VA: + 2015-01-01: 21 +WI: + 2015-01-01: 19 diff --git a/policyengine_us/parameters/gov/ssa/ssi/eligibility/child/deeming_student_age_limit.yaml b/policyengine_us/parameters/gov/ssa/ssi/eligibility/child/deeming_student_age_limit.yaml new file mode 100644 index 00000000000..2a300f97669 --- /dev/null +++ b/policyengine_us/parameters/gov/ssa/ssi/eligibility/child/deeming_student_age_limit.yaml @@ -0,0 +1,12 @@ +description: The Social Security Administration treats a student regularly attending school as an ineligible child for deeming below this age. + +values: + 1974-01-01: 21 + 2008-06-16: 22 +metadata: + label: SSI ineligible child deeming student age limit + unit: year + period: year + reference: + - title: POMS SI 01310.115 A.3 - Ineligible child for deeming purposes + href: https://secure.ssa.gov/poms.nsf/lnx/0501310115 diff --git a/policyengine_us/parameters/gov/ssa/ssi/eligibility/child/student_age_limit.yaml b/policyengine_us/parameters/gov/ssa/ssi/eligibility/child/student_age_limit.yaml new file mode 100644 index 00000000000..607f0f7986e --- /dev/null +++ b/policyengine_us/parameters/gov/ssa/ssi/eligibility/child/student_age_limit.yaml @@ -0,0 +1,13 @@ +description: The Social Security Administration treats a student regularly attending school as a child under the Supplemental Security Income program below this age. + +values: + 1974-01-01: 22 +metadata: + label: SSI child student age limit + unit: year + period: year + reference: + - title: 42 U.S.C. § 1382c(c) - Definition of child + href: https://www.law.cornell.edu/uscode/text/42/1382c#c + - title: 20 CFR § 416.1856(a)(2) - Who is considered a child + href: https://www.ecfr.gov/current/title-20/section-416.1856#p-416.1856(a)(2) diff --git a/policyengine_us/tests/core/test_ssi_annual_income_inputs.py b/policyengine_us/tests/core/test_ssi_annual_income_inputs.py new file mode 100644 index 00000000000..f6fecee9a0a --- /dev/null +++ b/policyengine_us/tests/core/test_ssi_annual_income_inputs.py @@ -0,0 +1,142 @@ +"""SSI monthly budgets must preserve existing annual input overrides. + +Policy examples and monthly allocation caps are covered in YAML. These tests +exercise Core's period conversion, supplied-input detection and branch caches, +which require the Python API. One small Core system uses the production +variable classes and the imported country's read-only parameters; it never +constructs or copies another country model. This belongs to Rest/core. +""" + +import numpy as np +import pytest +from policyengine_core.simulations import Simulation +from policyengine_core.taxbenefitsystems import TaxBenefitSystem + +from policyengine_us.entities import MaritalUnit, Person +from policyengine_us.system import system as country_system +from policyengine_us.tools.period_branch import get_override_branch + +YEAR = 2026 +MONTH = "2026-01" +ANNUAL_CURRENCY_VARIABLES = ( + "ssi_countable_income", + "ssi_unearned_income_deemed_from_ineligible_parent", + "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_unearned_income_deemed_from_ineligible_spouse", + "ssi_income_deemed_from_ineligible_spouse", + "ssi_ineligible_child_allocation", + "ssi_in_kind_support_and_maintenance", + "ssi_pmv_amount", + "medicaid_optional_senior_or_disabled_countable_income", + "medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent", + "medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse", +) + +ANNUAL_INPUT_VARIABLES = (*ANNUAL_CURRENCY_VARIABLES, "is_ssi_spousal_deeming_applies") + + +@pytest.fixture(scope="module") +def small_system(): + result = TaxBenefitSystem(entities=[Person, MaritalUnit]) + names = ( + "is_ssi_eligible_individual", + "is_ssi_ineligible_spouse", + "ssi_earned_income", + "ssi_unearned_income", + "ssi_spousal_deeming_child_allocations", + *ANNUAL_INPUT_VARIABLES, + *(f"{name}_monthly" for name in ANNUAL_INPUT_VARIABLES), + ) + result.add_variables(*(type(country_system.variables[name]) for name in names)) + result.parameters = country_system.parameters + return result + + +@pytest.mark.parametrize("input_period", [YEAR, MONTH]) +@pytest.mark.parametrize("name", ANNUAL_INPUT_VARIABLES) +def test_annual_input_overrides_reach_monthly_budgets(small_system, name, input_period): + amount = 1_200 if name in ANNUAL_CURRENCY_VARIABLES else True + monthly = 100 if name in ANNUAL_CURRENCY_VARIABLES else True + changed = 2_400 if name in ANNUAL_CURRENCY_VARIABLES else False + changed_monthly = 200 if name in ANNUAL_CURRENCY_VARIABLES else False + simulation = Simulation( + tax_benefit_system=small_system, + situation={ + "people": { + "claimant": { + name: {input_period: amount}, + # Supplied inputs bypass the formula's eligibility gate. + "is_ssi_eligible_individual": {YEAR: False}, + } + }, + "marital_units": {"unit": {"members": ["claimant"]}}, + }, + ) + assert small_system.variables[name].definition_period == "year" + np.testing.assert_array_equal(simulation.calculate(name, YEAR), [amount]) + np.testing.assert_array_equal(simulation.calculate(name, MONTH), [monthly]) + np.testing.assert_array_equal( + simulation.calculate(f"{name}_monthly", MONTH), [monthly] + ) + + branch = get_override_branch( + simulation, "annual_override", YEAR, {name: np.array([changed])} + ) + np.testing.assert_array_equal( + branch.calculate(f"{name}_monthly", MONTH), [changed_monthly] + ) + np.testing.assert_array_equal( + simulation.calculate(f"{name}_monthly", MONTH), [monthly] + ) + + +def test_computed_annual_cache_does_not_replace_monthly_budgets(small_system): + name = "ssi_earned_income_deemed_from_ineligible_spouse" + allocations = { + f"2026-{month:02d}": 0 if month <= 6 else 200 for month in range(1, 13) + } + simulation = Simulation( + tax_benefit_system=small_system, + situation={ + "people": { + "claimant": { + "is_ssi_ineligible_spouse": {YEAR: False}, + "ssi_earned_income": {YEAR: 0}, + "ssi_unearned_income": {YEAR: 0}, + "ssi_spousal_deeming_child_allocations": allocations, + }, + "spouse": { + "is_ssi_ineligible_spouse": {YEAR: True}, + "ssi_earned_income": {YEAR: 12_000}, + "ssi_unearned_income": {YEAR: 0}, + "ssi_spousal_deeming_child_allocations": { + month: 0 for month in allocations + }, + }, + }, + "marital_units": {"couple": {"members": ["claimant", "spouse"]}}, + }, + ) + # Six months of $1,000 and six of $800 are not $900 in every month. + np.testing.assert_array_equal(simulation.calculate(name, YEAR), [10_800, 0]) + np.testing.assert_array_equal( + simulation.calculate(f"{name}_monthly", MONTH), [1_000, 0] + ) + np.testing.assert_array_equal( + simulation.calculate(f"{name}_monthly", "2026-07"), [800, 0] + ) + + if hasattr(simulation, "_get_store_history"): + # Core 3.33+ invalidates inherited calculations on direct set_input. + branch = simulation.get_branch("first_annual_override") + branch.set_input(name, YEAR, [24_000, 0]) + else: + branch = get_override_branch( + simulation, "first_annual_override", YEAR, {name: np.array([24_000, 0])} + ) + np.testing.assert_array_equal( + branch.calculate(f"{name}_monthly", MONTH), [2_000, 0] + ) + np.testing.assert_array_equal( + simulation.calculate(f"{name}_monthly", MONTH), [1_000, 0] + ) diff --git a/policyengine_us/tests/core/test_ssi_spousal_deeming_child_allocations.py b/policyengine_us/tests/core/test_ssi_spousal_deeming_child_allocations.py new file mode 100644 index 00000000000..b06c1644832 --- /dev/null +++ b/policyengine_us/tests/core/test_ssi_spousal_deeming_child_allocations.py @@ -0,0 +1,445 @@ +"""Invariants of the SSI ineligible-child allocation in spousal deeming. + +20 CFR 416.1163(b) deducts an allocation from an ineligible spouse's income +for each ineligible child: 'your natural child or adopted child, or the +natural or adopted child of your spouse ... who lives in the same household +with you, and is not eligible for SSI benefits' (416.1160(d)). Each +Hypothesis example builds a batch of random households (a married couple, +their children, step-children, grandchildren, unrelated children and other +adults, some living elsewhere, filed in assorted tax units, with parent ids +on everyone, no one or some people) and checks: + +1. Differential: every person's ssi_spousal_deeming_child_allocations + equals an oracle written here from the regulation and the parent-link + contract in household/demographic/person/_parent_links.py, without the + model. The oracle knows each person's true parents, so with ids it is + the regulation itself. Without ids it is the documented proxy: the + co-resident members of the person's or the spouse's tax unit, and in a + household with links only for an unlinked parent. +2. Spouses see the same total: a child of either spouse is a child of 'you + or your spouse' for both. +3. Order: listing each household's people in reverse gives the same totals. +4. Monotonicity: raising every child's unearned income never raises a + total (416.1163(b)(2)). +5. Deemed unearned income is the spouse's unearned income less the total, + floored at zero (416.1163(b)(3)). + +A second test checks each child's own allocation against a month-by-month +oracle of 416.1163(b)(1)-(2) and 416.1161(c): the student earned income +exclusion runs month by month until its yearly limit is used up, each +month's allocation is floored at zero, and a loss counts as no income. It also checks the bounds and that +more income never raises an allocation. + +YAML covers the worked cases. This file routes to the Rest/core group. +""" + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +# 416.1163(b)(1): the couple rate ($1,491) less the individual rate ($994). +MONTHLY_ALLOCATION = 1_491 - 994 +CHILD_AGE = 18 # 416.1856(a)(1) +STUDENT_AGE = 22 # 416.1856(a)(2) +ABSENT_PARENT_ID = 999_999 +HOUSEHOLDS_PER_EXAMPLE = 30 +# 416.1112(c)(3) student earned income exclusion in 2026 (POMS SI 00820.510). +STUDENT_MONTHLY_EXCLUSION = 2_410 +STUDENT_YEARLY_EXCLUSION = 9_730 + +CHILD_KINDS = ("claimant", "spouse", "both", "grandchild", "unrelated") +OTHER = st.fixed_dictionaries( + { + "kind": st.sampled_from(CHILD_KINDS + ("adult",)), + "age": st.integers(0, 23), + "student": st.booleans(), + "monthly_unearned": st.sampled_from((0, 150, 600)), + # About one in four lives in another household. + "away": st.sampled_from((False, False, False, True)), + "tax_unit": st.sampled_from(("couple", "spouse", "own", "adult")), + "has_ids": st.booleans(), + } +) +HOUSEHOLD = st.fixed_dictionaries( + { + "aged_claimant": st.booleans(), + "spouse_unearned": st.sampled_from((0, 300, 600, 1_200)), + "spouse_files_separately": st.booleans(), + "ids": st.sampled_from(("none", "all", "some")), + "report_own_children": st.booleans(), + "others": st.lists(OTHER, min_size=0, max_size=5), + } +) +BATCH = st.lists( + HOUSEHOLD, min_size=HOUSEHOLDS_PER_EXAMPLE, max_size=HOUSEHOLDS_PER_EXAMPLE +) + + +def _people(index, household): + """Return each person's facts, with true parents and tax unit.""" + claimant, spouse = f"h{index}_claimant", f"h{index}_spouse" + people = [ + dict( + name=claimant, + age=70 if household["aged_claimant"] else 45, + disabled=not household["aged_claimant"], + unearned=0, + student=False, + parents=(), + away=False, + tax_unit="couple", + role="head", + ), + dict( + name=spouse, + age=50, + disabled=False, + unearned=household["spouse_unearned"], + student=False, + parents=(), + away=False, + tax_unit="spouse" if household["spouse_files_separately"] else "couple", + role="head" if household["spouse_files_separately"] else "spouse", + ), + ] + # The first other adult can parent grandchildren and head a tax unit. + adult_position = next( + (p for p, o in enumerate(household["others"]) if o["kind"] == "adult"), + None, + ) + adult = None if adult_position is None else f"h{index}_o{adult_position}" + for position, other in enumerate(household["others"]): + name = f"h{index}_o{position}" + kind = other["kind"] + parents = { + "claimant": (claimant,), + "spouse": (spouse, None), + "both": (claimant, spouse), + "grandchild": (adult,) if adult else (None,), + "unrelated": (None,), + "adult": (), + }[kind] + tax_unit = other["tax_unit"] + if tax_unit == "spouse" and not household["spouse_files_separately"]: + tax_unit = "couple" + if tax_unit == "adult" and adult is None: + tax_unit = "own" + if kind == "adult": + tax_unit = "adult" if name == adult else "own" + people.append( + dict( + name=name, + age=other["age"] + (30 if kind == "adult" else 0), + disabled=False, + unearned=other["monthly_unearned"], + student=other["student"], + # None is a parent who lives outside the simulation. + parents=parents, + away=other["away"] and kind != "adult", + tax_unit=tax_unit, + role="head" if tax_unit == "own" or name == adult else "dependent", + has_ids=other["has_ids"], + ) + ) + ids_mode = household["ids"] + for person in people: + person["has_ids"] = bool(person["parents"]) and ( + ids_mode == "all" or (ids_mode == "some" and person.get("has_ids")) + ) + return people + + +def _tax_unit_key(index, person): + if person["tax_unit"] == "own": + return f"{person['name']}_tu" + return f"h{index}_{person['tax_unit']}_tu" + + +def _situation(households, reverse=False, income_rise=0): + situation = { + plural: {} + for plural in ( + "people", + "tax_units", + "households", + "marital_units", + "families", + "spm_units", + ) + } + facts = {} + person_ids = {} + for index, household in enumerate(households): + people = _people(index, household) + if reverse: + people = people[::-1] + for number, person in enumerate(people): + person_ids[person["name"]] = 1_000 * (index + 1) + number + 1 + # Each spouse reports their co-resident children when asked. + own_children = {} + for person in people: + if not person["away"]: + for parent in person["parents"]: + own_children[parent] = own_children.get(parent, 0) + 1 + for person in people: + facts[person["name"]] = dict(person, household=index) + child = person["age"] < CHILD_AGE + parent_ids = [ + ( + person_ids.get(parent, ABSENT_PARENT_ID) + if parent + else ABSENT_PARENT_ID + ) + for parent in person["parents"] + ] + parent_ids = (parent_ids + [0, 0])[:2] if person["has_ids"] else [0, 0] + reports = household["report_own_children"] and person["name"].endswith( + ("_claimant", "_spouse") + ) + situation["people"][person["name"]] = { + "age": {YEAR: person["age"]}, + "is_ssi_disabled": {YEAR: person["disabled"]}, + "ssi_unearned_income": { + YEAR: 12 * (person["unearned"] + (income_rise if child else 0)) + }, + "is_full_time_college_student": {YEAR: person["student"]}, + "is_in_k12_school": {YEAR: False}, + "person_id": {YEAR: person_ids[person["name"]]}, + "parent_1_id": {YEAR: parent_ids[0]}, + "parent_2_id": {YEAR: parent_ids[1]}, + "own_children_in_household": { + YEAR: own_children.get(person["name"], 0) if reports else 0 + }, + "is_tax_unit_head": {YEAR: person["role"] == "head"}, + "is_tax_unit_spouse": {YEAR: person["role"] == "spouse"}, + "is_tax_unit_dependent": {YEAR: person["role"] == "dependent"}, + } + units = { + "tax_units": _tax_unit_key(index, person), + "households": ( + f"{person['name']}_away" if person["away"] else f"h{index}_home" + ), + "marital_units": ( + f"h{index}_couple" + if person["name"].endswith(("_claimant", "_spouse")) + else f"{person['name']}_mu" + ), + } + units["families"] = units["households"] + units["spm_units"] = units["households"] + for plural, key in units.items(): + group = situation[plural].setdefault(key, {"members": []}) + group["members"].append(person["name"]) + if plural == "households": + group["state_code"] = {YEAR: "TX"} + return situation, facts + + +def _allocation(person): + """416.1163(b)(1)-(2) for the oracle's people, who have no earnings.""" + child = person["age"] < CHILD_AGE or ( + person["age"] < STUDENT_AGE and person["student"] + ) + if not child or person["disabled"]: + return 0 + return max(0, 12 * (MONTHLY_ALLOCATION - person["unearned"])) + + +def _oracle(facts): + """Each person's total, from true relationships and the id contract.""" + names = list(facts) + spouse_of = {} + for name in names: + if name.endswith("_claimant"): + spouse_of[name] = name.replace("_claimant", "_spouse") + elif name.endswith("_spouse"): + spouse_of[name] = name.replace("_spouse", "_claimant") + + def home(name): + person = facts[name] + return (person["household"], person["away"] and name) + + def tax_unit(name): + return _tax_unit_key(facts[name]["household"], facts[name]) + + def co_resident_named_parents(name): + if not facts[name]["has_ids"]: + return set() + return { + parent + for parent in facts[name]["parents"] + if parent and home(parent) == home(name) + } + + def linked_count(name): + return sum(name in co_resident_named_parents(other) for other in names) + + def household_linked(name): + return any( + facts[other]["has_ids"] for other in names if home(other) == home(name) + ) + + def unlinked_parent(name): + own = facts[name].get("reported_children", 0) + linked = linked_count(name) + return (own > 0 or linked > 0) and (linked == 0 or own > linked) + + totals = {} + for name in names: + selves = {name} | ({spouse_of[name]} if name in spouse_of else set()) + presume = not household_linked(name) or any( + unlinked_parent(member) for member in selves + ) + units = {tax_unit(member) for member in selves} + total = 0 + for other in names: + if other in selves or home(other) != home(name): + continue + if facts[other]["has_ids"]: + counts = bool(co_resident_named_parents(other) & selves) + else: + counts = presume and tax_unit(other) in units + total += counts * _allocation(facts[other]) + totals[name] = total + return totals + + +def _record_reported_children(situation, facts): + for name, person in facts.items(): + person["reported_children"] = situation["people"][name][ + "own_children_in_household" + ][YEAR] + + +def _calculate(situation, variables): + simulation = Simulation(situation=situation) + names = list(situation["people"]) + return { + variable: dict(zip(names, simulation.calculate(variable, YEAR))) + for variable in variables + } + + +@settings(max_examples=3, derandomize=True, deadline=None) +@given(BATCH) +def test_allocations_match_oracle_and_invariants(households): + variables = ( + "ssi_spousal_deeming_child_allocations", + "ssi_unearned_income_deemed_from_ineligible_spouse", + "is_ssi_ineligible_spouse", + ) + situation, facts = _situation(households) + _record_reported_children(situation, facts) + results = _calculate(situation, variables) + totals = results["ssi_spousal_deeming_child_allocations"] + oracle = _oracle(facts) + + # 1. Differential against the oracle. + for name in facts: + assert totals[name] == oracle[name], name + + for index in range(len(households)): + claimant, spouse = f"h{index}_claimant", f"h{index}_spouse" + # 2. Both spouses see the same children. + assert totals[claimant] == totals[spouse] + # 5. Deemed unearned income, 416.1163(b)(3). + assert results["is_ssi_ineligible_spouse"][spouse] + spouse_unearned = 12 * facts[spouse]["unearned"] + assert results["ssi_unearned_income_deemed_from_ineligible_spouse"][ + claimant + ] == max(0, spouse_unearned - totals[claimant]) + + # 3. Order of people. + reversed_situation, _ = _situation(households, reverse=True) + reversed_totals = _calculate( + reversed_situation, ("ssi_spousal_deeming_child_allocations",) + )["ssi_spousal_deeming_child_allocations"] + assert reversed_totals == totals + + # 4. Children's income never raises a total. + richer_situation, _ = _situation(households, income_rise=100) + richer_totals = _calculate( + richer_situation, ("ssi_spousal_deeming_child_allocations",) + )["ssi_spousal_deeming_child_allocations"] + assert all(richer_totals[name] <= totals[name] for name in totals) + + +CHILD = st.fixed_dictionaries( + { + "age": st.integers(0, 23), + "in_school": st.booleans(), + "in_college": st.booleans(), + "monthly_earned": st.sampled_from( + (-1_500, 0, 300, 497, 800, 1_000, 2_410, 2_600) + ), + "monthly_unearned": st.sampled_from((-250, 0, 100, 497, 600)), + "public_income_maintenance": st.sampled_from((False, False, False, True)), + } +) + + +def _child_situation(children, rise=0): + people = {} + for index, child in enumerate(children): + people[f"c{index}"] = { + "age": {YEAR: child["age"]}, + "is_ssi_disabled": {YEAR: False}, + "is_in_k12_school": {YEAR: child["in_school"]}, + "is_full_time_college_student": {YEAR: child["in_college"]}, + "ssi_earned_income": {YEAR: 12 * (child["monthly_earned"] + rise)}, + "ssi_unearned_income": {YEAR: 12 * (child["monthly_unearned"] + rise)}, + "ssi_receives_public_income_maintenance": { + YEAR: child["public_income_maintenance"] + }, + } + situation = {"people": people} + for plural in ( + "tax_units", + "households", + "marital_units", + "families", + "spm_units", + ): + situation[plural] = {f"u{name}": {"members": [name]} for name in people} + for household in situation["households"].values(): + household["state_code"] = {YEAR: "TX"} + return situation + + +def _monthly_allocation_oracle(child): + """Sum over months of 416.1163(b)(2) with the 416.1161(c) exclusion.""" + student = child["in_school"] or child["in_college"] + is_child = child["age"] < CHILD_AGE or (child["age"] < STUDENT_AGE and student) + if not is_child or child["public_income_maintenance"]: + return 0 + left = STUDENT_YEARLY_EXCLUSION if student and child["age"] < STUDENT_AGE else 0 + # A loss is no income: it neither raises the allocation nor offsets other + # income. + earned = max(0, child["monthly_earned"]) + unearned = max(0, child["monthly_unearned"]) + total = 0 + for _ in range(12): + exclusion = min(STUDENT_MONTHLY_EXCLUSION, earned, left) + left -= exclusion + income = earned - exclusion + unearned + total += max(0, MONTHLY_ALLOCATION - income) + return total + + +@settings(max_examples=3, derandomize=True, deadline=None) +@given(st.lists(CHILD, min_size=60, max_size=60)) +def test_child_allocation_matches_monthly_oracle(children): + allocation = Simulation(situation=_child_situation(children)).calculate( + "ssi_ineligible_child_allocation", YEAR + ) + expected = [_monthly_allocation_oracle(child) for child in children] + np.testing.assert_allclose(allocation, expected, atol=1e-6) + assert np.all(allocation >= 0) + assert np.all(allocation <= 12 * MONTHLY_ALLOCATION) + richer = Simulation(situation=_child_situation(children, rise=100)).calculate( + "ssi_ineligible_child_allocation", YEAR + ) + assert np.all(richer <= allocation + 1e-6) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml index fe17f4085ea..fa1ff80e88f 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml @@ -29,6 +29,8 @@ is_ssi_disabled: true sibling: age: 14 + # Isolate pretax premiums from the student earned-income exclusion. + is_in_k12_school: false employment_income: 2_400 pre_tax_health_insurance_premiums: 1_200 parent: @@ -129,3 +131,1502 @@ # the parents' living allowance (1,491 x 12 = 17,892, the couple SSI # federal benefit rate) -> 19,098, split equally -> 9,549 each. medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [9_549, 9_549, 0, 0] + +- name: Missouri does not allocate to a co-resident adult student. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + is_parent: true + ssi_unearned_income: 24_000 + claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + student: + age: 20 + person_id: 3 + parent_1_id: 1 + is_tax_unit_dependent: true + is_full_time_student: true + tax_units: + family: + members: [parent, claimant, student] + marital_units: + parent_unit: + members: [parent] + claimant_unit: + members: [claimant] + student_unit: + members: [student] + households: + home: + members: [parent, claimant, student] + state_code: MO + output: + # DSS 0805.020.15 step 1 restricts allocations to children under 18: + # https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-020-00/0805-020-15/ + # SSI still allocates for this student, but Missouri must not deduct it. + ssi_ineligible_child_allocation: [0, 0, 5_964] + # ($2,000 - $20 - $994) * 12 = $11,832, not $5,868. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 11_832, 0] + +# Current state manual definitions constrain SSI's newly supported adult-student +# allocations before the common deduction sequence. Each household has one +# parent with $2,000 monthly unearned income and a disabled citizen claimant. +# At the state's age ceiling there is no Medicaid child allocation: +# ($2,000 - $20 - $994) * 12 = $11,832. California retains its existing +# $230 general disregard, giving ($2,000 - $230 - $994) * 12 = $9,312. +# SSI still gives each unmarried full-time student a $497 monthly allocation. +- name: Review r3 state age ceilings exclude adult students from Medicaid parental allocations. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + ca_parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + ca_claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + ca_student: + age: 21 + person_id: 3 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + fl_parent: + age: 40 + person_id: 7 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + fl_claimant: + age: 10 + person_id: 8 + parent_1_id: 7 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + fl_student: + age: 21 + person_id: 9 + parent_1_id: 7 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + id_parent: + age: 40 + person_id: 10 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + id_claimant: + age: 10 + person_id: 11 + parent_1_id: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + id_student: + age: 21 + person_id: 12 + parent_1_id: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + la_parent: + age: 40 + person_id: 13 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + la_claimant: + age: 10 + person_id: 14 + parent_1_id: 13 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + la_student: + age: 21 + person_id: 15 + parent_1_id: 13 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + mi_parent: + age: 40 + person_id: 16 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + mi_claimant: + age: 10 + person_id: 17 + parent_1_id: 16 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + mi_student: + age: 18 + person_id: 18 + parent_1_id: 16 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + mo_parent: + age: 40 + person_id: 19 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + mo_claimant: + age: 10 + person_id: 20 + parent_1_id: 19 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + mo_student: + age: 18 + person_id: 21 + parent_1_id: 19 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + mt_parent: + age: 40 + person_id: 22 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + mt_claimant: + age: 10 + person_id: 23 + parent_1_id: 22 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + mt_student: + age: 21 + person_id: 24 + parent_1_id: 22 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + nc_parent: + age: 40 + person_id: 25 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + nc_claimant: + age: 10 + person_id: 26 + parent_1_id: 25 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + nc_student: + age: 21 + person_id: 27 + parent_1_id: 25 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + nj_parent: + age: 40 + person_id: 28 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + nj_claimant: + age: 10 + person_id: 29 + parent_1_id: 28 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + nj_student: + age: 18 + person_id: 30 + parent_1_id: 28 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + ny_parent: + age: 40 + person_id: 31 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + ny_claimant: + age: 10 + person_id: 32 + parent_1_id: 31 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + ny_student: + age: 18 + person_id: 33 + parent_1_id: 31 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + oh_parent: + age: 40 + person_id: 34 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + oh_claimant: + age: 10 + person_id: 35 + parent_1_id: 34 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + oh_student: + age: 18 + person_id: 36 + parent_1_id: 34 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + va_parent: + age: 40 + person_id: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + va_claimant: + age: 10 + person_id: 41 + parent_1_id: 40 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + va_student: + age: 21 + person_id: 42 + parent_1_id: 40 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + wi_parent: + age: 40 + person_id: 43 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + wi_claimant: + age: 10 + person_id: 44 + parent_1_id: 43 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + wi_student: + age: 19 + person_id: 45 + parent_1_id: 43 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + tx_parent: + age: 40 + person_id: 46 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + tx_claimant: + age: 10 + person_id: 47 + parent_1_id: 46 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + tx_student: + age: 21 + person_id: 48 + parent_1_id: 46 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + tax_units: + ca_family: + members: [ca_parent, ca_claimant, ca_student] + fl_family: + members: [fl_parent, fl_claimant, fl_student] + id_family: + members: [id_parent, id_claimant, id_student] + la_family: + members: [la_parent, la_claimant, la_student] + mi_family: + members: [mi_parent, mi_claimant, mi_student] + mo_family: + members: [mo_parent, mo_claimant, mo_student] + mt_family: + members: [mt_parent, mt_claimant, mt_student] + nc_family: + members: [nc_parent, nc_claimant, nc_student] + nj_family: + members: [nj_parent, nj_claimant, nj_student] + ny_family: + members: [ny_parent, ny_claimant, ny_student] + oh_family: + members: [oh_parent, oh_claimant, oh_student] + va_family: + members: [va_parent, va_claimant, va_student] + wi_family: + members: [wi_parent, wi_claimant, wi_student] + tx_family: + members: [tx_parent, tx_claimant, tx_student] + marital_units: + ca_parent_unit: + members: [ca_parent] + ca_claimant_unit: + members: [ca_claimant] + ca_student_unit: + members: [ca_student] + fl_parent_unit: + members: [fl_parent] + fl_claimant_unit: + members: [fl_claimant] + fl_student_unit: + members: [fl_student] + id_parent_unit: + members: [id_parent] + id_claimant_unit: + members: [id_claimant] + id_student_unit: + members: [id_student] + la_parent_unit: + members: [la_parent] + la_claimant_unit: + members: [la_claimant] + la_student_unit: + members: [la_student] + mi_parent_unit: + members: [mi_parent] + mi_claimant_unit: + members: [mi_claimant] + mi_student_unit: + members: [mi_student] + mo_parent_unit: + members: [mo_parent] + mo_claimant_unit: + members: [mo_claimant] + mo_student_unit: + members: [mo_student] + mt_parent_unit: + members: [mt_parent] + mt_claimant_unit: + members: [mt_claimant] + mt_student_unit: + members: [mt_student] + nc_parent_unit: + members: [nc_parent] + nc_claimant_unit: + members: [nc_claimant] + nc_student_unit: + members: [nc_student] + nj_parent_unit: + members: [nj_parent] + nj_claimant_unit: + members: [nj_claimant] + nj_student_unit: + members: [nj_student] + ny_parent_unit: + members: [ny_parent] + ny_claimant_unit: + members: [ny_claimant] + ny_student_unit: + members: [ny_student] + oh_parent_unit: + members: [oh_parent] + oh_claimant_unit: + members: [oh_claimant] + oh_student_unit: + members: [oh_student] + va_parent_unit: + members: [va_parent] + va_claimant_unit: + members: [va_claimant] + va_student_unit: + members: [va_student] + wi_parent_unit: + members: [wi_parent] + wi_claimant_unit: + members: [wi_claimant] + wi_student_unit: + members: [wi_student] + tx_parent_unit: + members: [tx_parent] + tx_claimant_unit: + members: [tx_claimant] + tx_student_unit: + members: [tx_student] + households: + ca_home: + members: [ca_parent, ca_claimant, ca_student] + state_code: CA + fl_home: + members: [fl_parent, fl_claimant, fl_student] + state_code: FL + id_home: + members: [id_parent, id_claimant, id_student] + state_code: ID + la_home: + members: [la_parent, la_claimant, la_student] + state_code: LA + mi_home: + members: [mi_parent, mi_claimant, mi_student] + state_code: MI + mo_home: + members: [mo_parent, mo_claimant, mo_student] + state_code: MO + mt_home: + members: [mt_parent, mt_claimant, mt_student] + state_code: MT + nc_home: + members: [nc_parent, nc_claimant, nc_student] + state_code: NC + nj_home: + members: [nj_parent, nj_claimant, nj_student] + state_code: NJ + ny_home: + members: [ny_parent, ny_claimant, ny_student] + state_code: NY + oh_home: + members: [oh_parent, oh_claimant, oh_student] + state_code: OH + va_home: + members: [va_parent, va_claimant, va_student] + state_code: VA + wi_home: + members: [wi_parent, wi_claimant, wi_student] + state_code: WI + tx_home: + members: [tx_parent, tx_claimant, tx_student] + state_code: TX + output: + people: + ca_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 9312 + ca_student: + ssi_ineligible_child_allocation: 5964 + fl_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + fl_student: + ssi_ineligible_child_allocation: 5964 + id_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + id_student: + ssi_ineligible_child_allocation: 5964 + la_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + la_student: + ssi_ineligible_child_allocation: 5964 + mi_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + mi_student: + ssi_ineligible_child_allocation: 5964 + mo_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + mo_student: + ssi_ineligible_child_allocation: 5964 + mt_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + mt_student: + ssi_ineligible_child_allocation: 5964 + nc_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + nc_student: + ssi_ineligible_child_allocation: 5964 + nj_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + nj_student: + ssi_ineligible_child_allocation: 5964 + ny_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + ny_student: + ssi_ineligible_child_allocation: 5964 + oh_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + oh_student: + ssi_ineligible_child_allocation: 5964 + va_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + va_student: + ssi_ineligible_child_allocation: 5964 + wi_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + wi_student: + ssi_ineligible_child_allocation: 5964 + tx_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + tx_student: + ssi_ineligible_child_allocation: 5964 + +# Below-ceiling controls retain $497 monthly allocations: $11,832 - $5,964 +# = $5,868, or $9,312 - $5,964 = $3,348 in California. Wisconsin includes +# full-time secondary students at 18 regardless of tax role; Texas includes students below 21. +# The Wisconsin seniors each earn $9,000, excluded under MEH 15.5.8. +# Secondary enrollment proxies expected completion before 19 (MEH 1.1.3.3). +# The disabled siblings are immigration-ineligible for both programs, so +# their Medicaid allocation must be excluded by the state's nonblind and +# nondisabled definition, independently of the Medicaid recipient mask. +- name: Review r3 state allocation definitions preserve eligible students and exclude disabled children. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mo_below_parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + mo_below_claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + mo_below_student: + age: 17 + person_id: 3 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + la_below_parent: + age: 40 + person_id: 4 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + la_below_claimant: + age: 10 + person_id: 5 + parent_1_id: 4 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + la_below_student: + age: 20 + person_id: 6 + parent_1_id: 4 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + ca_below_parent: + age: 40 + person_id: 7 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + ca_below_claimant: + age: 10 + person_id: 8 + parent_1_id: 7 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + ca_below_student: + age: 20 + person_id: 9 + parent_1_id: 7 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + wi_dependent_parent: + age: 40 + person_id: 10 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + wi_dependent_claimant: + age: 10 + person_id: 11 + parent_1_id: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + wi_dependent_student: + age: 18 + person_id: 12 + parent_1_id: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_in_secondary_school: true + ssi_earned_income: 9000 + is_household_head: false + wi_not_dependent_parent: + age: 40 + person_id: 13 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + wi_not_dependent_claimant: + age: 10 + person_id: 14 + parent_1_id: 13 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + wi_not_dependent_student: + age: 18 + person_id: 15 + parent_1_id: 13 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_full_time_student: true + is_in_secondary_school: true + ssi_earned_income: 9000 + is_household_head: false + tx_below_parent: + age: 40 + person_id: 16 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + ga_ssi_scope_parent: + age: 40 + person_id: 1001 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + tx_below_claimant: + age: 10 + person_id: 17 + parent_1_id: 16 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + ga_ssi_scope_claimant: + age: 10 + person_id: 1002 + parent_1_id: 1001 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + tx_below_student: + age: 20 + person_id: 18 + parent_1_id: 16 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + ga_ssi_scope_student: + age: 21 + person_id: 1003 + parent_1_id: 1001 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + la_disabled_parent: + age: 40 + person_id: 19 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + la_disabled_claimant: + age: 10 + person_id: 20 + parent_1_id: 19 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + la_disabled_student: + age: 17 + person_id: 21 + parent_1_id: 19 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + is_medicaid_immigration_status_eligible: false + ny_disabled_parent: + age: 40 + person_id: 22 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + ny_disabled_claimant: + age: 10 + person_id: 23 + parent_1_id: 22 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + ny_disabled_student: + age: 17 + person_id: 24 + parent_1_id: 22 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + is_medicaid_immigration_status_eligible: false + va_disabled_parent: + age: 40 + person_id: 25 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + va_disabled_claimant: + age: 10 + person_id: 26 + parent_1_id: 25 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + va_disabled_student: + age: 17 + person_id: 27 + parent_1_id: 25 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + is_medicaid_immigration_status_eligible: false + wi_disabled_parent: + age: 40 + person_id: 28 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + wi_disabled_claimant: + age: 10 + person_id: 29 + parent_1_id: 28 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + wi_disabled_student: + age: 17 + person_id: 30 + parent_1_id: 28 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + is_medicaid_immigration_status_eligible: false + fl_disabled_parent: + age: 40 + person_id: 31 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + fl_disabled_claimant: + age: 10 + person_id: 32 + parent_1_id: 31 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + # This variable is supplied for siblings below; supply claimants too. + is_medicaid_immigration_status_eligible: true + fl_disabled_student: + age: 17 + person_id: 33 + parent_1_id: 31 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + is_medicaid_immigration_status_eligible: false + tax_units: + mo_below_family: + members: [mo_below_parent, mo_below_claimant, mo_below_student] + la_below_family: + members: [la_below_parent, la_below_claimant, la_below_student] + ca_below_family: + members: [ca_below_parent, ca_below_claimant, ca_below_student] + wi_dependent_family: + members: [wi_dependent_parent, wi_dependent_claimant, wi_dependent_student] + wi_not_dependent_family: + members: [wi_not_dependent_parent, wi_not_dependent_claimant] + wi_not_dependent_student_unit: + members: [wi_not_dependent_student] + tx_below_family: + members: [tx_below_parent, tx_below_claimant, tx_below_student] + ga_ssi_scope_family: + members: [ga_ssi_scope_parent, ga_ssi_scope_claimant, ga_ssi_scope_student] + la_disabled_family: + members: [la_disabled_parent, la_disabled_claimant, la_disabled_student] + ny_disabled_family: + members: [ny_disabled_parent, ny_disabled_claimant, ny_disabled_student] + va_disabled_family: + members: [va_disabled_parent, va_disabled_claimant, va_disabled_student] + wi_disabled_family: + members: [wi_disabled_parent, wi_disabled_claimant, wi_disabled_student] + fl_disabled_family: + members: [fl_disabled_parent, fl_disabled_claimant, fl_disabled_student] + marital_units: + mo_below_parent_unit: + members: [mo_below_parent] + mo_below_claimant_unit: + members: [mo_below_claimant] + mo_below_student_unit: + members: [mo_below_student] + la_below_parent_unit: + members: [la_below_parent] + la_below_claimant_unit: + members: [la_below_claimant] + la_below_student_unit: + members: [la_below_student] + ca_below_parent_unit: + members: [ca_below_parent] + ca_below_claimant_unit: + members: [ca_below_claimant] + ca_below_student_unit: + members: [ca_below_student] + wi_dependent_parent_unit: + members: [wi_dependent_parent] + wi_dependent_claimant_unit: + members: [wi_dependent_claimant] + wi_dependent_student_unit: + members: [wi_dependent_student] + wi_not_dependent_parent_unit: + members: [wi_not_dependent_parent] + wi_not_dependent_claimant_unit: + members: [wi_not_dependent_claimant] + wi_not_dependent_student_unit: + members: [wi_not_dependent_student] + tx_below_parent_unit: + members: [tx_below_parent] + ga_ssi_scope_parent_unit: + members: [ga_ssi_scope_parent] + tx_below_claimant_unit: + members: [tx_below_claimant] + ga_ssi_scope_claimant_unit: + members: [ga_ssi_scope_claimant] + tx_below_student_unit: + members: [tx_below_student] + ga_ssi_scope_student_unit: + members: [ga_ssi_scope_student] + la_disabled_parent_unit: + members: [la_disabled_parent] + la_disabled_claimant_unit: + members: [la_disabled_claimant] + la_disabled_student_unit: + members: [la_disabled_student] + ny_disabled_parent_unit: + members: [ny_disabled_parent] + ny_disabled_claimant_unit: + members: [ny_disabled_claimant] + ny_disabled_student_unit: + members: [ny_disabled_student] + va_disabled_parent_unit: + members: [va_disabled_parent] + va_disabled_claimant_unit: + members: [va_disabled_claimant] + va_disabled_student_unit: + members: [va_disabled_student] + wi_disabled_parent_unit: + members: [wi_disabled_parent] + wi_disabled_claimant_unit: + members: [wi_disabled_claimant] + wi_disabled_student_unit: + members: [wi_disabled_student] + fl_disabled_parent_unit: + members: [fl_disabled_parent] + fl_disabled_claimant_unit: + members: [fl_disabled_claimant] + fl_disabled_student_unit: + members: [fl_disabled_student] + households: + mo_below_home: + members: [mo_below_parent, mo_below_claimant, mo_below_student] + state_code: MO + la_below_home: + members: [la_below_parent, la_below_claimant, la_below_student] + state_code: LA + ca_below_home: + members: [ca_below_parent, ca_below_claimant, ca_below_student] + state_code: CA + wi_dependent_home: + members: [wi_dependent_parent, wi_dependent_claimant, wi_dependent_student] + state_code: WI + wi_not_dependent_home: + members: [wi_not_dependent_parent, wi_not_dependent_claimant, wi_not_dependent_student] + state_code: WI + tx_below_home: + members: [tx_below_parent, tx_below_claimant, tx_below_student] + state_code: TX + ga_ssi_scope_home: + members: [ga_ssi_scope_parent, ga_ssi_scope_claimant, ga_ssi_scope_student] + state_code: GA + la_disabled_home: + members: [la_disabled_parent, la_disabled_claimant, la_disabled_student] + state_code: LA + ny_disabled_home: + members: [ny_disabled_parent, ny_disabled_claimant, ny_disabled_student] + state_code: NY + va_disabled_home: + members: [va_disabled_parent, va_disabled_claimant, va_disabled_student] + state_code: VA + wi_disabled_home: + members: [wi_disabled_parent, wi_disabled_claimant, wi_disabled_student] + state_code: WI + fl_disabled_home: + members: [fl_disabled_parent, fl_disabled_claimant, fl_disabled_student] + state_code: FL + output: + people: + mo_below_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + mo_below_student: + ssi_ineligible_child_allocation: 5964 + la_below_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + la_below_student: + ssi_ineligible_child_allocation: 5964 + ca_below_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 3348 + ca_below_student: + ssi_ineligible_child_allocation: 5964 + wi_dependent_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + wi_dependent_student: + ssi_ineligible_child_allocation: 5964 + wi_not_dependent_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + wi_not_dependent_student: + ssi_ineligible_child_allocation: 5964 + tx_below_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + ga_ssi_scope_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + tx_below_student: + ssi_ineligible_child_allocation: 5964 + ga_ssi_scope_student: + ssi_ineligible_child_allocation: 5964 + la_disabled_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + la_disabled_student: + ssi_ineligible_child_allocation: 5964 + ny_disabled_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + ny_disabled_student: + ssi_ineligible_child_allocation: 5964 + va_disabled_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + va_disabled_student: + ssi_ineligible_child_allocation: 5964 + wi_disabled_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + wi_disabled_student: + ssi_ineligible_child_allocation: 5964 + fl_disabled_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + fl_disabled_student: + ssi_ineligible_child_allocation: 5964 + +# MEH 1.1.3.3 and 15.1.2: at 18 the education condition, not tax +# dependency, governs allocations. College alone does not qualify. Minors +# need no school attendance. Secondary includes equivalent-level training. +- name: Wisconsin allocation children require secondary attendance at 18 regardless of tax role. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + college_dependent_parent: + age: 40 + person_id: 100 + is_parent: true + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ssi_unearned_income: 24000 + college_dependent_claimant: + age: 10 + person_id: 101 + parent_1_id: 100 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + is_medicaid_immigration_status_eligible: true + college_dependent_student: + age: 18 + person_id: 102 + parent_1_id: 100 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_household_head: false + is_full_time_student: true + is_in_secondary_school: false + is_full_time_college_student: true + ssi_earned_income: 9000 + college_independent_parent: + age: 40 + person_id: 103 + is_parent: true + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ssi_unearned_income: 24000 + college_independent_claimant: + age: 10 + person_id: 104 + parent_1_id: 103 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + is_medicaid_immigration_status_eligible: true + college_independent_student: + age: 18 + person_id: 105 + parent_1_id: 103 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_household_head: false + is_full_time_student: true + is_in_secondary_school: false + is_full_time_college_student: true + ssi_earned_income: 9000 + part_time_secondary_parent: + age: 40 + person_id: 106 + is_parent: true + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ssi_unearned_income: 24000 + part_time_secondary_claimant: + age: 10 + person_id: 107 + parent_1_id: 106 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + is_medicaid_immigration_status_eligible: true + part_time_secondary_student: + age: 18 + person_id: 108 + parent_1_id: 106 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_household_head: false + is_full_time_student: false + is_in_secondary_school: true + is_full_time_college_student: false + ssi_earned_income: 0 + minor_nonstudent_parent: + age: 40 + person_id: 109 + is_parent: true + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + ssi_unearned_income: 24000 + minor_nonstudent_claimant: + age: 10 + person_id: 110 + parent_1_id: 109 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + is_medicaid_immigration_status_eligible: true + minor_nonstudent_student: + age: 17 + person_id: 111 + parent_1_id: 109 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_household_head: false + is_full_time_student: false + is_in_secondary_school: false + is_full_time_college_student: false + ssi_earned_income: 0 + tax_units: + college_dependent_family: + members: [college_dependent_parent, college_dependent_claimant, college_dependent_student] + college_independent_family: + members: [college_independent_parent, college_independent_claimant] + college_independent_student_unit: + members: [college_independent_student] + part_time_secondary_family: + members: [part_time_secondary_parent, part_time_secondary_claimant, part_time_secondary_student] + minor_nonstudent_family: + members: [minor_nonstudent_parent, minor_nonstudent_claimant, minor_nonstudent_student] + marital_units: + college_dependent_parent_marital_unit: + members: [college_dependent_parent] + college_dependent_claimant_marital_unit: + members: [college_dependent_claimant] + college_dependent_student_marital_unit: + members: [college_dependent_student] + college_independent_parent_marital_unit: + members: [college_independent_parent] + college_independent_claimant_marital_unit: + members: [college_independent_claimant] + college_independent_student_marital_unit: + members: [college_independent_student] + part_time_secondary_parent_marital_unit: + members: [part_time_secondary_parent] + part_time_secondary_claimant_marital_unit: + members: [part_time_secondary_claimant] + part_time_secondary_student_marital_unit: + members: [part_time_secondary_student] + minor_nonstudent_parent_marital_unit: + members: [minor_nonstudent_parent] + minor_nonstudent_claimant_marital_unit: + members: [minor_nonstudent_claimant] + minor_nonstudent_student_marital_unit: + members: [minor_nonstudent_student] + households: + college_dependent_home: + members: [college_dependent_parent, college_dependent_claimant, college_dependent_student] + state_code: WI + college_independent_home: + members: [college_independent_parent, college_independent_claimant, college_independent_student] + state_code: WI + part_time_secondary_home: + members: [part_time_secondary_parent, part_time_secondary_claimant, part_time_secondary_student] + state_code: WI + minor_nonstudent_home: + members: [minor_nonstudent_parent, minor_nonstudent_claimant, minor_nonstudent_student] + state_code: WI + output: + people: + college_dependent_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + college_dependent_student: + ssi_ineligible_child_allocation: 5964 + college_independent_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + college_independent_student: + ssi_ineligible_child_allocation: 5964 + part_time_secondary_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11832 + part_time_secondary_student: + ssi_ineligible_child_allocation: 0 + minor_nonstudent_claimant: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5868 + minor_nonstudent_student: + ssi_ineligible_child_allocation: 5964 + +# Original review r4 witnesses: K-12 evidence without is_in_secondary_school. +- name: WI legal dependent 18-year-old high-school senior, tax dependent True + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + student: + age: 18 + person_id: 3 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + is_household_head: false + is_in_k12_school: true + is_full_time_college_student: false + ssi_earned_income: 9000 + tax_units: + family: + members: [parent, claimant, student] + marital_units: + parent_unit: + members: [parent] + claimant_unit: + members: [claimant] + student_unit: + members: [student] + households: + home: + members: [parent, claimant, student] + state_code: WI + output: + ssi_ineligible_child_allocation: [0, 0, 5964] + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 5868, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 5868, 0] + +- name: WI legal dependent 18-year-old high-school senior, tax dependent False + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_parent: true + ssi_unearned_income: 24000 + claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + student: + age: 18 + person_id: 3 + parent_1_id: 1 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_full_time_student: true + is_household_head: false + is_in_k12_school: true + is_full_time_college_student: false + ssi_earned_income: 9000 + tax_units: + family: + members: [parent, claimant] + student_family: + members: [student] + marital_units: + parent_unit: + members: [parent] + claimant_unit: + members: [claimant] + student_unit: + members: [student] + households: + home: + members: [parent, claimant, student] + state_code: WI + output: + ssi_ineligible_child_allocation: [0, 0, 5964] + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 5868, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 5868, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml index 1724495a64d..f68aa900459 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml @@ -100,6 +100,8 @@ pre_tax_health_insurance_premiums: 2_400 child: age: 14 + # Isolate pretax premiums from the student earned-income exclusion. + is_in_k12_school: false employment_income: 2_400 pre_tax_health_insurance_premiums: 1_200 tax_units: @@ -120,3 +122,101 @@ # (17,004 - 20 * 12 - 65 * 12) / 2 = 7,992 deemed and countable. medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: [7_992, 0, 0] medicaid_optional_senior_or_disabled_countable_income: [7_992, 0, 0] + +- name: Optional Medicaid budgets monthly allocations for a student earning $1_000 per month + # 20 CFR 416.1163(d): deem only in months above the FBR differential. + # The spouse has $580 countable income in each month with no allocation. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + is_medicare_eligible: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + is_medicare_eligible: false + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 1_000 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + claimant: + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: 1_160 + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly: + "2026-01": 0 + "2026-10": 0 + "2026-11": 580 + medicaid_optional_senior_or_disabled_countable_income: 1_160 + +- name: Optional Medicaid budgets monthly allocations for a student earning $2_600 per month + # 20 CFR 416.1163(d): deem only in months above the FBR differential. + # The spouse has $580 countable income in each month with no allocation. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + is_medicare_eligible: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + is_medicare_eligible: false + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 2_600 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + claimant: + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: 4_640 + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly: + "2026-01": 0 + "2026-04": 0 + "2026-05": 580 + medicaid_optional_senior_or_disabled_countable_income: 4_640 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml index 1c823197a3b..4e0130bd7db 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml @@ -113,3 +113,99 @@ # (12,000 - 2,400 + 24,000 - 3,600) / 12 = 2,500 monthly. # Apply the couple's exclusions once: (2,500 - 20 - 65) / 2 = 1,207.50. msp_countable_income: [1_207.50, 1_207.50] + +- name: MSP budgets monthly allocations for a student earning $1_000 per month + # 20 CFR 416.1163(d): deem only in months above the FBR differential. + # The spouse has $580 countable income in each month with no allocation. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + is_medicare_eligible: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + is_medicare_eligible: false + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 1_000 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + claimant: + msp_countable_income: + "2026": 1_160 + "2026-01": 0 + "2026-10": 0 + "2026-11": 580 + +- name: MSP budgets monthly allocations for a student earning $2_600 per month + # 20 CFR 416.1163(d): deem only in months above the FBR differential. + # The spouse has $580 countable income in each month with no allocation. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + is_medicare_eligible: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + is_medicare_eligible: false + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 2_600 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + claimant: + msp_countable_income: + "2026": 4_640 + "2026-01": 0 + "2026-04": 0 + "2026-05": 580 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml index 96f3e7376a9..351201e2e96 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml @@ -85,6 +85,8 @@ is_ssi_disabled: true sibling: age: 14 + # Isolate pretax premiums from the student earned-income exclusion. + is_in_k12_school: false employment_income: 2_400 pre_tax_health_insurance_premiums: 1_200 parent: diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml index 64125417f99..20f1e3a5253 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml @@ -73,6 +73,8 @@ pre_tax_health_insurance_premiums: 2_400 child: age: 14 + # Isolate pretax premiums from the student earned-income exclusion. + is_in_k12_school: false employment_income: 2_400 pre_tax_health_insurance_premiums: 1_200 tax_units: @@ -93,3 +95,51 @@ is_ssi_spousal_deeming_applies: [true, false, false] ssi_income_deemed_from_ineligible_spouse: [7_992, 0, 0] ssi_countable_income: [7_992, 0, 0] + +- name: Monthly child allocations floor each spouse income pool before annual aggregation + # With $1,000 monthly student wages, the allocations are $497 in + # January-September, $227 in October, then zero. The spouse has $100 + # unearned and $300 earned a month: both pools are exhausted in the + # first nine months; October leaves $173 earned; November-December + # leave $100 unearned and $300 earned each. An annual floor would + # incorrectly leave only $100 earned and no unearned income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + employment_income: 300 * 12 + ssi_unearned_income: 100 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + employment_income: 1_000 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + claimant: + ssi_earned_income_deemed_from_ineligible_spouse: 773 + ssi_unearned_income_deemed_from_ineligible_spouse: 200 + ssi: 11_928 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml index 0b05128cf11..543418815f8 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.yaml @@ -224,3 +224,101 @@ members: [person1, person2] output: ssi_income_deemed_from_ineligible_spouse: [0, 0] + +- name: Review r2 applies monthly spousal thresholds after a student's annual exclusion expires + # 20 CFR 416.1163(d), (e)(2): budget each month's allocations and cap + # before aggregation. Ten months at $994 and two at $911 = $11,762. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + employment_income: 1_000 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + student: + ssi_ineligible_child_allocation: 4_700 + claimant: + ssi_unearned_income_deemed_from_ineligible_spouse: 2_500 + ssi_income_deemed_from_ineligible_spouse: 1_160 + ssi: + "2026": 11_762 + "2026-01": 994 + "2026-10": 994 + "2026-11": 911 + +- name: Review r2 applies monthly spousal thresholds for wages above the student monthly exclusion + # Four months at $994 and eight at $911 = $11,264. The $307 child + # allocation in each of the first four months keeps deeming below $497. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + employment_income: 2_600 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + student: + ssi_ineligible_child_allocation: 1_228 + claimant: + ssi_unearned_income_deemed_from_ineligible_spouse: 5_972 + ssi_income_deemed_from_ineligible_spouse: 4_640 + ssi: + "2026": 11_264 + "2026-01": 994 + "2026-04": 994 + "2026-05": 911 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_spousal_deeming_child_allocations.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_spousal_deeming_child_allocations.yaml new file mode 100644 index 00000000000..000c880d26f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_spousal_deeming_child_allocations.yaml @@ -0,0 +1,637 @@ +# 20 CFR 416.1163(b): before deeming an ineligible spouse's income, SSI +# deducts an allocation for each ineligible child: 'your natural child or +# adopted child, or the natural or adopted child of your spouse ... who lives +# in the same household with you, and is not eligible for SSI benefits' +# (416.1160(d)). The allocation is the couple rate less the individual rate +# (416.1163(b)(1)): $168 a month in 1986 ($504 - $336) and $497 a month in +# 2026 ($1,491 - $994). Allocations come out of unearned income first, then +# earned income (416.1163(b)(3)). Deeming applies when the spouse's remaining +# income is more than that difference (416.1163(d)). + +- name: 416.1163(g) Example 1, the Todds, with parent ids + period: 1986 + absolute_error_margin: 0 + input: + people: + mr_todd: + age: 70 + person_id: 1 + is_tax_unit_head: true + mrs_todd: + age: 60 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 252 * 12 + mike: + age: 10 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [mr_todd, mrs_todd, mike] + marital_units: + todds: + members: [mr_todd, mrs_todd] + mike_alone: + members: [mike] + households: + household: + members: [mr_todd, mrs_todd, mike] + state_code: TX + output: + is_ssi_ineligible_child: [false, false, true] + ssi_ineligible_child_allocation: [0, 0, 2_016] # $168 * 12 + # Mike is the child of both spouses, so each sees the same allocation. + ssi_spousal_deeming_child_allocations: [2_016, 2_016, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_008, 0, 0] # ($252 - $168) * 12 + # '$84 remaining income is not more than $168': nothing is deemed. + is_ssi_spousal_deeming_applies: [false, false, false] + ssi_income_deemed_from_ineligible_spouse: [0, 0, 0] + +- name: 416.1163(g) Example 2, the Joneses, with parent ids + period: 1986 + absolute_error_margin: 0 + input: + people: + mr_jones: + age: 50 + is_ssi_disabled: true + person_id: 1 + is_tax_unit_head: true + mrs_jones: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + ssi_earned_income: 401 * 12 + ssi_unearned_income: 252 * 12 + christine: + age: 8 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [mr_jones, mrs_jones, christine] + marital_units: + joneses: + members: [mr_jones, mrs_jones] + christine_alone: + members: [christine] + households: + household: + members: [mr_jones, mrs_jones, christine] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [2_016, 2_016, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_008, 0, 0] # $84 * 12 + ssi_earned_income_deemed_from_ineligible_spouse: [4_812, 0, 0] # $401 * 12 + # '$84 unearned plus $401 earned is more than $168'. + is_ssi_spousal_deeming_applies: [true, false, false] + # $64 unearned after the $20 exclusion plus $168 earned after $65 and + # half: $232 a month. + ssi_income_deemed_from_ineligible_spouse: [2_784, 0, 0] + ssi: [3_264, 0, 0] # ($504 - $232) * 12 + +- name: Example 1 when Mike receives public income-maintenance payments + # 416.1163(b): 'We do not allocate for ineligible children who are + # receiving public income-maintenance payments.' All of Mrs. Todd's $252 + # remains, which is more than $168, so it is deemed. + period: 1986 + absolute_error_margin: 0 + input: + people: + mr_todd: + age: 70 + person_id: 1 + is_tax_unit_head: true + mrs_todd: + age: 60 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 252 * 12 + mike: + age: 10 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + ssi_receives_public_income_maintenance: true + tax_units: + tax_unit: + members: [mr_todd, mrs_todd, mike] + marital_units: + todds: + members: [mr_todd, mrs_todd] + mike_alone: + members: [mike] + households: + household: + members: [mr_todd, mrs_todd, mike] + state_code: TX + output: + is_ssi_ineligible_child: [false, false, true] + ssi_ineligible_child_allocation: [0, 0, 0] + ssi_spousal_deeming_child_allocations: [0, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [3_024, 0, 0] # $252 * 12 + is_ssi_spousal_deeming_applies: [true, false, false] + ssi_income_deemed_from_ineligible_spouse: [2_784, 0, 0] # ($252 - $20) * 12 + ssi: [3_264, 0, 0] # ($504 - $232) * 12 + +- name: A grandchild in the claimant's tax unit is not the claimant's child + # The grandchild's id names their father, the claimants' son, who files + # alone. The grandparents claim the grandchild, but the grandchild is + # neither spouse's child, so no allocation is made. + period: 2026 + absolute_error_margin: 0 + input: + people: + grandmother: + age: 70 + person_id: 1 + is_tax_unit_head: true + grandfather: + age: 62 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + father: + age: 35 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_head: true + grandchild: + age: 8 + person_id: 4 + parent_1_id: 3 + is_tax_unit_dependent: true + tax_units: + grandparents: + members: [grandmother, grandfather, grandchild] + father_alone: + members: [father] + marital_units: + grandparents: + members: [grandmother, grandfather] + father_alone: + members: [father] + grandchild_alone: + members: [grandchild] + households: + household: + members: [grandmother, grandfather, father, grandchild] + state_code: TX + output: + is_ssi_ineligible_child: [false, false, false, true] + # The father's own total holds his child's allocation; he has no spouse, + # so nothing is deemed with it. + ssi_spousal_deeming_child_allocations: [0, 0, 5_964, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [7_200, 0, 0, 0] + # $600 is more than $497. + is_ssi_spousal_deeming_applies: [true, false, false, false] + ssi_income_deemed_from_ineligible_spouse: [6_960, 0, 0, 0] # ($600 - $20) * 12 + ssi: [10_932, 0, 0, 0] # ($1,491 - $580) * 12 + +- name: Without parent ids the tax-unit proxy still allocates for a grandchild + # The same household with no parent ids. The parent-link contract keeps + # the tax-unit grouping when no member has ids, and a grandchild there + # cannot be told apart from a child. Supplying ids, as above, avoids this. + period: 2026 + absolute_error_margin: 0 + input: + people: + grandmother: + age: 70 + is_tax_unit_head: true + grandfather: + age: 62 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + father: + age: 35 + is_tax_unit_head: true + grandchild: + age: 8 + is_tax_unit_dependent: true + tax_units: + grandparents: + members: [grandmother, grandfather, grandchild] + father_alone: + members: [father] + marital_units: + grandparents: + members: [grandmother, grandfather] + father_alone: + members: [father] + grandchild_alone: + members: [grandchild] + households: + household: + members: [grandmother, grandfather, father, grandchild] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [5_964, 5_964, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_236, 0, 0, 0] # ($600 - $497) * 12 + is_ssi_spousal_deeming_applies: [false, false, false, false] + ssi: [11_928, 0, 0, 0] # $994 * 12 + +- name: The spouse's child from another relationship, claimed by their other parent + # The stepson lives with his father and his father's wife, the claimant. + # His mother lives elsewhere and claims him. He is 'the natural or adopted + # child of your spouse ... who lives in the same household with you'. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_ssi_disabled: true + person_id: 1 + is_tax_unit_head: true + husband: + age: 42 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + stepson: + age: 12 + person_id: 3 + parent_1_id: 2 + parent_2_id: 4 + is_tax_unit_dependent: true + mother: + age: 38 + person_id: 4 + is_tax_unit_head: true + tax_units: + couple: + members: [claimant, husband] + mother: + members: [mother, stepson] + marital_units: + couple: + members: [claimant, husband] + stepson_alone: + members: [stepson] + mother_alone: + members: [mother] + households: + home: + members: [claimant, husband, stepson] + state_code: TX + elsewhere: + members: [mother] + state_code: TX + output: + is_ssi_ineligible_child: [false, false, true, false] + # The mother does not live with her son, so her total is zero. + ssi_spousal_deeming_child_allocations: [5_964, 5_964, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_236, 0, 0, 0] # ($600 - $497) * 12 + # $103 is not more than $497. + is_ssi_spousal_deeming_applies: [false, false, false, false] + ssi_income_deemed_from_ineligible_spouse: [0, 0, 0, 0] + ssi: [11_928, 0, 0, 0] # $994 * 12 + +- name: The claimants' own child, claimed in another tax unit, still counts + # Their daughter lives with them but her grandmother claims her. Tax + # filing does not change whose child she is or where she lives. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 45 + is_ssi_disabled: true + person_id: 1 + parent_1_id: 4 + is_tax_unit_head: true + husband: + age: 47 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + daughter: + age: 15 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + grandmother: + age: 64 + person_id: 4 + is_tax_unit_head: true + tax_units: + couple: + members: [claimant, husband] + grandmother: + members: [grandmother, daughter] + marital_units: + couple: + members: [claimant, husband] + daughter_alone: + members: [daughter] + grandmother_alone: + members: [grandmother] + households: + household: + members: [claimant, husband, daughter, grandmother] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [5_964, 5_964, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_236, 0, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false, false] + ssi_income_deemed_from_ineligible_spouse: [0, 0, 0, 0] + ssi: [11_928, 0, 0, 0] + +- name: A 19-year-old student who files her own return counts, and her wages do not reduce it + # 416.1856: a student under 22 regularly attending school is a child. + # 416.1161(c) excludes a student child's earned income up to the + # 416.1112(c)(3) amounts ($2,410 a month in 2026), so her $300 a month of + # wages leaves the full allocation. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_ssi_disabled: true + person_id: 1 + is_tax_unit_head: true + husband: + age: 52 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + daughter: + age: 19 + is_full_time_college_student: true + employment_income: 300 * 12 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_head: true + tax_units: + couple: + members: [claimant, husband] + daughter: + members: [daughter] + marital_units: + couple: + members: [claimant, husband] + daughter_alone: + members: [daughter] + households: + household: + members: [claimant, husband, daughter] + state_code: TX + output: + is_ssi_ineligible_child: [false, false, true] + ssi_ineligible_child_allocation: [0, 0, 5_964] + ssi_spousal_deeming_child_allocations: [5_964, 5_964, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_236, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false] + ssi: [11_928, 0, 0] + +- name: A child in the tax unit who lives in another household does not count + # No member has parent ids, so the tax-unit proxy applies, but only to + # children who live with the claimant: 'who lives in the same household + # with you'. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 70 + is_tax_unit_head: true + wife: + age: 60 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + son: + age: 16 + is_tax_unit_dependent: true + aunt: + age: 40 + is_tax_unit_head: true + tax_units: + family: + members: [claimant, wife, son] + aunt: + members: [aunt] + marital_units: + couple: + members: [claimant, wife] + son_alone: + members: [son] + aunt_alone: + members: [aunt] + households: + home: + members: [claimant, wife] + state_code: TX + away: + members: [son, aunt] + state_code: TX + output: + is_ssi_ineligible_child: [false, false, true, false] + ssi_spousal_deeming_child_allocations: [0, 0, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [7_200, 0, 0, 0] + is_ssi_spousal_deeming_applies: [true, false, false, false] + ssi_income_deemed_from_ineligible_spouse: [6_960, 0, 0, 0] # ($600 - $20) * 12 + ssi: [10_932, 0, 0, 0] # ($1,491 - $580) * 12 + +- name: With parent links, a child without ids is presumed only for an unlinked parent + # The first child's ids name both spouses. The second has none. Neither + # spouse has a child that no id names (own_children_in_household is not + # supplied), so the parent-link contract does not presume the second + # child is theirs. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 70 + person_id: 1 + is_tax_unit_head: true + husband: + age: 62 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + linked_child: + age: 10 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + child_without_ids: + age: 8 + person_id: 4 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [claimant, husband, linked_child, child_without_ids] + marital_units: + couple: + members: [claimant, husband] + linked_child_alone: + members: [linked_child] + child_without_ids_alone: + members: [child_without_ids] + households: + household: + members: [claimant, husband, linked_child, child_without_ids] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [5_964, 5_964, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [1_236, 0, 0, 0] + +- name: With parent links, a child without ids counts for an unlinked parent + # The claimant reports two children in the household but ids name only + # one, so the claimant is an unlinked parent and the second child, in the + # claimant's tax unit, is presumed to be theirs. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 70 + person_id: 1 + own_children_in_household: 2 + is_tax_unit_head: true + husband: + age: 62 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + linked_child: + age: 10 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + child_without_ids: + age: 8 + person_id: 4 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [claimant, husband, linked_child, child_without_ids] + marital_units: + couple: + members: [claimant, husband] + linked_child_alone: + members: [linked_child] + child_without_ids_alone: + members: [child_without_ids] + households: + household: + members: [claimant, husband, linked_child, child_without_ids] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [11_928, 11_928, 0, 0] + # The $11,928 of allocations exceed the husband's $7,200. + ssi_unearned_income_deemed_from_ineligible_spouse: [0, 0, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false, false] + +- name: Allocations beyond unearned income come out of earned income + # 416.1163(b)(3). Two children: $994 a month of allocations against $200 + # of unearned income leaves $794 to take from $1,500 of earnings: $706. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_ssi_disabled: true + person_id: 1 + is_tax_unit_head: true + husband: + age: 41 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 200 * 12 + ssi_earned_income: 1_500 * 12 + first_child: + age: 9 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + second_child: + age: 6 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [claimant, husband, first_child, second_child] + marital_units: + couple: + members: [claimant, husband] + first_child_alone: + members: [first_child] + second_child_alone: + members: [second_child] + households: + household: + members: [claimant, husband, first_child, second_child] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [11_928, 11_928, 0, 0] + ssi_unearned_income_deemed_from_ineligible_spouse: [0, 0, 0, 0] + ssi_earned_income_deemed_from_ineligible_spouse: [8_472, 0, 0, 0] # $706 * 12 + # $706 is more than $497. + is_ssi_spousal_deeming_applies: [true, false, false, false] + # $706 less the $20 general and $65 earned exclusions, halved: $310.50. + ssi_income_deemed_from_ineligible_spouse: [3_726, 0, 0, 0] + +- name: POMS SI 01320.400 D.2, Jack, August 2024 + # $472 allocation ($1,415 - $943). The spouse's $662 of unearned income + # less $472 leaves $190; with $715 of earnings that is more than $472. + # Countable: $190 - $20 = $170 unearned, ($715 - $65) / 2 = $325 earned, + # $495 in all. The same income is used in every month of the year. + period: 2024 + absolute_error_margin: 0 + input: + people: + jack: + age: 50 + is_ssi_disabled: true + person_id: 1 + is_tax_unit_head: true + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + ssi_earned_income: 715 * 12 + ssi_unearned_income: 662 * 12 + child: + age: 9 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [jack, spouse, child] + marital_units: + couple: + members: [jack, spouse] + child_alone: + members: [child] + households: + household: + members: [jack, spouse, child] + state_code: TX + output: + ssi_spousal_deeming_child_allocations: [5_664, 5_664, 0] # $472 * 12 + ssi_unearned_income_deemed_from_ineligible_spouse: [2_280, 0, 0] # $190 * 12 + ssi_earned_income_deemed_from_ineligible_spouse: [8_580, 0, 0] # $715 * 12 + is_ssi_spousal_deeming_applies: [true, false, false] + ssi_income_deemed_from_ineligible_spouse: [5_940, 0, 0] # $495 * 12 + ssi: [11_040, 0, 0] # ($1,415 - $495) * 12 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml index 76164837620..a6df6a9e2dd 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml @@ -1,3 +1,4 @@ + - name: Child with no income, full allocation period: 2022 input: @@ -5,11 +6,97 @@ output: ssi_ineligible_child_allocation: 5_040 # (1,261 - 841) * 12 +- name: No allocation for a child receiving public income-maintenance payments + # 416.1163(b): 'We do not allocate for ineligible children who are + # receiving public income-maintenance payments (see § 416.1142(a)).' + period: 2026 + input: + age: 14 + ssi_receives_public_income_maintenance: true + output: + ssi_ineligible_child_allocation: 0 + +- name: The child's unearned income reduces the allocation + # 416.1163(b)(2). ($1,491 - $994 - $200) * 12. + period: 2026 + input: + age: 10 + ssi_unearned_income: 200 * 12 + output: + ssi_ineligible_child_allocation: 3_564 + +- name: A student child's wages up to the student exclusion do not reduce it + # 416.1161(c) excludes a student child's earned income up to the + # 416.1112(c)(3) amounts: $2,410 a month and $9,730 a year in 2026. + period: 2026 + input: + age: 16 + employment_income: 500 * 12 + output: + ssi_ineligible_child_allocation: 5_964 # $497 * 12 + +- name: Once the student exclusion's yearly limit is used up, wages reduce it + # $1,000 a month is excluded in full for nine months ($9,000), leaving + # $730 of the $9,730 yearly limit. In month 10, $270 counts: $497 - $270 = + # $227. In months 11 and 12, $1,000 counts and the allocation is zero; + # the excess does not reduce other months (416.1163(b)(2), monthly). + period: 2026 + input: + age: 16 + employment_income: 1_000 * 12 + output: + ssi_ineligible_child_allocation: 4_700 # 9 * 497 + 227 + +- name: Student wages above the monthly exclusion reduce each month separately + # $2,600 a month: $2,410 is excluded in months 1-4 ($9,640), leaving $190 + # counted and a $307 allocation each month. In month 5 only the last $90 + # of the yearly limit is excluded, so $2,510 counts and the allocation is + # zero, as in months 6-12. + period: 2026 + input: + age: 17 + employment_income: 2_600 * 12 + output: + ssi_ineligible_child_allocation: 1_228 # 4 * 307 + +- name: A child who is not a student has no wage exclusion + period: 2026 + input: + age: 17 + is_in_k12_school: false + employment_income: 300 * 12 + output: + ssi_ineligible_child_allocation: 2_364 # 5,964 - 3,600 + +- name: A child's net business loss does not raise the allocation + # 416.1163(b)(1)-(2): the allocation is the couple rate less the + # individual rate, reduced by the child's income. A loss is no income. + period: 2026 + input: + age: 17 + is_in_k12_school: false + ssi_earned_income: -20_000 + output: + ssi_ineligible_child_allocation: 5_964 # $497 * 12 + +- name: Negative unearned income neither raises the allocation nor offsets wages + # $300 a month of wages reduce it; the -$250 a month does not. + period: 2026 + input: + age: 17 + is_in_k12_school: false + ssi_earned_income: 300 * 12 + ssi_unearned_income: -3_000 + output: + ssi_ineligible_child_allocation: 2_364 # 5,964 - 3,600 + - name: Ineligible child's pretax premiums do not reduce the child allocation as wages. period: 2025 absolute_error_margin: 0.01 input: age: 14 + # Isolate the pretax-premium rule from the new student exclusion. + is_in_k12_school: false employment_income: 2_400 pre_tax_health_insurance_premiums: 1_200 output: diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_unearned_income_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_unearned_income_deemed_from_ineligible_parent.yaml new file mode 100644 index 00000000000..6ac59e6060f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_unearned_income_deemed_from_ineligible_parent.yaml @@ -0,0 +1,154 @@ +- name: Review r2 excludes a permanently absent adult student's allocation from parental deeming + # 20 CFR 416.1165(b): allocations cover ineligible children in the household. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + ssi_unearned_income: 24_000 + claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + student: + age: 20 + person_id: 3 + parent_1_id: 1 + is_tax_unit_dependent: true + is_full_time_college_student: true + tax_units: + family: + members: [parent, claimant, student] + marital_units: + parent_unit: + members: [parent] + claimant_unit: + members: [claimant] + student_unit: + members: [student] + households: + home: + members: [parent, claimant] + state_code: TX + away: + members: [student] + state_code: TX + output: + people: + claimant: + # 24,000 - 240 general exclusion - 11,928 parent allocation. + ssi_unearned_income_deemed_from_ineligible_parent: 11_832 + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 11_832 + # 11,928 - (11,832 - 240 own general exclusion). + ssi: 336 + student: + ssi_ineligible_child_allocation: 5_964 + +- name: Review r2 excludes an immigration-ineligible sibling from the SSI parental recipient divisor + # 20 CFR 416.1165(e)(2): divide the remainder among eligible children only. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + ssi_unearned_income: 24_000 + claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + sibling: + age: 12 + person_id: 3 + parent_1_id: 1 + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + tax_units: + family: + members: [parent, claimant, sibling] + marital_units: + parent_unit: + members: [parent] + claimant_unit: + members: [claimant] + sibling_unit: + members: [sibling] + households: + home: + members: [parent, claimant, sibling] + state_code: TX + output: + people: + claimant: + # 24,000 - 5,964 sibling allocation - 240 - 11,928 = 5,868. + ssi_unearned_income_deemed_from_ineligible_parent: 5_868 + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 5_868 + ssi: 6_300 + sibling: + is_ssi_ineligible_child: true + ssi_ineligible_child_allocation: 5_964 + ssi_unearned_income_deemed_from_ineligible_parent: 0 + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 0 + +- name: Review r2 Medicaid uses California's broader immigration eligibility independently of SSI + # California covers undocumented minors. Both are Medicaid recipients, so + # neither receives an ineligible-child allocation in Medicaid's own pool. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + ssi_unearned_income: 24_000 + claimant: + age: 10 + person_id: 2 + parent_1_id: 1 + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: CITIZEN + sibling: + age: 12 + person_id: 3 + parent_1_id: 1 + is_tax_unit_dependent: true + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + tax_units: + family: + members: [parent, claimant, sibling] + marital_units: + parent_unit: + members: [parent] + claimant_unit: + members: [claimant] + sibling_unit: + members: [sibling] + households: + home: + members: [parent, claimant, sibling] + state_code: CA + output: + people: + claimant: + ssi_unearned_income_deemed_from_ineligible_parent: 5_868 + # (24,000 - 230 * 12 - 11,928 parent allocation) / 2. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 4_656 + sibling: + is_medicaid_immigration_status_eligible: true + ssi_unearned_income_deemed_from_ineligible_parent: 0 + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: 4_656 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.yaml new file mode 100644 index 00000000000..b6876e8691f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.yaml @@ -0,0 +1,202 @@ +# 416.1160(d): an ineligible child is a child, 'as the term child is defined +# in § 416.1101', who 'is not eligible for SSI benefits'. 416.1856: under 18, +# or under 22 and a student regularly attending school; not married; not the +# head of a household. + +- name: An unmarried 17-year-old is a child + period: 2026 + input: + age: 17 + output: + is_ssi_ineligible_child: true + +- name: An 18-year-old who is not in school is not a child + period: 2026 + input: + age: 18 + output: + is_ssi_ineligible_child: false + +- name: A 21-year-old full-time college student is a child + period: 2026 + input: + age: 21 + is_full_time_college_student: true + output: + is_ssi_ineligible_child: true + +- name: A 22-year-old full-time college student is not a child + period: 2026 + input: + age: 22 + is_full_time_college_student: true + output: + is_ssi_ineligible_child: false + +- name: A 16-year-old head of a household is not a child + period: 2026 + input: + age: 16 + is_household_head: true + output: + is_ssi_ineligible_child: false + +- name: A married 17-year-old is not a child + # Tax roles are supplied: when they are omitted the model makes the minor + # spouse a dependent, and the SSI spouse test cannot then see the marriage. + period: 2026 + input: + people: + wife: + age: 17 + is_tax_unit_spouse: true + husband: + age: 20 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [wife, husband] + marital_units: + couple: + members: [wife, husband] + households: + household: + members: [wife, husband] + output: + is_ssi_ineligible_child: [false, false] + +- name: A disabled child who is a citizen is treated as eligible for SSI + period: 2026 + input: + age: 10 + is_ssi_disabled: true + output: + is_ssi_ineligible_child: false + +- name: A disabled child without qualifying immigration status is not eligible for SSI + period: 2026 + input: + age: 10 + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + output: + is_ssi_ineligible_child: true + +- name: Review r2 a married student remains married while her husband lives elsewhere + # 20 CFR 416.1856(b) and 416.1806(a)(1): legal marriage does not require + # co-residence, although spousal deeming does. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + person_id: 1 + is_tax_unit_head: true + spouse: + age: 60 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + daughter: + age: 20 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_spouse: true + is_full_time_college_student: true + husband: + age: 22 + person_id: 4 + is_tax_unit_head: true + tax_units: + parents: + members: [claimant, spouse] + daughter_couple: + members: [daughter, husband] + marital_units: + parents: + members: [claimant, spouse] + daughter_unit: + members: [daughter] + husband_unit: + members: [husband] + households: + home: + members: [claimant, spouse, daughter] + state_code: TX + away: + members: [husband] + state_code: TX + output: + people: + daughter: + is_ssi_ineligible_child: false + ssi_ineligible_child_allocation: 0 + claimant: + ssi: 10_932 + +- name: Review r2 a 21-year-old student was not an ineligible child before June 2008 + # POMS SI 01310.115 A.3: before June 16, 2008 the deeming age limit was 21. + # The general SSI child definition keeps its separate student age limit. + period: 1986 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + person_id: 1 + is_tax_unit_head: true + spouse: + age: 60 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 252 * 12 + student: + age: 21 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + is_full_time_college_student: true + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: TX + output: + people: + student: + is_ssi_ineligible_child: false + ssi_ineligible_child_allocation: 0 + claimant: + ssi: 3_264 + +- name: Review r2 the deeming student age limit is under 21 in May 2008 + period: 2008-05 + input: + # Demographic inputs are annual; do not date them after January 1. + age: + 2008: 21 + is_full_time_college_student: + 2008: true + output: + is_ssi_ineligible_child: false + +- name: Review r2 the deeming student age limit is under 22 in July 2008 + period: 2008-07 + input: + # Demographic inputs are annual; do not date them after January 1. + age: + 2008: 21 + is_full_time_college_student: + 2008: true + output: + is_ssi_ineligible_child: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_eligible.yaml index 357e7530ca7..e039fc02ef4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_eligible.yaml @@ -159,11 +159,31 @@ - name: Case 10, SSI spousal deeming triggers the married SSI SSP standard. period: 2026 input: - state_code: CA - is_disabled: true - employment_income: 1_000 - pension_income: 20_000 - ssi_countable_resources: 10_000 - is_ssi_spousal_deeming_applies: true + people: + recipient: + age: 45 + is_disabled: true + meets_ssi_disability_criteria: true + employment_income: 1_000 + pension_income: 20_000 + ssi_countable_resources: 10_000 + is_tax_unit_head: true + spouse: + age: 45 + is_tax_unit_spouse: true + pension_income: 600 * 12 + tax_units: + couple: + members: [recipient, spouse] + marital_units: + couple: + members: [recipient, spouse] + households: + home: + members: [recipient, spouse] + state_code: CA output: - ca_wdp_ssi_ssp_income_eligible: true + people: + recipient: + is_ssi_spousal_deeming_applies: true + ca_wdp_ssi_ssp_income_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.yaml new file mode 100644 index 00000000000..184ae663be6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.yaml @@ -0,0 +1,48 @@ +- name: WDP annual screen preserves its annual approximation when December deeming differs + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + person_id: 1 + is_tax_unit_head: true + is_ssi_disabled: true + ssi_unearned_income: 16_000 + spouse: + age: 48 + person_id: 2 + is_tax_unit_spouse: true + ssi_unearned_income: 600 * 12 + student: + age: 16 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 1_000 * 12 + tax_units: + family: + members: [claimant, spouse, student] + marital_units: + parents: + members: [claimant, spouse] + student_unit: + members: [student] + households: + home: + members: [claimant, spouse, student] + state_code: CA + output: + people: + claimant: + # The annual stock value reflects December, when the student's + # exclusion is exhausted and SSI deeming applies. WDP's existing + # annual approximation instead compares the $2,500 annual spouse + # remainder with the annual differential, $5,964. + is_ssi_spousal_deeming_applies: true + # $16,000 - $240 = $15,760 exceeds the single SSI/SSP standard, + # $1,233.94 * 12 = $14,807.28. December's flag must not select + # the couple standard for this whole-year approximation. + ca_wdp_ssi_ssp_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.yaml index 1f1d9f3fd88..b0b413be60c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.yaml @@ -21,3 +21,22 @@ ssi_couple_computation_applies: true output: co_oap_in_kind_support_and_maintenance: 150 + +- name: Matching SSI ISM follows each month's amount rather than the annual average. + period: 2026 + absolute_error_margin: 0.01 + input: + ssi: 0 + receives_ssi: true + ssi_in_kind_support_and_maintenance_monthly: + 2026-01: 300 + 2026-02: 120 + ssi_couple_computation_applies: true + output: + # Other months have no ISM, so the annual total is 300 + 120 = 420. + ssi_in_kind_support_and_maintenance: 420 + # The recipient's share is January's 300 / 2 = 150 and February's + # 120 / 2 = 60. Averaging the year would give 420 / 12 / 2 = 17.50. + co_oap_in_kind_support_and_maintenance: + 2026-01: 150 + 2026-02: 60 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.yaml new file mode 100644 index 00000000000..add648ddbac --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.yaml @@ -0,0 +1,61 @@ +- name: A blind child's parental deeming uses January's student allocation. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + is_tax_unit_head: true + employment_income: 31_200 + child: + age: 9 + person_id: 2 + parent_1_id: 1 + is_tax_unit_dependent: true + is_blind: true + student: + age: 19 + person_id: 3 + parent_1_id: 1 + is_tax_unit_dependent: true + is_full_time_college_student: true + employment_income: 24_000 + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + student_unit: + members: [student] + tax_units: + family_tax_unit: + members: [parent, child, student] + families: + family: + members: [parent, child, student] + households: + home: + members: [parent, child, student] + state_code: NH + output: + people: + child: + # Student earnings are 24,000 / 12 = 2,000 per month. The 9,730 + # annual student exclusion is exhausted in May: sibling allocations + # are 497 Jan-Apr, 497 - (2,000 - 1,730) = 227 May, and 0 Jun-Dec. + # January parental deeming: (2,600 - 497 - 85) / 2 - 994 = 15. + ssi_unearned_income_deemed_from_ineligible_parent_monthly: 15 + # May deeming is (2,600 - 227 - 85) / 2 - 994 = 150; Jun-Dec is + # (2,600 - 85) / 2 - 994 = 263.50. Annual deeming is therefore + # 4 * 15 + 150 + 7 * 263.50 = 2,054.50; its monthly average is 171.21. + ssi_unearned_income_deemed_from_ineligible_parent: + 2026: 2_054.5 + # January's 15 is below the federal 20 general exclusion: full SSI. + ssi: 994 + # NH counts January's 15 + 994 SSI - 13 standard disregard = 996. + nh_ssp_countable_income: 996 + # One-person standard: 994 + 27 - 13 = 1,008; grant: 1,008 - 996 = 12. + # Annual-average deeming would give 171.21 + 994 - 13 = 1,152.21 + # countable income and incorrectly pay no state grant. + nh_ssp: 12 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/otda/ssp/ny_ssp.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/otda/ssp/ny_ssp.yaml index 0559c19f184..8ad24555b88 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/otda/ssp/ny_ssp.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/otda/ssp/ny_ssp.yaml @@ -739,3 +739,122 @@ # counts as living alone. The resident gets the family care rate. ny_ssp_living_arrangement: [LIVING_ALONE, FAMILY_CARE] ny_ssp: [87, 228.48] + +- name: Case 49, student exclusions change spousal deeming and SSP during the year. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + meets_ssi_disability_criteria: true + spouse: + age: 43 + pension_income: 14_400 + student1: + age: 19 + is_full_time_student: true + employment_income: 24_000 + student2: + age: 20 + is_full_time_student: true + employment_income: 24_000 + marital_units: + couple: + members: [claimant, spouse] + student1: + members: [student1] + student2: + members: [student2] + tax_units: + family: + members: [claimant, spouse, student1, student2] + households: + household: + members: [claimant, spouse, student1, student2] + state_code: NY + output: + # Each student earns 2_000/month. January-April exclude all earnings: + # 1_200 spouse pension - 2 * 497 allocations = 206 <= 497, no deeming. + # In May, 9_730 - 4 * 2_000 = 1_730 of each student's exclusion remains. + # Allocation = 497 - (2_000 - 1_730) = 227 each; countable deemed income + # = 1_200 - 2 * 227 - 20 = 726. June-December allocations are zero, + # so countable income = 1_200 - 20 = 1_180 and SSI = 1_491 - 1_180 = 311. + is_ssi_spousal_deeming_applies_monthly: + 2026-01: [false, false, false, false] + 2026-05: [true, false, false, false] + 2026-06: [true, false, false, false] + ssi_countable_income_monthly: + 2026-01: [0, 0, 0, 0] + 2026-05: [726, 0, 0, 0] + 2026-06: [1_180, 0, 0, 0] + ssi: + 2026-01: [994, 0, 0, 0] + 2026-05: [765, 0, 0, 0] + 2026-06: [311, 0, 0, 0] + # Living with others rate 23. Through May countable income is below 994; + # June-December max(23 - (1_180 - 994), 0) = 0. Annual = 5 * 23 = 115. + # The annual-average countable income (726 + 7 * 1_180) / 12 = 748.83 + # would incorrectly preserve all 12 payments, totaling 276. + ny_ssp: + 2026-01: [23, 0, 0, 0] + 2026-05: [23, 0, 0, 0] + 2026-06: [0, 0, 0, 0] + 2026-12: [0, 0, 0, 0] + 2026: [115, 0, 0, 0] + +- name: Case 50, January spousal deeming uses January status rather than December. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: NY + age: 65 + ny_ssp_living_arrangement: LIVING_WITH_OTHERS + is_ssi_spousal_deeming_applies_monthly: + 2026-01: true + 2026-12: false + ssi_countable_income_monthly: + 2026-01: 1_180 + ssi_income_deemed_from_ineligible_spouse_monthly: + 2026-01: 1_180 + output: + # January's SSI couple budget is 1_491 - 1_180 = 311. New York compares + # countable income with the individual FBR: max(23 - (1_180 - 994), 0). + # December's false status would retain the shared helper's 23 payment. + ssi: 311 + ny_ssp: 0 + +- name: Case 51, personal needs allowance uses the facility resident's monthly income. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: NY + age: 70 + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true + ssi_countable_income_monthly: + 2026-01: 0 + 2026-02: 0 + 2026-03: 0 + 2026-04: 0 + 2026-05: 0 + 2026-06: 0 + 2026-07: 10 + 2026-08: 10 + 2026-09: 10 + 2026-10: 10 + 2026-11: 10 + 2026-12: 10 + output: + # Federal facility rate 30 less countable income 0/10 gives SSI 30/20. + # New York pays 25 only in the six zero-income months: 6 * 25 = 150. + # Annual countable income is 60, so its average of 5 would wrongly + # suppress the allowance even in January. + ssi: + 2026-01: 30 + 2026-07: 20 + 2026: 300 + ny_ssp: + 2026-01: 25 + 2026-07: 0 + 2026: 150 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/ssp/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/ssp/integration.yaml index 95731b1700c..184eb2cdb4e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/ssp/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/ssp/integration.yaml @@ -418,6 +418,7 @@ employment_income: 41_600 person3: age: 16 + is_in_k12_school: false employment_income: 3_600 person4: age: 9 @@ -571,3 +572,178 @@ # 757.50 + 783.50 = 1_541, not below the 1_532 ineligible spouse standard. ok_ssp_countable_income: [1_541, 1_541] ok_ssp: [0, 0] + +- name: Case 20, a teen student's excluded earnings preserve the federal child allocation. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + meets_ssi_disability_criteria: true + person2: + age: 43 + employment_income: 41_600 + person3: + age: 16 + is_in_k12_school: true + employment_income: 3_600 + person4: + age: 9 + marital_units: + couple: + members: [person1, person2] + teen: + members: [person3] + child: + members: [person4] + families: + family: + members: [person1, person2, person3, person4] + households: + hh: + members: [person1, person2, person3, person4] + state_code: OK + output: + # Oklahoma counts the teen's earnings: (498 - 300) + 498 = 696. + ok_ssp_spouse_child_allocation: [696, 696, 696, 696] + # Federal SSI excludes the student's 300 monthly earnings, so allocations + # are 2 * 497 = 994. Deemed earnings: 3_466.67 - 994 = 2_472.67 > 497. + # (2_472.67 - 85) / 2 = 1_193.83; 1_491 - 1_193.83 = 297.17 SSI. + ssi: [297.17, 0, 0, 0] + ok_ssp_eligible: [true, false, false, false] + # Claimant: 297.17 SSI. Spouse: (3_467 - 85) / 2 = 1_691. + # Teen: (300 - 85) / 2 = 107.5. + ok_ssp_own_countable_income: [297.17, 1_691, 107.5, 0] + # Combined: (3_467 - 696 - 85) / 2 + 297.17 = 1_640.17 > 1_532. + # The combined-income helper also subtracts the family's 696 allocation + # from the ineligible teen's 300 earnings, leaving 0 for that unit. + ok_ssp_countable_income: [1_640.17, 1_640.17, 0, 0] + ok_ssp: [0, 0, 0, 0] + +- name: Case 21, a disabled child's parental deeming uses January's student allocation. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + employment_income: 31_200 + child: + age: 9 + person_id: 2 + parent_1_id: 1 + meets_ssi_disability_criteria: true + student: + age: 19 + person_id: 3 + parent_1_id: 1 + is_full_time_college_student: true + employment_income: 24_000 + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + student_unit: + members: [student] + families: + family: + members: [parent, child, student] + households: + hh: + members: [parent, child, student] + state_code: OK + output: + # The student's 2_000 monthly earnings use up the 9_730 annual exclusion + # in May. Sibling allocations are 497 Jan-Apr, 227 May, and 0 Jun-Dec. + # January parental deeming: (2_600 - 497 - 85) / 2 - 994 = 15. + # May is 150; Jun-Dec is 263.5. Annual average deeming would be + # (4 * 15 + 150 + 7 * 263.5) / 12 = 171.21, which does not apply here. + ssi_unearned_income_deemed_from_ineligible_parent_monthly: [0, 15, 0] + # January's 15 is below the child's 20 general exclusion, so full SSI. + ssi: [0, 994, 0] + # Parent: (2_600 - 85) / 2 = 1_257.5. Child: max(15 - 20, 0) + 994. + # Student: (2_000 - 85) / 2 = 957.5. + ok_ssp_own_countable_income: [1_257.5, 994, 957.5] + # min(1_034 - 994, 40) = 40. The annual average would instead produce + # 171.21 - 20 + 994 = 1_145.21 and incorrectly bar the child's SSP. + ok_ssp: [0, 40, 0] + +- name: Case 22, June in-kind support follows that month's spousal deeming. + # Annual inputs cover calendar 2026; outputs below select June. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + person_id: 1 + is_tax_unit_head: true + meets_ssi_disability_criteria: true + ssi_receives_outside_shelter_support: true + spouse: + age: 43 + person_id: 2 + is_tax_unit_spouse: true + pension_income: 14_400 + student1: + age: 19 + person_id: 3 + is_tax_unit_dependent: true + parent_1_id: 1 + parent_2_id: 2 + is_full_time_college_student: true + employment_income: 24_000 + student2: + age: 20 + person_id: 4 + is_tax_unit_dependent: true + parent_1_id: 1 + parent_2_id: 2 + is_full_time_college_student: true + employment_income: 24_000 + marital_units: + couple: + members: [claimant, spouse] + student1_unit: + members: [student1] + student2_unit: + members: [student2] + tax_units: + family_tax_unit: + members: [claimant, spouse, student1, student2] + families: + family: + members: [claimant, spouse, student1, student2] + households: + hh: + members: [claimant, spouse, student1, student2] + state_code: OK + output: + # Student exclusions run out in May, so no federal child allocation + # remains in June. Spouse's 1_200 pension is above the 497 threshold. + is_ssi_spousal_deeming_applies_monthly: + 2026-01: [false, false, false, false] + 2026-06: [true, false, false, false] + # ISM Jan-Apr is 994 / 3 + 20 = 351.33; May-Dec is + # 1_491 / 6 + 10 = 258.5. The annual average, 289.44, is not June's ISM. + ssi_in_kind_support_and_maintenance_monthly: + 2026-06: [258.5, 0, 0, 0] + # Federal SSI: 1_491 - ((258.5 - 20) + (1_200 - 20)) = 72.5. + ssi: + 2026-06: [72.5, 0, 0, 0] + # Oklahoma's allocation is 0 because each student's gross 2_000 > 498. + ok_ssp_spouse_child_allocation: + 2026-06: [0, 0, 0, 0] + # Claimant: 258.5 - 20 + 72.5 = 311. Spouse: 1_200 - 20 = 1_180. + # Each student: (2_000 - 85) / 2 = 957.5. + ok_ssp_own_countable_income: + 2026-06: [311, 1_180, 957.5, 957.5] + # Combined: 258.5 + 1_200 - 20 + 72.5 = 1_511, leaving 1_532 - 1_511 = 21. + # Using annual-average ISM would give 1_541.94 and incorrectly pay 0. + ok_ssp_countable_income: + 2026-06: [1_511, 1_511, 957.5, 957.5] + ok_ssp: + 2026-06: [21, 0, 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..8a9df91b7a4 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,30 @@ va_ag: 2026-01: 558 2026-02: 1_223 + +- name: Case 31, SSI recipients use each month's verified countable income. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: VA + age: 65 + va_ag_certified: true + ssi_countable_income_monthly: + 2026-01: 100 + 2026-02: 300 + output: + # January SSI: 994 - 100 = 894; February SSI: 994 - 300 = 694. + ssi: + 2026-01: 894 + 2026-02: 694 + # Other months have no income, so the annual total is 100 + 300 = 400. + ssi_countable_income: 400 + # AG counts January's 100 + 894 and February's 300 + 694, both 994. + # The annual average, 400 / 12 = 33.33, would give 927.33 and 727.33. + va_ag_countable_income: + 2026-01: 994 + 2026-02: 994 + # Each full-month grant is 2,130 + 87 - 994 = 1,223. + va_ag: + 2026-01: 1_223 + 2026-02: 1_223 diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py index 2ecd23b455a..7ce567ee0c9 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py @@ -1,10 +1,4 @@ from policyengine_us.model_api import * -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.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, -) class medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent( @@ -15,77 +9,17 @@ class medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligibl label = "Medicaid optional senior or disabled unearned income deemed from ineligible parent" unit = USD definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/20/416.1165" + reference = ( + "https://www.law.cornell.edu/cfr/text/20/416.1165", + "https://www.law.cornell.edu/uscode/text/42/1396b#v", + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-020-00/0805-020-15/", + "https://www.emhandbooks.wisconsin.gov/meh-ebd/policy_files/1/1.1.htm#1.1.3.3", + "https://www.emhandbooks.wisconsin.gov/meh-ebd/policy_files/15/15.1.htm#15.1.2", + ) def formula(person, period, parameters): - eligible_child = person("is_ssi_aged_blind_disabled", period) & person( - "is_child", period + # Aggregate monthly budgets while retaining the annual input contract. + return person( + "medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent_monthly", + period, ) - ineligible_parent = person("is_ssi_ineligible_parent", period) - tax_unit = person.tax_unit - - prereduction_parental_earned_income = eligible_child * tax_unit.sum( - person("ssi_earned_income", period) * ineligible_parent - ) - blind_disabled_working_student_income = person( - "ssi_blind_or_disabled_working_student_exclusion", period - ) - parental_earned_income = max_( - prereduction_parental_earned_income - blind_disabled_working_student_income, - 0, - ) - - parental_unearned_income = eligible_child * tax_unit.sum( - person("ssi_unearned_income", period) * ineligible_parent - ) - - child_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_child_allocation"] - ) - parental_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_parent_allocation"] - ) - - parental_unearned_income -= child_allocations - remaining_child_allocations = max_(0, -parental_unearned_income) - parental_unearned_income = max_(0, parental_unearned_income) - parental_earned_income = max_( - 0, parental_earned_income - remaining_child_allocations - ) - - state = person.household("state_code_str", period) - general_disregard = parameters( - period - ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard.individual[ - state - ] - # Missouri deems parental income in the SSI order: the $20 - # exclusion comes off before the $65 and the half (DSS Manual - # § 0805.020.15 steps 4-6), unlike its applicant budget. - net_parental_deemed_income = where( - state == "MO", - _apply_ssi_exclusions( - parental_earned_income, - parental_unearned_income, - parameters, - period, - general_exclusion=general_disregard, - ), - _apply_medicaid_optional_senior_or_disabled_exclusions( - parental_earned_income, - parental_unearned_income, - state, - general_disregard, - parameters, - period, - ), - ) - net_parental_deemed_income = max_( - 0, net_parental_deemed_income - parental_allocations - ) - - count_eligible_children = tax_unit.sum(eligible_child) - income = np.zeros_like(count_eligible_children, dtype=float) - mask = count_eligible_children > 0 - income[mask] = net_parental_deemed_income[mask] / count_eligible_children[mask] - return income diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent_monthly.py b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent_monthly.py new file mode 100644 index 00000000000..33b0e025d80 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent_monthly.py @@ -0,0 +1,156 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * +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.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_child_allocation_scope import ( + ssi_parental_child_allocation_total, +) + + +class medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent_monthly( + Variable +): + value_type = float + entity = Person + label = "Medicaid optional senior or disabled unearned income deemed from ineligible parent" + unit = USD + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/cfr/text/20/416.1165", + "https://www.law.cornell.edu/uscode/text/42/1396b#v", + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-020-00/0805-020-15/", + "https://www.emhandbooks.wisconsin.gov/meh-ebd/policy_files/1/1.1.htm#1.1.3.3", + "https://www.emhandbooks.wisconsin.gov/meh-ebd/policy_files/15/15.1.htm#15.1.2", + ) + + def formula(person, period, parameters): + supplied = annual_input( + person, + period, + "medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent", + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + # Medicaid's own immigration rules include eligible groups who do + # not qualify for SSI, including state-covered noncitizen children. + eligible_child = ( + person("is_ssi_aged_blind_disabled", period) + & person("is_child", period) + & person("is_medicaid_immigration_status_eligible", period) + ) + ineligible_parent = person("is_ssi_ineligible_parent", period) + tax_unit = person.tax_unit + + prereduction_parental_earned_income = eligible_child * tax_unit.sum( + person("ssi_earned_income", period) * ineligible_parent + ) + blind_disabled_working_student_income = person( + "ssi_blind_or_disabled_working_student_exclusion", period + ) + parental_earned_income = max_( + prereduction_parental_earned_income - blind_disabled_working_student_income, + 0, + ) + + parental_unearned_income = eligible_child * tax_unit.sum( + person("ssi_unearned_income", period) * ineligible_parent + ) + + state = person.household("state_code_str", period) + # State allocation definitions can be narrower than SSI's adult + # student definition. Apply each sourced ceiling before deducting + # the shared allocation amount; other states retain their existing + # shared allocation behavior. + age = person("age", period.this_year) + age_limits = parameters( + period + ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.deeming.ineligible_child_age_limit + allocation_child = np.ones(person.count, dtype=bool) + for state_code in age_limits._children: + # State breakdown expansion fills omitted states with zero. + # Only a positive, sourced ceiling overrides the shared scope. + age_limit = age_limits[state_code] + if age_limit > 0: + allocation_child &= (state != state_code) | (age < age_limit) + # These states define allocation children as nonblind and + # nondisabled, independently of Medicaid immigration eligibility. + allocation_child &= ~( + np.isin(state, ("FL", "LA", "NY", "VA", "WI")) + & person("is_ssi_aged_blind_disabled", period) + ) + # MEH 1.1.3.3 and 15.1.2 define dependent 18-year-olds through + # full-time secondary school or equivalent-level training, not + # tax dependency. Enrollment proxies expected completion before + # 19: the model has no completion-date or remaining-credit input. + # At age 18, explicit K-12 enrollment is also secondary-school + # evidence; the age gate excludes younger pupils from this extension. + dependent_18_year_old = ( + (age == 18) + & ( + person("is_in_secondary_school", period.this_year) + | person("is_in_k12_school", period.this_year) + ) + & person("is_full_time_student", period.this_year) + ) + allocation_child &= (state != "WI") | (age < 18) | dependent_18_year_old + allocations = ( + person("ssi_ineligible_child_allocation_monthly", period) * allocation_child + ) + child_allocations = eligible_child * ssi_parental_child_allocation_total( + person, period, eligible_child, allocations + ) + parental_allocations = eligible_child * add( + tax_unit, period, ["ssi_ineligible_parent_allocation"] + ) + + parental_unearned_income -= child_allocations + remaining_child_allocations = max_(0, -parental_unearned_income) + parental_unearned_income = max_(0, parental_unearned_income) + parental_earned_income = max_( + 0, parental_earned_income - remaining_child_allocations + ) + + general_disregard = parameters( + period + ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard.individual[ + state + ] + # Missouri deems parental income in the SSI order: the $20 + # exclusion comes off before the $65 and the half (DSS Manual + # § 0805.020.15 steps 4-6), unlike its applicant budget. + net_parental_deemed_income = where( + state == "MO", + _apply_ssi_exclusions( + parental_earned_income, + parental_unearned_income, + parameters, + period, + general_exclusion=general_disregard, + monthly=True, + ), + _apply_medicaid_optional_senior_or_disabled_exclusions( + parental_earned_income * MONTHS_IN_YEAR, + parental_unearned_income * MONTHS_IN_YEAR, + state, + general_disregard, + parameters, + period, + ) + / MONTHS_IN_YEAR, + ) + net_parental_deemed_income = max_( + 0, net_parental_deemed_income - parental_allocations + ) + + count_eligible_children = tax_unit.sum(eligible_child) + income = np.zeros_like(count_eligible_children, dtype=float) + mask = count_eligible_children > 0 + income[mask] = net_parental_deemed_income[mask] / count_eligible_children[mask] + return where(eligible_child, income, 0) 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..47cf98bfcb8 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 @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.hhs.medicaid.income._apply_medicaid_optional_senior_or_disabled_exclusions import ( - _apply_medicaid_optional_senior_or_disabled_exclusions, -) class medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse( @@ -19,57 +16,8 @@ class medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse( unit = USD def formula(person, period, parameters): - deeming_applies = person("is_ssi_spousal_deeming_applies", period) - spouse_earned = person( - "ssi_earned_income_deemed_from_ineligible_spouse", period - ) - spouse_unearned = person( - "ssi_unearned_income_deemed_from_ineligible_spouse", period - ) - individual_earned = person("ssi_earned_income", period) - individual_unearned = person("ssi_unearned_income", period) - - p = parameters( - period - ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard - state = person.household("state_code_str", period) - - # Missouri MHABD considers the gross income of a married couple - # living together (DSS Manual § 0805.015.05) rather than the SSI - # 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) - - ineligible_spouse * individual_earned - ) - spouse_gross_unearned = ( - marital_unit.sum(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 - deeming_applies = deeming_applies | (is_mo & has_ineligible_spouse) - - alone_countable = _apply_medicaid_optional_senior_or_disabled_exclusions( - individual_earned, - individual_unearned, - state, - p.individual[state], - parameters, + # Aggregate monthly budgets while retaining the annual input contract. + return person( + "medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly", period, ) - couple_countable = _apply_medicaid_optional_senior_or_disabled_exclusions( - individual_earned + spouse_earned, - individual_unearned + spouse_unearned, - state, - p.couple[state], - parameters, - period, - ) - - deemed_amount = max_(0, couple_countable - alone_countable) - return deeming_applies * deemed_amount diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly.py b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly.py new file mode 100644 index 00000000000..69c6caabf3e --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly.py @@ -0,0 +1,95 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.medicaid.income._apply_medicaid_optional_senior_or_disabled_exclusions import ( + _apply_medicaid_optional_senior_or_disabled_exclusions, +) + + +class medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly( + Variable +): + value_type = float + entity = Person + label = "Medicaid optional senior or disabled income deemed from ineligible spouse" + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/cfr/text/20/416.1163", + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-015-00/0805-015-05/", + ) + unit = USD + + def formula(person, period, parameters): + supplied = annual_input( + person, + period, + "medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse", + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + deeming_applies = person("is_ssi_spousal_deeming_applies_monthly", period) + spouse_earned = person( + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", period + ) + spouse_unearned = person( + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", period + ) + individual_earned = person("ssi_earned_income", period) + individual_unearned = person("ssi_unearned_income", period) + + p = parameters( + period + ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard + state = person.household("state_code_str", period) + + # Missouri MHABD considers the gross income of a married couple + # living together (DSS Manual § 0805.015.05) rather than the SSI + # 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) + - ineligible_spouse * individual_earned + ) + spouse_gross_unearned = ( + marital_unit.sum(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 + deeming_applies = deeming_applies | (is_mo & has_ineligible_spouse) + + alone_countable = ( + _apply_medicaid_optional_senior_or_disabled_exclusions( + individual_earned * MONTHS_IN_YEAR, + individual_unearned * MONTHS_IN_YEAR, + state, + p.individual[state], + parameters, + period, + ) + / MONTHS_IN_YEAR + ) + couple_countable = ( + _apply_medicaid_optional_senior_or_disabled_exclusions( + (individual_earned + spouse_earned) * MONTHS_IN_YEAR, + (individual_unearned + spouse_unearned) * MONTHS_IN_YEAR, + state, + p.couple[state], + parameters, + period, + ) + / MONTHS_IN_YEAR + ) + + deemed_amount = max_(0, couple_countable - alone_countable) + return where( + person("is_ssi_eligible_individual", period), + deeming_applies * deemed_amount, + 0, + ) 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 044371e7419..1fec8756b1e 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 @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.hhs.medicaid.income._apply_medicaid_optional_senior_or_disabled_exclusions import ( - _apply_medicaid_optional_senior_or_disabled_exclusions, -) class medicaid_optional_senior_or_disabled_countable_income(Variable): @@ -20,58 +17,7 @@ class medicaid_optional_senior_or_disabled_countable_income(Variable): ) def formula(person, period, parameters): - pre_reduction_earned_income = person("ssi_marital_earned_income", period) - blind_disabled_working_student_income = person( - "ssi_blind_or_disabled_working_student_exclusion", period - ) - earned_income = max_( - pre_reduction_earned_income - blind_disabled_working_student_income, - 0, - ) - - total_unearned_income = add( - person, - period, - [ - "ssi_marital_unearned_income", - "medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent", - "ssi_in_kind_support_and_maintenance", - ], - ) - - couple_computation = person("ssi_couple_computation_applies", period) - state = person.household("state_code_str", period) - p = parameters( - period - ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard - income_disregard = where( - couple_computation, - p.couple[state], - p.individual[state], - ) - personal_countable = _apply_medicaid_optional_senior_or_disabled_exclusions( - earned_income, - total_unearned_income, - state, - income_disregard, - parameters, - period, - ) - - spousal_deemed = person( - "medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse", - period, - ) - is_eligible_individual = person("is_ssi_eligible_individual", period) - is_eligible_spouse = person("is_ssi_eligible_spouse", period) - is_eligible = is_eligible_individual | is_eligible_spouse - - return where( - ~is_eligible, - 0, - where( - couple_computation, - personal_countable / 2, - personal_countable + spousal_deemed, - ), + # Aggregate monthly budgets while retaining the annual input contract. + return person( + "medicaid_optional_senior_or_disabled_countable_income_monthly", period ) diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income_monthly.py b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income_monthly.py new file mode 100644 index 00000000000..ec38da4e52f --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income_monthly.py @@ -0,0 +1,89 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.medicaid.income._apply_medicaid_optional_senior_or_disabled_exclusions import ( + _apply_medicaid_optional_senior_or_disabled_exclusions, +) + + +class medicaid_optional_senior_or_disabled_countable_income_monthly(Variable): + value_type = float + entity = Person + label = "Countable income for Medicaid's optional senior or disabled pathway" + unit = USD + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1396a#m", + "https://www.kff.org/medicaid/medicaid-financial-eligibility-for-seniors-and-people-with-disabilities-findings-from-a-50-state-survey/", + # Missouri MHABD budgeting order and rounding + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-015-00/", + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-015-00/0805-015-25/", + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-015-00/0805-015-40/", + ) + + def formula(person, period, parameters): + supplied = annual_input( + person, period, "medicaid_optional_senior_or_disabled_countable_income" + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + pre_reduction_earned_income = person("ssi_marital_earned_income", period) + blind_disabled_working_student_income = person( + "ssi_blind_or_disabled_working_student_exclusion", period + ) + earned_income = max_( + pre_reduction_earned_income - blind_disabled_working_student_income, + 0, + ) + + total_unearned_income = add( + person, + period, + [ + "ssi_marital_unearned_income", + "medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent_monthly", + "ssi_in_kind_support_and_maintenance_monthly", + ], + ) + + couple_computation = person("ssi_couple_computation_applies", period) + state = person.household("state_code_str", period) + p = parameters( + period + ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard + income_disregard = where( + couple_computation, + p.couple[state], + p.individual[state], + ) + personal_countable = ( + _apply_medicaid_optional_senior_or_disabled_exclusions( + earned_income * MONTHS_IN_YEAR, + total_unearned_income * MONTHS_IN_YEAR, + state, + income_disregard, + parameters, + period, + ) + / MONTHS_IN_YEAR + ) + + spousal_deemed = person( + "medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse_monthly", + period, + ) + is_eligible_individual = person("is_ssi_eligible_individual", period) + is_eligible_spouse = person("is_ssi_eligible_spouse", period) + is_eligible = is_eligible_individual | is_eligible_spouse + + return where( + ~is_eligible, + 0, + where( + couple_computation, + personal_countable / 2, + personal_countable + spousal_deemed, + ), + ) diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py index 73a0ce26fb3..beb87578a6f 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py @@ -32,32 +32,32 @@ class msp_countable_income(Variable): """ def formula(person, period, parameters): - year = period.this_year - earned_income = person("ssi_earned_income", year) - unearned_income = person("ssi_unearned_income", year) - is_medicare_eligible = person("is_medicare_eligible", year) + earned_income = person("ssi_earned_income", period) + unearned_income = person("ssi_unearned_income", period) + is_medicare_eligible = person("is_medicare_eligible", period) both_medicare_eligible = person.marital_unit.sum(is_medicare_eligible) == 2 blind_or_disabled_working_student_exclusion = person( - "ssi_blind_or_disabled_working_student_exclusion", year + "ssi_blind_or_disabled_working_student_exclusion", period ) personal_earned_income = max_( earned_income - blind_or_disabled_working_student_exclusion, 0, ) - deeming_applies = person("is_ssi_spousal_deeming_applies", year) + deeming_applies = person("is_ssi_spousal_deeming_applies_monthly", period) spouse_earned_income = person( - "ssi_earned_income_deemed_from_ineligible_spouse", year + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", period ) spouse_unearned_income = person( - "ssi_unearned_income_deemed_from_ineligible_spouse", year + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", period ) personal_countable_income = _apply_ssi_exclusions( personal_earned_income, unearned_income, parameters, - year, + period, + monthly=True, ) couple_countable = _apply_ssi_exclusions( @@ -68,21 +68,20 @@ def formula(person, period, parameters): ), person.marital_unit.sum(unearned_income), parameters, - year, + period, + monthly=True, ) deemed_countable = _apply_ssi_exclusions( personal_earned_income + spouse_earned_income, unearned_income + spouse_unearned_income, parameters, - year, + period, + monthly=True, ) single_countable = where( deeming_applies, deemed_countable, personal_countable_income ) - annual_countable = where( - both_medicare_eligible, couple_countable, single_countable - ) - return annual_countable / MONTHS_IN_YEAR + return where(both_medicare_eligible, couple_countable, single_countable) 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..a8d05f9be6e 100644 --- a/policyengine_us/variables/gov/ssa/ssi/_ssi_state_supplement.py +++ b/policyengine_us/variables/gov/ssa/ssi/_ssi_state_supplement.py @@ -20,13 +20,13 @@ def ssi_state_supplement( joint = person("ssi_couple_computation_applies", period) couple_total = person.marital_unit.sum(couple_rate / 2) standard = where(joint, couple_total / 2, individual_rate) - countable = person("ssi_countable_income", period) + countable = person("ssi_countable_income_monthly", period) federal_amount = person("ssi_amount_if_eligible", period) excess = max_(countable - federal_amount, 0) payment = max_(standard - excess, 0) - deeming = person("is_ssi_spousal_deeming_applies", period.this_year) - deemed = person("ssi_income_deemed_from_ineligible_spouse", period) + deeming = person("is_ssi_spousal_deeming_applies_monthly", period) + deemed = person("ssi_income_deemed_from_ineligible_spouse_monthly", period) own_income = max_(countable - deemed, 0) ssi = parameters(period).gov.ssa.ssi federal_la = person("ssi_federal_living_arrangement", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_annual_input.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_annual_input.py new file mode 100644 index 00000000000..30e76d941bf --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_annual_input.py @@ -0,0 +1,31 @@ +"""Read legacy annual inputs without averaging calculated monthly budgets.""" + +from policyengine_us.model_api import has_input_for_period + + +def annual_input(person, period, name): + """Return a supplied annual input, or None to calculate the monthly budget. + + A calculated annual aggregate must never replace a month's own budget. + Recording the input-presence dependency also lets Core invalidate cached + monthly budgets when a branch supplies its first annual override later. + """ + simulation = person.simulation + holder = simulation.get_holder(name) + # Core 3.33+ records dependencies by store order. Older supported Core + # versions rely on callers clearing branch caches with get_override_branch. + if hasattr(holder, "_record_store"): + from policyengine_core.data_storage.store_history import next_sequence_number + + holder._record_store(period.this_year, next_sequence_number()) + + abolished = simulation.tax_benefit_system.parameters( + period.this_year + ).gov.abolitions[name] + if ( + holder.variable.is_neutralized + or abolished + or has_input_for_period(simulation, name, period.this_year) + ): + return person(name, period.this_year) + return None diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_apply_ssi_exclusions.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_apply_ssi_exclusions.py index 0b5de7a0f16..e5cc1c7ab68 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_apply_ssi_exclusions.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/_apply_ssi_exclusions.py @@ -7,6 +7,7 @@ def _apply_ssi_exclusions( parameters: ParameterNode, period: Period, general_exclusion: ArrayLike = None, + monthly: bool = False, ) -> ArrayLike: """ Applies standard SSI income exclusions to convert total earned/unearned income @@ -15,12 +16,12 @@ def _apply_ssi_exclusions( 2) Subtract $65 'earned' exclusion from any remaining earned income. 3) Exclude 50% of the remainder of earned income. - All amounts are stored as annual but exclusions are monthly amounts, so we convert - from annual -> monthly, apply monthly exclusions, then go back to annual. + By default inputs and outputs are annual. Monthly callers pass monthly=True + so exclusions apply to that month before annual aggregation. """ - # Convert from annual to monthly - earned_monthly = earned_income / MONTHS_IN_YEAR - unearned_monthly = unearned_income / MONTHS_IN_YEAR + factor = 1 if monthly else MONTHS_IN_YEAR + earned_monthly = earned_income / factor + unearned_monthly = unearned_income / factor p = parameters(period).gov.ssa.ssi.income.exclusions general_exclusion = p.general if general_exclusion is None else general_exclusion @@ -39,5 +40,4 @@ def _apply_ssi_exclusions( # So the fraction that remains is (1 - earned_share). countable_earned = earned_after_flat * (1.0 - p.earned_share) - # Convert back to annual - return (countable_unearned + countable_earned) * MONTHS_IN_YEAR + return (countable_unearned + countable_earned) * factor diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/_ssi_parental_child_allocation_scope.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/_ssi_parental_child_allocation_scope.py new file mode 100644 index 00000000000..52a368fcb88 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/_ssi_parental_child_allocation_scope.py @@ -0,0 +1,102 @@ +"""Relationship scope for allocations deducted before parental income deeming.""" + +import numpy as np + +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_established_spouse_indices, +) +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + group_member_indices, + has_parent_ids, + household_has_parent_ids, + household_member_indices, + unlinked_parent, +) + + +def _allocation_parent_rows(person, period, first, second): + """Resolve the co-resident parents and their spouses for allocation scope. + + Parent ids take precedence. Without ids, only unlinked parents enter + the tax-unit proxy, together with their co-resident spouses, as in the + existing SSI resource-deeming relationship contract. + """ + linked = has_parent_ids(person, period) + spouse = _ssi_established_spouse_indices(person, period, first, second) + first_spouse = np.where(first >= 0, spouse[np.maximum(first, 0)], -1) + second_spouse = np.where(second >= 0, spouse[np.maximum(second, 0)], -1) + named = np.column_stack((first, second, first_spouse, second_spouse)) + rows = [np.where(linked[:, None], named, -1)] + + unlinked = unlinked_parent(person, period) + parent_or_spouse = unlinked | ((spouse >= 0) & unlinked[np.maximum(spouse, 0)]) + households = person.household.reference_entity.members_entity_id + tax_units = person.tax_unit.reference_entity.members_entity_id + for member in group_member_indices(person.tax_unit): + safe = np.maximum(member, 0) + selected = ( + ~linked + & (member >= 0) + & (households[safe] == households) + & parent_or_spouse[safe] + ) + rows.append(np.where(selected, member, -1)[:, None]) + # A parent's spouse can file in a different tax unit. + partner = np.where(selected, spouse[safe], -1) + outside = (partner >= 0) & (tax_units[np.maximum(partner, 0)] != tax_units) + rows.append(np.where(outside, partner, -1)[:, None]) + return np.column_stack(rows) + + +def ssi_parental_child_allocation_total(person, period, recipients, allocations=None): + """Allocations for relevant children who live with this recipient. + + 20 CFR 416.1165(b) limits allocations to ineligible children in the + household. Parent ids identify a child of this person or their parents + and parents' spouses; the unlinked-parent tax-unit proxy applies only + where the parent-link contract permits it. A recipient never receives + an allocation from the same pool of income they share. + + Medicaid callers can supply allocations after applying their state's + child definition, while retaining the same relationship scope. + """ + first, second = co_resident_parent_indices(person, period) + parents = _allocation_parent_rows(person, period, first, second) + own_index = np.arange(person.count) + relatives = np.column_stack((own_index, parents)) + present = relatives >= 0 + safe_relatives = np.maximum(relatives, 0) + tax_unit = person.tax_unit.reference_entity.members_entity_id + relative_tax_units = np.where(present, tax_unit[safe_relatives], -1) + unlinked = unlinked_parent(person, period) + may_presume = ~household_has_parent_ids(person, period) | np.any( + present & unlinked[safe_relatives], axis=1 + ) + linked = has_parent_ids(person, period) + if allocations is None: + allocations = person("ssi_ineligible_child_allocation_monthly", period) + total = np.zeros(person.count) + for child in household_member_indices(person): + safe = np.maximum(child, 0) + named = np.any( + present + & ( + (relatives == first[safe][:, None]) + | (relatives == second[safe][:, None]) + ), + axis=1, + ) + presumed = may_presume & np.any( + present & (relative_tax_units == tax_unit[safe][:, None]), axis=1 + ) + is_parent = np.any(parents == child[:, None], axis=1) + counts = ( + (child >= 0) + & (child != own_index) + & ~is_parent + & ~recipients[safe] + & np.where(linked[safe], named, presumed) + ) + total += np.where(counts, allocations[safe], 0) + return total diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py index a6d6252e24b..add18cfbebc 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, -) class ssi_unearned_income_deemed_from_ineligible_parent(Variable): @@ -13,60 +10,7 @@ class ssi_unearned_income_deemed_from_ineligible_parent(Variable): reference = "https://www.law.cornell.edu/cfr/text/20/416.1165" def formula(person, period, parameters): - eligible_child = person("is_ssi_aged_blind_disabled", period) & person( - "is_child", period + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person( + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", period ) - ineligible_parent = person("is_ssi_ineligible_parent", period) - tax_unit = person.tax_unit - - prereduction_parental_earned_income = eligible_child * tax_unit.sum( - person("ssi_earned_income", period) * ineligible_parent - ) - blind_disabled_working_student_income = person( - "ssi_blind_or_disabled_working_student_exclusion", period - ) - parental_earned_income = max_( - prereduction_parental_earned_income - blind_disabled_working_student_income, - 0, - ) - - parental_unearned_income = eligible_child * tax_unit.sum( - person("ssi_unearned_income", period) * ineligible_parent - ) - - child_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_child_allocation"] - ) - parental_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_parent_allocation"] - ) - - # To understand this ordering of operations, see Example 4 of - # https://www.law.cornell.edu/cfr/text/20/416.1165#h. - - parental_unearned_income -= child_allocations - remaining_child_allocations = max_(0, -parental_unearned_income) - parental_unearned_income = max_(0, parental_unearned_income) - parental_earned_income = max_( - 0, parental_earned_income - remaining_child_allocations - ) - - net_parental_deemed_income = _apply_ssi_exclusions( - parental_earned_income, - parental_unearned_income, - parameters, - period, - ) - - net_parental_deemed_income = max_( - 0, net_parental_deemed_income - parental_allocations - ) - - count_eligible_children = tax_unit.sum(eligible_child) - # avoid array divide-by-zero warnings by not using where() function - # see the following GitHub issue for more details: - # https://github.com/PolicyEngine/policyengine-us/issues/2494 - income = np.zeros_like(count_eligible_children) - mask = count_eligible_children > 0 - income[mask] = net_parental_deemed_income[mask] / count_eligible_children[mask] - return income diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent_monthly.py new file mode 100644 index 00000000000..9dfbd7c04cd --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent_monthly.py @@ -0,0 +1,92 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_child_allocation_scope import ( + ssi_parental_child_allocation_total, +) + + +class ssi_unearned_income_deemed_from_ineligible_parent_monthly(Variable): + value_type = float + entity = Person + label = "SSI unearned income (deemed from ineligible parent)" + unit = USD + definition_period = MONTH + reference = "https://www.law.cornell.edu/cfr/text/20/416.1165" + + def formula(person, period, parameters): + supplied = annual_input( + person, period, "ssi_unearned_income_deemed_from_ineligible_parent" + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + immigration_status = person("immigration_status", period) + is_citizen = immigration_status == immigration_status.possible_values.CITIZEN + # 416.1165(e)(2) divides only among SSI-eligible children. Income + # and resource eligibility depend on this calculation, so use the + # status conditions shared with the ineligible-child classification. + eligible_child = ( + person("is_ssi_aged_blind_disabled", period) + & person("is_child", period) + & (is_citizen | person("is_ssi_qualified_noncitizen", period)) + ) + ineligible_parent = person("is_ssi_ineligible_parent", period) + tax_unit = person.tax_unit + + prereduction_parental_earned_income = eligible_child * tax_unit.sum( + person("ssi_earned_income", period) * ineligible_parent + ) + blind_disabled_working_student_income = person( + "ssi_blind_or_disabled_working_student_exclusion", period + ) + parental_earned_income = max_( + prereduction_parental_earned_income - blind_disabled_working_student_income, + 0, + ) + + parental_unearned_income = eligible_child * tax_unit.sum( + person("ssi_unearned_income", period) * ineligible_parent + ) + + child_allocations = eligible_child * ssi_parental_child_allocation_total( + person, period, eligible_child + ) + parental_allocations = eligible_child * add( + tax_unit, period, ["ssi_ineligible_parent_allocation"] + ) + + # To understand this ordering of operations, see Example 4 of + # https://www.law.cornell.edu/cfr/text/20/416.1165#h. + + parental_unearned_income -= child_allocations + remaining_child_allocations = max_(0, -parental_unearned_income) + parental_unearned_income = max_(0, parental_unearned_income) + parental_earned_income = max_( + 0, parental_earned_income - remaining_child_allocations + ) + + net_parental_deemed_income = _apply_ssi_exclusions( + parental_earned_income, + parental_unearned_income, + parameters, + period, + monthly=True, + ) + + net_parental_deemed_income = max_( + 0, net_parental_deemed_income - parental_allocations + ) + + count_eligible_children = tax_unit.sum(eligible_child) + # avoid array divide-by-zero warnings by not using where() function + # see the following GitHub issue for more details: + # https://github.com/PolicyEngine/policyengine-us/issues/2494 + income = np.zeros_like(count_eligible_children, dtype=float) + mask = count_eligible_children > 0 + income[mask] = net_parental_deemed_income[mask] / count_eligible_children[mask] + return where(eligible_child, income, 0) 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..b3ea6dd2ebc 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 @@ -19,11 +19,18 @@ The implementation follows these steps according to the regulations: - Deduct allocations for ineligible children from the ineligible spouse's income - Deductions are made from unearned income first, then earned income + - An ineligible child is the claimant's or the spouse's child who lives in the claimant's household and is not eligible for SSI (§416.1160(d)), and who is a child under §416.1856: under 18, or a student under the deeming age limit (under 21 before June 16, 2008, under 22 afterward); unmarried even when a legal spouse lives elsewhere; not the head of a household + - `ssi_spousal_deeming_child_allocations` sums the allocations. Parent ids (`parent_1_id`, `parent_2_id`) decide whose child someone is. Without ids, the co-resident members of the claimant's or the spouse's tax unit stand in, as the parent-link contract in `household/demographic/person/_parent_links.py` allows; that proxy cannot tell a grandchild from a child + - No allocation is made for a child receiving public income-maintenance payments (`ssi_receives_public_income_maintenance`) + - Student earnings use the monthly student exclusion until its annual cap is exhausted. Each month retains its allocation through spouse-income deductions, the deeming threshold, income exclusions, floors and benefit caps; annual outputs sum the completed monthly calculations. Annual income inputs are spread evenly over the year + - Existing income and allocation variables retain their annual input and output basis, including when supplied in a monthly situation. Their `*_monthly` counterparts carry each month's budget through the calculation chain and honor supplied annual inputs. Annual eligibility screens read the annual variables directly, avoiding rounding from dividing supplied annual income and summing it again + - `is_ssi_spousal_deeming_applies` also retains its annual input basis; its calculated annual output remains the December status. Benefit calculations use `is_ssi_spousal_deeming_applies_monthly` for the current month's threshold + - Parent deeming and its Medicaid counterpart also restrict allocations to relevant co-resident children. SSI recipient division uses SSI immigration status; Medicaid uses its own immigration eligibility and excludes its recipients from the allocation pool 3. **Compare remaining income to FBR differential** (§416.1163(d)) - Compare the remaining income to the difference between couple and individual FBR - - If less than the differential, no income is deemed + - If no more than the differential, no income is deemed - If more than the differential, continue with deeming calculations 4. **Calculate combined countable income** (§416.1163(d)(2)) @@ -58,7 +65,7 @@ These tests can be found in `ssi_income_deemed_from_ineligible_spouse.yaml` and ### Integration Tests -Integration tests verify the full SSI benefit calculation pipeline and can be found in the `integration.yaml` file. These tests check: +Integration tests verify the full SSI benefit calculation pipeline and can be found in the `integration_tests.yaml` file. These tests check: - The entire SSI benefit calculation for households with ineligible spouses - Interactions between spousal deeming and other components of the SSI program @@ -66,3 +73,5 @@ Integration tests verify the full SSI benefit calculation pipeline and can be fo - Households with various income combinations Integration tests are particularly important for SSI spousal deeming as they validate the correct application of the regulation's multi-step process in a full benefit calculation context. + +Monthly policy assertions and consumers must use the `*_monthly` variables. The existing annual public variables aggregate those monthly budgets; Core divides an annual variable by 12 when it is requested for a month, so that request is an annual average rather than the month’s budget. diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies.py index 9c7348fb608..6e2e484a4ae 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies.py @@ -22,20 +22,5 @@ class is_ssi_spousal_deeming_applies(Variable): """ def formula(person, period, parameters): - # Get spouse's deemed income (after child allocations) - spouse_deemed_income = add( - person, - period, - [ - "ssi_earned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_spouse", - ], - ) - - # Compare to FBR differential (couple rate - individual rate) - p = parameters(period).gov.ssa.ssi.amount - diff = (p.couple - p.individual) * MONTHS_IN_YEAR - - # Deeming applies when spouse's deemed income exceeds the differential - # Note: regulation says "not more than" means ≤, so we use > for deeming - return spouse_deemed_income > diff + # Preserve the annual status input and the monthly flag's year-end output. + return person("is_ssi_spousal_deeming_applies_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies_monthly.py new file mode 100644 index 00000000000..f6482c10d30 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/is_ssi_spousal_deeming_applies_monthly.py @@ -0,0 +1,49 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * + + +class is_ssi_spousal_deeming_applies_monthly(Variable): + value_type = bool + entity = Person + label = "SSI spousal deeming applies" + definition_period = MONTH + reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" + documentation = """ + Returns True when spousal deeming applies according to 20 CFR §416.1163. + + Spousal deeming applies when: + 1. Person is an eligible individual (not part of an eligible couple) + 2. Spouse is ineligible (not aged/blind/disabled) + 3. Spouse's leftover income (after child allocations) exceeds the difference + between couple FBR and individual FBR + + When deeming applies, the benefit calculation uses the couple FBR instead + of the individual FBR, recognizing that two people have higher expenses. + """ + + def formula(person, period, parameters): + supplied = annual_input(person, period, "is_ssi_spousal_deeming_applies") + if supplied is not None: + return supplied + + # Get spouse's deemed income (after child allocations) + spouse_deemed_income = add( + person, + period, + [ + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", + ], + ) + + # Compare to FBR differential (couple rate - individual rate) + p = parameters(period).gov.ssa.ssi.amount + diff = p.couple - p.individual + + # Deeming applies when spouse's deemed income exceeds the differential + # Note: regulation says "not more than" means ≤, so we use > for deeming + return person("is_ssi_eligible_individual", period) & ( + spouse_deemed_income > diff + ) 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..7375d4eabcd 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 @@ -10,28 +10,5 @@ class ssi_earned_income_deemed_from_ineligible_spouse(Variable): reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" 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) - 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 - ) - earned_income = person("ssi_earned_income", period) - ineligible_spousal_earned_income = ( - marital_unit.sum(ineligible_spouse * earned_income) - - ineligible_spouse * earned_income - ) - - # Next, (b) determine and subtract allocations for ineligible children. - child_allocations = add( - person.tax_unit, period, ["ssi_ineligible_child_allocation"] - ) * person("is_ssi_aged_blind_disabled", period) - # Child allocations are deducted from unearned income first - ensure we only deduct - # the remaining allocations from earned income. - remaining_child_allocations = max_( - 0, child_allocations - ineligible_spousal_unearned_income - ) - return max_(0, ineligible_spousal_earned_income - remaining_child_allocations) + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person("ssi_earned_income_deemed_from_ineligible_spouse_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse_monthly.py new file mode 100644 index 00000000000..77a6b047ed0 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_earned_income_deemed_from_ineligible_spouse_monthly.py @@ -0,0 +1,44 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * + + +class ssi_earned_income_deemed_from_ineligible_spouse_monthly(Variable): + value_type = float + entity = Person + label = "SSI earned income (deemed from ineligible spouse)" + unit = USD + definition_period = MONTH + reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" + + def formula(person, period, parameters): + supplied = annual_input( + person, period, "ssi_earned_income_deemed_from_ineligible_spouse" + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + # First, determine unearned income from the ineligible spouse. + # This is (a) in the law. + ineligible_spouse = person("is_ssi_ineligible_spouse", period) + 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 + ) + earned_income = person("ssi_earned_income", period) + ineligible_spousal_earned_income = ( + marital_unit.sum(ineligible_spouse * earned_income) + - ineligible_spouse * earned_income + ) + + # Next, (b) determine and subtract allocations for ineligible children. + child_allocations = person("ssi_spousal_deeming_child_allocations", period) + # Child allocations are deducted from unearned income first - ensure we only deduct + # the remaining allocations from earned income. + remaining_child_allocations = max_( + 0, child_allocations - ineligible_spousal_unearned_income + ) + return max_(0, ineligible_spousal_earned_income - remaining_child_allocations) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.py index 8a885f8d66f..f7b4dd1e733 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.py @@ -1,9 +1,4 @@ -# policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.py - from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, -) class ssi_income_deemed_from_ineligible_spouse(Variable): @@ -16,43 +11,11 @@ class ssi_income_deemed_from_ineligible_spouse(Variable): Spousal deeming: 1) If leftover spouse income <= (coupleFBR - indivFBR), then 0 is deemed. 2) Otherwise, spouse's deemed = (couple combined countable) - (individual alone countable). - This yields 816 for 1986 Example 3 and 12000 for the 2025 test. + Each month is calculated before annual amounts are summed. """ defined_for = "is_ssi_eligible_individual" unit = USD def formula(person, period, parameters): - # 1. Check if spousal deeming applies (avoids duplicating threshold logic) - deeming_applies = person("is_ssi_spousal_deeming_applies", period) - - # 2. If deeming applies, calculate deemed amount using "difference" approach: - # (couple combined countable) - (individual alone countable) - - # Get spouse's leftover income (post-child allocations) - spouse_earned = person( - "ssi_earned_income_deemed_from_ineligible_spouse", period - ) - spouse_unearned = person( - "ssi_unearned_income_deemed_from_ineligible_spouse", period - ) - - # Get individual's own income - individual_earned = person("ssi_earned_income", period) - individual_unearned = person("ssi_unearned_income", period) - - # Calculate individual's countable income (alone) - alone_countable = _apply_ssi_exclusions( - individual_earned, individual_unearned, parameters, period - ) - - # Calculate couple's combined countable income - couple_countable = _apply_ssi_exclusions( - individual_earned + spouse_earned, - individual_unearned + spouse_unearned, - parameters, - period, - ) - - # Deemed amount is the difference (only when deeming applies) - deemed_amount = max_(0, couple_countable - alone_countable) - return deeming_applies * deemed_amount + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person("ssi_income_deemed_from_ineligible_spouse_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse_monthly.py new file mode 100644 index 00000000000..83456df096d --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse_monthly.py @@ -0,0 +1,68 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +# policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_income_deemed_from_ineligible_spouse.py + +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + + +class ssi_income_deemed_from_ineligible_spouse_monthly(Variable): + value_type = float + entity = Person + label = "SSI income (deemed from ineligible spouse)" + definition_period = MONTH + reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" + documentation = """ + Spousal deeming: + 1) If leftover spouse income <= (coupleFBR - indivFBR), then 0 is deemed. + 2) Otherwise, spouse's deemed = (couple combined countable) - (individual alone countable). + Each month is calculated before annual amounts are summed. + """ + unit = USD + + def formula(person, period, parameters): + supplied = annual_input( + person, period, "ssi_income_deemed_from_ineligible_spouse" + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + # 1. Check if spousal deeming applies (avoids duplicating threshold logic) + deeming_applies = person("is_ssi_spousal_deeming_applies_monthly", period) + + # 2. If deeming applies, calculate deemed amount using "difference" approach: + # (couple combined countable) - (individual alone countable) + + # Get spouse's leftover income (post-child allocations) + spouse_earned = person( + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", period + ) + spouse_unearned = person( + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", period + ) + + # Get individual's own income + individual_earned = person("ssi_earned_income", period) + individual_unearned = person("ssi_unearned_income", period) + + # Calculate individual's countable income (alone) + alone_countable = _apply_ssi_exclusions( + individual_earned, individual_unearned, parameters, period, monthly=True + ) + + # Calculate couple's combined countable income + couple_countable = _apply_ssi_exclusions( + individual_earned + spouse_earned, + individual_unearned + spouse_unearned, + parameters, + period, + monthly=True, + ) + + # Deemed amount is the difference (only when deeming applies) + deemed_amount = max_(0, couple_countable - alone_countable) + result = deeming_applies * deemed_amount + return where(person("is_ssi_eligible_individual", period), result, 0) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_spousal_deeming_child_allocations.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_spousal_deeming_child_allocations.py new file mode 100644 index 00000000000..8cdf6d16d4d --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_spousal_deeming_child_allocations.py @@ -0,0 +1,83 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_established_spouse_indices, +) +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + has_parent_ids, + household_has_parent_ids, + household_member_indices, + unlinked_parent, +) + + +class ssi_spousal_deeming_child_allocations(Variable): + value_type = float + entity = Person + label = "SSI ineligible child allocations from an ineligible spouse's income" + documentation = ( + "The sum of the ineligible child allocations deducted from the " + "income of this person's ineligible spouse before it is deemed: one " + "for each ineligible child of this person or of their spouse who " + "lives in their household. A child whose parent ids are set counts " + "when an id names this person or their co-resident spouse. A child " + "without ids counts when they belong to the tax unit of this person " + "or of their spouse; in a household with parent links, only when " + "this person or their spouse is a parent some of whose children no " + "id names. Without ids, a grandchild or other relative in that tax " + "unit cannot be told apart from a child, so supply parent ids. Enter " + "a child who is only temporarily absent, such as a child away at " + "school who is still under parental control, in the household they " + "remain a member of (416.1167, POMS SI 01310.165)." + ) + unit = USD + definition_period = MONTH + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1163#p-416.1163(b)", + "https://www.ecfr.gov/current/title-20/section-416.1160#p-416.1160(d)", + "https://secure.ssa.gov/poms.nsf/lnx/0501310115", + "https://secure.ssa.gov/poms.nsf/lnx/0501310140", + ) + + def formula(person, period, parameters): + allocation = person("ssi_ineligible_child_allocation_monthly", period) + # 416.1160(d): 'your natural child or adopted child, or the natural + # or adopted child of your spouse ... who lives in the same household + # with you'. Its other clause, the child 'of your parent or of your + # parent's spouse', serves parent-to-child deeming: POMS SI 01310.115 + # A.3 and SI 01310.140 A give spouse-to-spouse deeming the children of + # either member of the couple. Parent ids resolve among household + # members, so a child who lives elsewhere names no co-resident parent. + first, second = co_resident_parent_indices(person, period) + spouse = _ssi_established_spouse_indices(person, period, first, second) + has_spouse = spouse >= 0 + safe_spouse = np.maximum(spouse, 0) + own_index = np.arange(person.count) + # Without ids, keep the tax-unit grouping, extended to a spouse who + # files separately. In a household with links, the parent-link + # contract lets this proxy assign a child only to unlinked parents. + tax_unit = person.tax_unit.reference_entity.members_entity_id + spouse_tax_unit = np.where(has_spouse, tax_unit[safe_spouse], -1) + unlinked = unlinked_parent(person, period) + may_presume = ~household_has_parent_ids(person, period) | ( + unlinked | (has_spouse & unlinked[safe_spouse]) + ) + linked_child = has_parent_ids(person, period) + total = np.zeros(person.count) + for child in household_member_indices(person): + safe = np.maximum(child, 0) + named = (first[safe] == own_index) | (second[safe] == own_index) + named_spouse = has_spouse & ( + (first[safe] == spouse) | (second[safe] == spouse) + ) + presumed = may_presume & ( + (tax_unit[safe] == tax_unit) | (tax_unit[safe] == spouse_tax_unit) + ) + counts = ( + (child >= 0) + & (child != own_index) + & (child != spouse) + & where(linked_child[safe], named | named_spouse, presumed) + ) + total += where(counts, allocation[safe], 0) + return total 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..8134c2ed6cc 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 @@ -13,17 +13,7 @@ class ssi_unearned_income_deemed_from_ineligible_spouse(Variable): reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" 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) - unearned_income = person("ssi_unearned_income", period) - ineligible_spousal_unearned_income = ( - person.marital_unit.sum(ineligible_spouse * unearned_income) - - ineligible_spouse * unearned_income + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person( + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", period ) - - # Next, (b) determine and subtract allocations for ineligible children. - child_allocations = add( - person.tax_unit, period, ["ssi_ineligible_child_allocation"] - ) - return max_(0, ineligible_spousal_unearned_income - child_allocations) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse_monthly.py new file mode 100644 index 00000000000..87006c24179 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse/ssi_unearned_income_deemed_from_ineligible_spouse_monthly.py @@ -0,0 +1,36 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * + + +class ssi_unearned_income_deemed_from_ineligible_spouse_monthly(Variable): + value_type = float + entity = Person + label = "SSI unearned income (deemed from ineligible spouse)" + documentation = ( + "This is ignored if total income is under the SSI individual allowance." + ) + unit = USD + definition_period = MONTH + reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" + + def formula(person, period, parameters): + supplied = annual_input( + person, period, "ssi_unearned_income_deemed_from_ineligible_spouse" + ) + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + # First, determine unearned income from the ineligible spouse. + # This is (a) in the law. + ineligible_spouse = person("is_ssi_ineligible_spouse", period) + unearned_income = person("ssi_unearned_income", period) + ineligible_spousal_unearned_income = ( + person.marital_unit.sum(ineligible_spouse * unearned_income) + - ineligible_spouse * unearned_income + ) + + # Next, (b) determine and subtract allocations for ineligible children. + child_allocations = person("ssi_spousal_deeming_child_allocations", period) + return max_(0, ineligible_spousal_unearned_income - child_allocations) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.py index 36aeedef7e9..7e3b6104210 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.py @@ -6,16 +6,19 @@ class ssi_ineligible_child_allocation(Variable): entity = Person label = "SSI ineligible child allocation" unit = USD - documentation = "The amount of income that SSI deems ought to be spent on this child, and therefore is not deemed to SSI claimants." + documentation = ( + "The allocation SSI deducts for this child from an ineligible " + "spouse's or parent's income before deeming it. Which claimants it " + "applies to is decided where the allocations are summed." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1163#p-416.1163(b)", + "https://www.ecfr.gov/current/title-20/section-416.1161#p-416.1161(c)", + "https://www.ecfr.gov/current/title-20/section-416.1165#p-416.1165(b)", + "https://secure.ssa.gov/poms.nsf/lnx/0501320185", + ) def formula(person, period, parameters): - income = add(person, period, ["ssi_earned_income", "ssi_unearned_income"]) - ssi = parameters(period).gov.ssa.ssi.amount - allocation = ( - person("is_ssi_ineligible_child", period) - * (ssi.couple - ssi.individual) - * MONTHS_IN_YEAR - ) - return max_(0, allocation - income) + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person("ssi_ineligible_child_allocation_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation_monthly.py new file mode 100644 index 00000000000..79b218e219d --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation_monthly.py @@ -0,0 +1,59 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * + + +class ssi_ineligible_child_allocation_monthly(Variable): + value_type = float + entity = Person + label = "SSI ineligible child allocation" + unit = USD + documentation = ( + "The allocation SSI deducts for this child from an ineligible " + "spouse's or parent's income before deeming it. Which claimants it " + "applies to is decided where the allocations are summed." + ) + definition_period = MONTH + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1163#p-416.1163(b)", + "https://www.ecfr.gov/current/title-20/section-416.1161#p-416.1161(c)", + "https://www.ecfr.gov/current/title-20/section-416.1165#p-416.1165(b)", + "https://secure.ssa.gov/poms.nsf/lnx/0501320185", + ) + + def formula(person, period, parameters): + supplied = annual_input(person, period, "ssi_ineligible_child_allocation") + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + p = parameters(period).gov.ssa.ssi + # 416.1163(b): 'We do not allocate for ineligible children who are + # receiving public income-maintenance payments.' + allocated = person("is_ssi_ineligible_child", period) & ~person( + "ssi_receives_public_income_maintenance", period + ) + # 416.1163(b)(1): the couple rate less the individual rate, a month. + monthly_allocation = p.amount.couple - p.amount.individual + # 416.1163(b)(2) reduces it by the child's income as described in + # 416.1161(c), which excludes a student's earned income up to the + # amounts in 416.1112(c)(3), whether or not the child is blind or + # disabled (POMS SI 01320.185). No $20 or $65 exclusion applies. + student = p.income.exclusions.blind_or_disabled_working_student + is_student = person("is_full_time_student", period) & ( + person("age", period.this_year) < student.age_limit + ) + # Annual income is spread evenly over the year. Keep each month's + # allocation through downstream deductions, exclusions and benefit + # caps; annual reads sum the completed monthly budgets. + earned = max_(0, person("ssi_earned_income", period)) + unearned = max_(0, person("ssi_unearned_income", period)) + # The student exclusion is used consecutively from January until + # its annual cap is exhausted (POMS SI 00820.510). Annual income + # inputs imply the same monthly earnings in the preceding months. + monthly_exclusion = min_(student.amount, earned) + used = monthly_exclusion * (period.start.month - 1) + exclusion_left = max_(0, student.cap - used) + exclusion = where(is_student, min_(monthly_exclusion, exclusion_left), 0) + income = earned - exclusion + unearned + return allocated * max_(0, monthly_allocation - income) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance.py index 993b20fe33b..6d7e1efd0d5 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance.py @@ -13,17 +13,5 @@ class ssi_in_kind_support_and_maintenance(Variable): ) def formula(person, period, parameters): - # ISM is counted as unearned income for own-household recipients - # who receive shelter (or food, pre-10/2024) support. - # The PMV rule caps ISM at the presumed maximum value. - # If the recipient provides an actual shelter value (rebuttal), - # ISM is the lesser of the actual value and the PMV. - # Another person's household: VTR FBR adjustment, not here. - # Child in parental household: deeming replaces ISM. - # Medical treatment facility: ISM not countable ($30 rate). - pmv_applies = person("ssi_pmv_applies", period) - pmv_amount = person("ssi_pmv_amount", period) - actual_value = person("ssi_shelter_support_value", period) - has_actual = actual_value > 0 - ism_value = where(has_actual, min_(pmv_amount, actual_value), pmv_amount) - return pmv_applies * ism_value + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person("ssi_in_kind_support_and_maintenance_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance_monthly.py new file mode 100644 index 00000000000..2bd3e99aa22 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_in_kind_support_and_maintenance_monthly.py @@ -0,0 +1,36 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * + + +class ssi_in_kind_support_and_maintenance_monthly(Variable): + value_type = float + entity = Person + label = "SSI in-kind support and maintenance" + unit = USD + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/cfr/text/20/416.1130", + "https://www.law.cornell.edu/cfr/text/20/416.1140", + ) + + def formula(person, period, parameters): + supplied = annual_input(person, period, "ssi_in_kind_support_and_maintenance") + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + # ISM is counted as unearned income for own-household recipients + # who receive shelter (or food, pre-10/2024) support. + # The PMV rule caps ISM at the presumed maximum value. + # If the recipient provides an actual shelter value (rebuttal), + # ISM is the lesser of the actual value and the PMV. + # Another person's household: VTR FBR adjustment, not here. + # Child in parental household: deeming replaces ISM. + # Medical treatment facility: ISM not countable ($30 rate). + pmv_applies = person("ssi_pmv_applies", period) + pmv_amount = person("ssi_pmv_amount_monthly", period) + actual_value = person("ssi_shelter_support_value", period) + has_actual = actual_value > 0 + ism_value = where(has_actual, min_(pmv_amount, actual_value), pmv_amount) + return pmv_applies * ism_value diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount.py index 18c70e2a9be..1064779b8e4 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount.py @@ -14,25 +14,5 @@ class ssi_pmv_amount(Variable): ) def formula(person, period, parameters): - p = parameters(period).gov.ssa.ssi - couple_computation = person("ssi_couple_computation_applies", period) - deeming_applies = person("is_ssi_spousal_deeming_applies", period) - - # Per POMS SI 00835.300 and SI 00835.901: - # Individual / joint couple: PMV = 1/3 × FBR + $20 - # Joint couple PMV is unit-level; the /2 in ssi_countable_income - # splits it per-person. - # - # Per POMS SI 01320.150: when spousal deeming applies, the PMV - # is halved to 1/6 × couple FBR + $10, because the deeming path - # in ssi_countable_income does NOT do a /2 split. - applicable_fbr = where( - couple_computation | deeming_applies, - p.amount.couple, - p.amount.individual, - ) - pmv_monthly = ( - applicable_fbr * p.income.ism.pmv_fbr_fraction + p.income.exclusions.general - ) - pmv_monthly = where(deeming_applies, pmv_monthly / 2, pmv_monthly) - return pmv_monthly * MONTHS_IN_YEAR + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person("ssi_pmv_amount_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount_monthly.py new file mode 100644 index 00000000000..e7082a954ae --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ism/ssi_pmv_amount_monthly.py @@ -0,0 +1,45 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * + + +class ssi_pmv_amount_monthly(Variable): + value_type = float + entity = Person + label = "SSI presumed maximum value amount" + unit = USD + definition_period = MONTH + reference = ( + "https://www.law.cornell.edu/cfr/text/20/416.1140", + "https://secure.ssa.gov/poms.nsf/lnx/0500835300", + "https://secure.ssa.gov/poms.nsf/lnx/0501320150", + ) + + def formula(person, period, parameters): + supplied = annual_input(person, period, "ssi_pmv_amount") + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + p = parameters(period).gov.ssa.ssi + couple_computation = person("ssi_couple_computation_applies", period) + deeming_applies = person("is_ssi_spousal_deeming_applies_monthly", period) + + # Per POMS SI 00835.300 and SI 00835.901: + # Individual / joint couple: PMV = 1/3 × FBR + $20 + # Joint couple PMV is unit-level; the /2 in ssi_countable_income + # splits it per-person. + # + # Per POMS SI 01320.150: when spousal deeming applies, the PMV + # is halved to 1/6 × couple FBR + $10, because the deeming path + # in ssi_countable_income does NOT do a /2 split. + applicable_fbr = where( + couple_computation | deeming_applies, + p.amount.couple, + p.amount.individual, + ) + pmv_monthly = ( + applicable_fbr * p.income.ism.pmv_fbr_fraction + p.income.exclusions.general + ) + pmv_monthly = where(deeming_applies, pmv_monthly / 2, pmv_monthly) + return pmv_monthly diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income.py index 1d268ec5b6c..c1910b94df8 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income.py @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, -) class ssi_countable_income(Variable): @@ -26,62 +23,5 @@ class ssi_countable_income(Variable): """ def formula(person, period, parameters): - # The individual's earned income, after the blind/disabled student exclusion - pre_reduction_earned_income = person("ssi_marital_earned_income", period) - blind_disabled_working_student_income = person( - "ssi_blind_or_disabled_working_student_exclusion", period - ) - earned_income = max_( - pre_reduction_earned_income - blind_disabled_working_student_income, - 0, - ) - - # The individual's unearned + any unearned deemed from a parent - # When both spouses are SSI eligible, ssi_marital_unearned_income and ssi_marital_earned_income - # will already have attributed the combined earned/unearned income to each person in the - # marital unit, so we don't need additional head/spouse checks here. - # This occurs via the both_eligible check in those variables. - unearned_income = person("ssi_marital_unearned_income", period) - parent_deemed = person( - "ssi_unearned_income_deemed_from_ineligible_parent", period - ) - - # ISM (PMV for own-household recipients receiving shelter support) is - # counted as unearned income per 20 CFR § 416.1140. It enters - # the unearned pool BEFORE exclusions so the $20 general exclusion - # naturally applies. POMS SI 00835.901 confirms that the net - # countable ISM is 1/3 FBR (PMV minus $20 general exclusion). - ism = person("ssi_in_kind_support_and_maintenance", period) - - total_unearned = unearned_income + parent_deemed + ism - - # Apply standard SSI exclusions to the individual's own income - personal_countable = _apply_ssi_exclusions( - earned_income, - total_unearned, - parameters, - period, - ) - - # Check if couple computation applies (both eligible AND not - # separated by a medical facility per 20 CFR 416.414(b)(3)) - both_eligible = person("ssi_couple_computation_applies", period) - - # Add spousal deemed income (only relevant when one spouse is ineligible) - spousal_deemed = person("ssi_income_deemed_from_ineligible_spouse", period) - - # Check if person is eligible (either as individual OR as eligible spouse) - is_eligible_individual = person("is_ssi_eligible_individual", period) - is_eligible_spouse = person("is_ssi_eligible_spouse", period) - is_eligible = is_eligible_individual | is_eligible_spouse - - # Calculate final countable income - return where( - ~is_eligible, - 0, - where( - both_eligible, - personal_countable / 2, - personal_countable + spousal_deemed, - ), - ) + # Sum completed monthly budgets; supplied amounts retain their annual basis. + return person("ssi_countable_income_monthly", period) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income_monthly.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income_monthly.py new file mode 100644 index 00000000000..6bc694307c1 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_countable_income_monthly.py @@ -0,0 +1,97 @@ +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._annual_input import ( + annual_input, +) +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + + +class ssi_countable_income_monthly(Variable): + value_type = float + entity = Person + label = "SSI countable income" + unit = USD + definition_period = MONTH + reference = "https://www.law.cornell.edu/uscode/text/42/1382a#b" + documentation = """ + Calculates total countable income for SSI: + - Earned (after ignoring blind/disabled student exclusion) + - Unearned + - Parental deemed if child + - Spousal deemed if married to an ineligible spouse + - In-kind support and maintenance (ISM) via the PMV rule for Status A + - Applies standard SSI exclusions ($20 general, $65 earned, 50% earned). + + ISM is added to unearned income BEFORE exclusions per 20 CFR § 416.1140. + The PMV formula includes $20 so that after the general exclusion applies, + the net countable ISM equals 1/3 FBR (matching POMS SI 00835.901 values). + """ + + def formula(person, period, parameters): + supplied = annual_input(person, period, "ssi_countable_income") + if supplied is not None: + return supplied / MONTHS_IN_YEAR + + # The individual's earned income, after the blind/disabled student exclusion + pre_reduction_earned_income = person("ssi_marital_earned_income", period) + blind_disabled_working_student_income = person( + "ssi_blind_or_disabled_working_student_exclusion", period + ) + earned_income = max_( + pre_reduction_earned_income - blind_disabled_working_student_income, + 0, + ) + + # The individual's unearned + any unearned deemed from a parent + # When both spouses are SSI eligible, ssi_marital_unearned_income and ssi_marital_earned_income + # will already have attributed the combined earned/unearned income to each person in the + # marital unit, so we don't need additional head/spouse checks here. + # This occurs via the both_eligible check in those variables. + unearned_income = person("ssi_marital_unearned_income", period) + parent_deemed = person( + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", period + ) + + # ISM (PMV for own-household recipients receiving shelter support) is + # counted as unearned income per 20 CFR § 416.1140. It enters + # the unearned pool BEFORE exclusions so the $20 general exclusion + # naturally applies. POMS SI 00835.901 confirms that the net + # countable ISM is 1/3 FBR (PMV minus $20 general exclusion). + ism = person("ssi_in_kind_support_and_maintenance_monthly", period) + + total_unearned = unearned_income + parent_deemed + ism + + # Apply standard SSI exclusions to the individual's own income + personal_countable = _apply_ssi_exclusions( + earned_income, + total_unearned, + parameters, + period, + monthly=True, + ) + + # Check if couple computation applies (both eligible AND not + # separated by a medical facility per 20 CFR 416.414(b)(3)) + both_eligible = person("ssi_couple_computation_applies", period) + + # Add spousal deemed income (only relevant when one spouse is ineligible) + spousal_deemed = person( + "ssi_income_deemed_from_ineligible_spouse_monthly", period + ) + + # Check if person is eligible (either as individual OR as eligible spouse) + is_eligible_individual = person("is_ssi_eligible_individual", period) + is_eligible_spouse = person("is_ssi_eligible_spouse", period) + is_eligible = is_eligible_individual | is_eligible_spouse + + # Calculate final countable income + return where( + ~is_eligible, + 0, + where( + both_eligible, + personal_countable / 2, + personal_countable + spousal_deemed, + ), + ) 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..cb5f30895e3 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 @@ -1,6 +1,9 @@ from policyengine_us.model_api import * from policyengine_us.variables.household.demographic.person._parent_links import ( + _parent_indices, co_resident_parent_indices, + group_member_indices, + tax_unit_parent_indices, ) @@ -8,8 +11,8 @@ def _ssi_established_spouse_indices(person, period, first, second): """Rows of each person's co-resident spouse, or -1. SSI resource deeming uses this one test for a claimant's spouse - (416.1202(a)), a parent's spouse (416.1202(b)) and whether a child is - married (416.1856). A marital unit holds an unmarried person or a married, + (416.1202(a)) and a parent's spouse (416.1202(b)). A marital unit holds + an unmarried person or a married, cohabiting couple, so the members of a two-person marital unit who live in the same household are spouses. So are the head and spouse of one tax unit who share a marital unit, which covers situations that omit marital @@ -64,6 +67,64 @@ def _ssi_established_spouse_indices(person, period, first, second): def _ssi_spouse_index(person, period): - """The row of each person's co-resident spouse, or -1.""" + """The row of each person's co-resident spouse, or -1. + + New York's SSP minor test also uses this co-resident spouse proxy. + SSI's 416.1856 marriage classification uses _ssi_is_married instead, + including legal spouses living apart. + """ first, second = co_resident_parent_indices(person, period) return _ssi_established_spouse_indices(person, period, first, second) + + +def _ssi_is_married(person, period): + """Marriage classification without a co-residence requirement. + + Legal spouses can live apart (416.1806(a)(1)). A two-person marital + unit establishes marriage, as does a tax unit's head/spouse pair, even + when they have separate marital units. Apply the spouse lookup's + parent-link and default-marital-unit safeguards to both sources; + ambiguous candidates do not establish marriage. + """ + period = period.this_year + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + dependent = person("is_tax_unit_dependent", period) + joint = head_or_spouse & person.tax_unit("tax_unit_married", period) + tax_unit = person.tax_unit.reference_entity.members_entity_id + couple = person.marital_unit.nb_persons() == 2 + possible_default = person.marital_unit.count == 1 + own_index = np.arange(person.count) + married = np.zeros(person.count, dtype=bool) + + for group, marital_couple in ( + (person.marital_unit, True), + (person.tax_unit, False), + ): + first, second = ( + _parent_indices(person, period, group) + if marital_couple + else tax_unit_parent_indices(person, period) + ) + candidates = np.zeros(person.count, dtype=int) + for member in group_member_indices(group): + safe = np.maximum(member, 0) + same_tax_unit = tax_unit[safe] == tax_unit + names_applicant = (first[safe] == own_index) | (second[safe] == own_index) + named_by_applicant = (first == member) | (second == member) + claims = possible_default & same_tax_unit & (dependent != dependent[safe]) + established = ( + couple + if marital_couple + else joint & head_or_spouse[safe] & same_tax_unit + ) + shown = ( + (member >= 0) + & (member != own_index) + & established + & ~claims + & ~names_applicant + & ~named_by_applicant + ) + candidates += shown + married |= candidates == 1 + return married diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py index 2c0a1f658dd..a1c99d3c6ae 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py @@ -133,9 +133,9 @@ def formula(person, period, parameters): # the payment is ineligible 'for any reason': they are still deemed a # share, but do not count in the division. Neither variable reads the # resource test. - income_eligible = person( - "ssi_countable_income", period.this_year - ) / MONTHS_IN_YEAR < person("ssi_amount_if_eligible", period) + income_eligible = person("ssi_countable_income_monthly", period) < person( + "ssi_amount_if_eligible", period + ) deemed = np.zeros(person.count) # SI 01330.200 B: 'equally divide' among eligible children; when a # child is ineligible 'divide ... among the remaining eligible children'. diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.py index d178b8b63d8..ffd3d36668c 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_child.py @@ -1,13 +1,53 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_is_married, +) class is_ssi_ineligible_child(Variable): value_type = bool entity = Person label = "Is an SSI-ineligible child" - definition_period = YEAR + documentation = ( + "A child under the SSI ineligible-child deeming definition who is not " + "eligible for SSI. " + "Whose child the person is, and whether they live with the " + "claimant, are tested where the allocation is summed." + ) + definition_period = MONTH + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1160#p-416.1160(d)", + "https://www.ecfr.gov/current/title-20/section-416.1856", + "https://www.law.cornell.edu/uscode/text/42/1382c#c", + "https://secure.ssa.gov/poms.nsf/lnx/0501310115", + ) def formula(person, period, parameters): - abd = person("is_ssi_aged_blind_disabled", period) - child = person("is_child", period) - return ~abd & child + p = parameters(period).gov.ssa.ssi.eligibility.child + # SI 01310.115 A.3: the ineligible-student deeming limit was under + # 21 before June 16, 2008 and under 22 afterward. The general SSI + # child definition remains distinct. Monthly status follows the + # parameters at the start of the month; no mid-month proration is + # modeled. Full-time attendance remains the model's proxy for + # regularly attending school. + annual_period = period.this_year + student = person("is_full_time_student", annual_period) & ( + person("age", annual_period) < p.deeming_student_age_limit + ) + # Marriage is distinct from the co-resident spouse lookup used for + # deeming: legal spouses can live apart. + child = ( + (person("is_child", annual_period) | student) + & ~_ssi_is_married(person, annual_period) + & ~person("is_household_head", annual_period) + ) + # 'Is not eligible for SSI benefits': aged, blind or disabled people + # who meet the citizenship or qualified-noncitizen condition are + # treated as eligible. The income and resource tests are not read, + # because they depend on deemed income and so on this allocation. + immigration_status = person("immigration_status", annual_period) + is_citizen = immigration_status == immigration_status.possible_values.CITIZEN + eligible = person("is_ssi_aged_blind_disabled", annual_period) & ( + is_citizen | person("is_ssi_qualified_noncitizen", annual_period) + ) + return child & ~eligible diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_receives_public_income_maintenance.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_receives_public_income_maintenance.py new file mode 100644 index 00000000000..d5bb891478a --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_receives_public_income_maintenance.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class ssi_receives_public_income_maintenance(Variable): + value_type = bool + entity = Person + label = "Receives public income-maintenance payments for SSI purposes" + documentation = ( + "Whether this person receives one of the public income-maintenance " + "payments listed in 20 CFR 416.1142(a), such as TANF, need-based " + "State or local assistance, or, from September 30, 2024, SNAP. SSI " + "makes no ineligible-child allocation for a child who receives them " + "(416.1163(b)). A child in a TANF grant held down by a family cap " + "is not treated as receiving TANF (POMS SI 01320.410). This input " + "is not derived from modeled benefits: SNAP counts SSI as income, " + "so deriving it from modeled SNAP would make SSI depend on itself." + ) + definition_period = YEAR + default_value = False + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1142#p-416.1142(a)", + "https://www.ecfr.gov/current/title-20/section-416.1163#p-416.1163(b)", + "https://secure.ssa.gov/poms.nsf/lnx/0501320141", + "https://secure.ssa.gov/poms.nsf/lnx/0501320410", + ) 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 bfe5cdf394a..8b20920e981 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 @@ -26,7 +26,7 @@ def formula(person, period, parameters): # SSI-eligible. ssi_claim_is_joint remains true for state SSPs. couple_computation = person("ssi_couple_computation_applies", period) - deeming_applies = person("is_ssi_spousal_deeming_applies", period.this_year) + deeming_applies = person("is_ssi_spousal_deeming_applies_monthly", period) individual_or_deeming_amount = where( deeming_applies, 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..fc6202c5f31 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 @@ -28,7 +28,7 @@ def formula(person, period, parameters): # - 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) + deeming_applies = person("is_ssi_spousal_deeming_applies_monthly", period) p = parameters(period).gov.ssa.ssi.amount capped_benefit = min_(benefit, p.individual) diff --git a/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py b/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py index 04b751a49b7..52fbea2ce8d 100644 --- a/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py +++ b/policyengine_us/variables/gov/ssa/ssi/uncapped_ssi.py @@ -12,5 +12,5 @@ class uncapped_ssi(Variable): def formula(person, period, parameters): amount = person("ssi_amount_if_eligible", period) - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) return amount - countable_income diff --git a/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement.py b/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement.py index cb5e88fe1df..c83bb0e5af3 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement.py +++ b/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement.py @@ -13,5 +13,5 @@ class ca_state_supplement(Variable): def formula(spm_unit, period, parameters): payment_standard = spm_unit("ca_state_supplement_payment_standard", period) ssi = add(spm_unit, period, ["ssi"]) - countable_income = add(spm_unit, period, ["ssi_countable_income"]) + countable_income = add(spm_unit, period, ["ssi_countable_income_monthly"]) return max_(0, payment_standard - ssi - countable_income) diff --git a/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/_ca_wdp_annual_spousal_deeming.py b/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/_ca_wdp_annual_spousal_deeming.py new file mode 100644 index 00000000000..e786377aaad --- /dev/null +++ b/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/_ca_wdp_annual_spousal_deeming.py @@ -0,0 +1,26 @@ +"""Retain the California WDP screens' existing annual income approximation.""" + +from policyengine_us.model_api import * + + +def ca_wdp_annual_spousal_deeming_applies(person, period, parameters): + """Apply the SSI differential to the screens' annual income budget. + + These first-pass WDP screens use annual income. The SSI deeming flag + itself is monthly, and an annual read of that stock gives December's + result. Compare annual aggregate spouse income with the annualized FBR + differential here to preserve the screens' existing approximation; + federal SSI continues to apply the threshold separately each month. + """ + spouse_income = add( + person, + period, + [ + "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_unearned_income_deemed_from_ineligible_spouse", + ], + ) + p = parameters(period).gov.ssa.ssi.amount + return person("is_ssi_eligible_individual", period) & ( + spouse_income > (p.couple - p.individual) * MONTHS_IN_YEAR + ) diff --git a/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_income_eligible.py b/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_income_eligible.py index bbf6698e347..75d9f038670 100644 --- a/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_income_eligible.py +++ b/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_income_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ca.chhs.wdp.eligibility._ca_wdp_annual_spousal_deeming import ( + ca_wdp_annual_spousal_deeming_applies, +) class ca_wdp_income_eligible(Variable): @@ -20,7 +23,9 @@ def formula(person, period, parameters): p = parameters(period).gov.states.ca.chhs.wdp.eligibility.income personal_income = person("ca_wdp_countable_income", period) spouse_income = person.marital_unit.sum(personal_income) - personal_income - spousal_deeming_applies = person("is_ssi_spousal_deeming_applies", period) + spousal_deeming_applies = ca_wdp_annual_spousal_deeming_applies( + person, period, parameters + ) countable_income = personal_income + where( spousal_deeming_applies, spouse_income, diff --git a/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.py b/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.py index d7fb59b6bf2..7ea1892ebe1 100644 --- a/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.py +++ b/policyengine_us/variables/gov/states/ca/chhs/wdp/eligibility/ca_wdp_ssi_ssp_income_eligible.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.states.ca.chhs.wdp.eligibility._ca_wdp_annual_spousal_deeming import ( + ca_wdp_annual_spousal_deeming_applies, +) class ca_wdp_ssi_ssp_income_eligible(Variable): @@ -36,9 +39,9 @@ def formula(person, period, parameters): p = parameters( period.first_month ).gov.states.ca.cdss.state_supplement.payment_standard - uses_couple_standard = person("ssi_claim_is_joint", period) | person( - "is_ssi_spousal_deeming_applies", period - ) + uses_couple_standard = person( + "ssi_claim_is_joint", period + ) | ca_wdp_annual_spousal_deeming_applies(person, period, parameters) monthly_standard = where( uses_couple_standard, p.aged_or_disabled.amount.married, diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py index fd0e5adaf85..ebe7625512f 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py @@ -21,7 +21,7 @@ def formula(person, period, parameters): # couple, which SSI splits equally between the spouses. ISM through # SSA's one-third reduction, and Colorado's own ISM for a client not # on SSI (3.520.79(C)), are not modeled. - ism = person("ssi_in_kind_support_and_maintenance", period) + ism = person("ssi_in_kind_support_and_maintenance_monthly", period) couple_computation = person("ssi_couple_computation_applies", period) ssi = ssi_paid(person("ssi", period)) receives_ssi = (ssi > 0) | person("receives_ssi", period) diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py index 7c715bc5a79..41e14700aba 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py @@ -26,7 +26,7 @@ def formula(person, period, parameters): # A joint couple's in-kind support and maintenance is one presumed # maximum value for the couple (ssi_pmv_amount), which SSI splits # equally between the spouses. - ism_share = person("ssi_in_kind_support_and_maintenance", period) / 2 + ism_share = person("ssi_in_kind_support_and_maintenance_monthly", period) / 2 unearned = person("ssi_unearned_income", period) + ism_share ssi = person("ssi", period) # 3.549(A)(2)(c): $20 is deducted from unearned income other than SSI, @@ -44,5 +44,5 @@ def formula(person, period, parameters): return where( couple_computation, own_countable, - person("ssi_countable_income", period), + person("ssi_countable_income_monthly", period), ) diff --git a/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_countable_income.py b/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_countable_income.py index 44c69bb62e6..3ba941bc3af 100644 --- a/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_countable_income.py +++ b/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_countable_income.py @@ -23,7 +23,7 @@ def formula(person, period, parameters): [ "ssi_unearned_income", "ssi", - "ssi_unearned_income_deemed_from_ineligible_spouse", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", ], ) unearned_disregard = person("ct_ssp_unearned_income_disregard", period) @@ -34,7 +34,7 @@ def formula(person, period, parameters): period, [ "ssi_earned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", ], ) earned_disregard = person("ct_ssp_earned_income_disregard", period) diff --git a/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_earned_income_disregard.py b/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_earned_income_disregard.py index ca004a4ce77..cde939e3af9 100644 --- a/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_earned_income_disregard.py +++ b/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_earned_income_disregard.py @@ -20,7 +20,7 @@ def formula(person, period, parameters): period, [ "ssi_earned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", ], ) is_blind = person("is_blind", period.this_year) diff --git a/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_gross_income.py b/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_gross_income.py index e8e2a0cb64a..037827d747b 100644 --- a/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_gross_income.py +++ b/policyengine_us/variables/gov/states/ct/dss/ssp/income/ct_ssp_gross_income.py @@ -23,7 +23,7 @@ def formula(person, period, parameters): "ssi_earned_income", "ssi_unearned_income", "ssi", - "ssi_earned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_spouse", + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", ], ) diff --git a/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss.py b/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss.py index 08d2f86d495..799f263fc5b 100644 --- a/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss.py +++ b/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss.py @@ -53,7 +53,7 @@ def formula(person, period, parameters): provider_rate = person("fl_oss_provider_rate", period) # total_needs = recognized cost of care + personal needs allowance. total_needs = provider_rate + p.pna - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) max_oss = person("fl_oss_max_oss", period) # Deficit (2640.0131.04 Step 2), floored at $0 and capped by the # Appendix A-12 Maximum OSS Payment. diff --git a/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_eligible.py b/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_eligible.py index b535187e546..3b7b60286e7 100644 --- a/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_eligible.py +++ b/policyengine_us/variables/gov/states/fl/dcf/oss/fl_oss_eligible.py @@ -66,7 +66,7 @@ def formula(person, period, parameters): receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) # FAC 65A-2.033(2): income within OSS income standard (Group 2). income_standard = person("fl_oss_income_standard", period) - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) income_within_standard = countable_income <= income_standard return ( categorically_eligible diff --git a/policyengine_us/variables/gov/states/id/dhw/aabd/id_aabd.py b/policyengine_us/variables/gov/states/id/dhw/aabd/id_aabd.py index 822990e39b4..3f4effd5219 100644 --- a/policyengine_us/variables/gov/states/id/dhw/aabd/id_aabd.py +++ b/policyengine_us/variables/gov/states/id/dhw/aabd/id_aabd.py @@ -32,6 +32,6 @@ def formula(person, period, parameters): # AABD cash = basic allowance - countable income, capped at max # (Section 514). Idaho mirrors SSI disregards (Sections 540-546). - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) financial_need = max_(0, per_person_allowance - countable_income) return min_(financial_need, per_person_max) diff --git a/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_rcap.py b/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_rcap.py index 72773a67fdc..7cd4535f360 100644 --- a/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_rcap.py +++ b/policyengine_us/variables/gov/states/in/fssa/ssp/in_ssp_rcap.py @@ -29,5 +29,5 @@ def formula(person, period, parameters): average_days_per_month = 365 / MONTHS_IN_YEAR state_standard = per_diem * average_days_per_month ssi_monthly = person("ssi", period) - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) return max_(state_standard - ssi_monthly - countable_income, 0) 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..7bcb5d3a24d 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 @@ -24,7 +24,7 @@ def formula(person, period, parameters): both_eligible = person.marital_unit.sum(eligible) == 2 is_couple = joint_claim & both_eligible ssi_monthly = person("ssi", period) - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) max_individual = p.sapn.amount.individual max_couple = p.sapn.amount.couple pna = p.personal_needs_allowance diff --git a/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_countable_income.py b/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_countable_income.py index ed143bb9dfd..d5229b2f855 100644 --- a/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_countable_income.py +++ b/policyengine_us/variables/gov/states/ky/dcbs/ssp/ky_ssp_countable_income.py @@ -28,15 +28,19 @@ def formula(person, period, parameters): ) unearned = ( person("ssi_marital_unearned_income", period) - + person("ssi_unearned_income_deemed_from_ineligible_parent", period) - + person("ssi_in_kind_support_and_maintenance", period) + + person( + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", period + ) + + person("ssi_in_kind_support_and_maintenance_monthly", period) ) p = parameters(period).gov.ssa.ssi.income.exclusions countable_earned = max_(earned - p.earned, 0) * (1 - p.earned_share) personal_countable = unearned + countable_earned both_eligible = person("ssi_couple_computation_applies", period) - spousal_deemed = person("ssi_income_deemed_from_ineligible_spouse", period) + spousal_deemed = person( + "ssi_income_deemed_from_ineligible_spouse_monthly", period + ) is_eligible = person("is_ssi_eligible_individual", period.this_year) | person( "is_ssi_eligible_spouse", period.this_year ) diff --git a/policyengine_us/variables/gov/states/la/hhs/oss/la_oss.py b/policyengine_us/variables/gov/states/la/hhs/oss/la_oss.py index e7f9a8d8992..f82ea496eae 100644 --- a/policyengine_us/variables/gov/states/la/hhs/oss/la_oss.py +++ b/policyengine_us/variables/gov/states/la/hhs/oss/la_oss.py @@ -23,6 +23,6 @@ def formula(person, period, parameters): # $0.50/$0.49 round-up rule are not modeled at the moment. p = parameters(period).gov.states.la.hhs.oss ssi = person("ssi", period) - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) raw = p.personal_care_needs_allowance - ssi - countable_income return min_(max_(raw, 0), p.maximum_payment) 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..e20094802e5 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 @@ -47,8 +47,8 @@ def formula(person, period, parameters): period, [ "ssi_unearned_income", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", ], ) @@ -71,7 +71,7 @@ def formula(person, period, parameters): period.this_year, [ "ssi_earned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", ], ) annual_unearned = add( @@ -79,8 +79,8 @@ def formula(person, period, parameters): period.this_year, [ "ssi_unearned_income", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", ], ) couple_earned = person.marital_unit.sum(annual_earned) diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py index a6438e3c287..f06c60f2ce7 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py @@ -23,9 +23,9 @@ def formula(person, period, parameters): [ "ssi_earned_income", "ssi_unearned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", ], ) # MSA gross income is measured before deductions and disregards. 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..5e2d46f8d51 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 @@ -54,8 +54,8 @@ def formula(person, period, parameters): period, [ "ssi_unearned_income", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", ], ) disregard = where(is_medicaid_facility, 0, p.general) @@ -85,7 +85,7 @@ def formula(person, period, parameters): period.this_year, [ "ssi_earned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_earned_income_deemed_from_ineligible_spouse_monthly", ], ) annual_unearned = add( @@ -93,8 +93,8 @@ def formula(person, period, parameters): period.this_year, [ "ssi_unearned_income", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", + "ssi_unearned_income_deemed_from_ineligible_spouse_monthly", + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", ], ) couple_earned = person.marital_unit.sum(annual_earned) diff --git a/policyengine_us/variables/gov/states/nc/ncdhhs/sa/nc_sa_eligible.py b/policyengine_us/variables/gov/states/nc/ncdhhs/sa/nc_sa_eligible.py index 30db42526e9..d7e56816df5 100644 --- a/policyengine_us/variables/gov/states/nc/ncdhhs/sa/nc_sa_eligible.py +++ b/policyengine_us/variables/gov/states/nc/ncdhhs/sa/nc_sa_eligible.py @@ -34,7 +34,7 @@ def formula(person, period, parameters): # countable income, including deemed spousal income, so an applicant # denied SSI for spousal income qualifies (SA-5400 V). over_income_for_ssi = person("ssi_amount_if_eligible", period) <= person( - "ssi_countable_income", period + "ssi_countable_income_monthly", period ) # An SSI recipient automatically meets the categorical, financial and # resource requirements (SA-3100 II.B; SA-3200 I.A). An applicant over diff --git a/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.py b/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.py index 3df789f2c0c..3fd721a4b82 100644 --- a/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.py +++ b/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_countable_income.py @@ -61,7 +61,7 @@ def formula(person, period, parameters): # NOTE: that term uses SSI's earned income, so the parents' pre-tax # health premiums are excluded there; 652.03(f)(2) names no such exclusion. unearned = unearned + person( - "ssi_unearned_income_deemed_from_ineligible_parent", period + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", period ) unearned = unearned + spouse_value(unearned, spouse) other = max_(person("nh_ssp_other_income_deductions", period), 0) diff --git a/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp.py b/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp.py index c69e3ac2acf..4cf4ab020eb 100644 --- a/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp.py +++ b/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp.py @@ -91,7 +91,7 @@ def formula(person, period, parameters): # October 1, 2014, and the model follows the regulation: countable # income includes the deemed income (398-2.1(g)), measured against the # individual's own FBR. - deeming = person("is_ssi_spousal_deeming_applies", period.this_year) + deeming = person("is_ssi_spousal_deeming_applies_monthly", period) federal_la = person("ssi_federal_living_arrangement", period) # 20 CFR 416.1131(a): one-third of the FBR counts as income in another # person's household. @@ -100,7 +100,7 @@ def formula(person, period, parameters): 1 - fbr.one_third_reduction_rate, 1, ) - countable = person("ssi_countable_income", period) + countable = person("ssi_countable_income_monthly", period) regulatory = max_(individual - max_(countable - own_fbr, 0), 0) eligible = person("is_ssi_eligible", period) & person( "takes_up_ssi_if_eligible", period.this_year @@ -122,7 +122,7 @@ def formula(person, period, parameters): # hospitals and pay $25 in residential health care facilities. The # model follows the statute and regulation. personal_needs_eligible = (person("ssi", period) > 0) & ( - person("ssi_countable_income", period) == 0 + person("ssi_countable_income_monthly", period) == 0 ) personal_needs = select( [ diff --git a/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_is_minor.py b/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_is_minor.py index 55e7265f1b6..53db36d19d7 100644 --- a/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_is_minor.py +++ b/policyengine_us/variables/gov/states/ny/otda/ssp/ny_ssp_is_minor.py @@ -28,8 +28,9 @@ def formula(person, period, parameters): p = parameters(period).gov.states.ny.otda.ssp age = person("age", period) student = person("is_full_time_student", period) - # Marriage uses the SSI spouse test, so a child of a married couple - # is not treated as married. + # New York uses the co-resident SSI spouse lookup as its marriage + # proxy, which does not pair a parent with their child. SSI's separate + # 416.1856 marriage classification uses _ssi_is_married. married = _ssi_spouse_index(person, period) >= 0 head = person("is_household_head", period) under_age = (age < p.minor_age) | ((age < p.student_minor_age) & student) 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..522cee87bf6 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 @@ -35,7 +35,7 @@ def ok_ssp_income(person, period, parameters, *, combined): # It also uses SSI's earned income, which excludes the parents' pre-tax # health premiums that 317:35-5-42(b)(9)(A) counts as wages. unearned = unearned + person( - "ssi_unearned_income_deemed_from_ineligible_parent", period + "ssi_unearned_income_deemed_from_ineligible_parent_monthly", period ) earned = max_( earned @@ -59,7 +59,7 @@ def ok_ssp_income(person, period, parameters, *, combined): # disabled spouse who is not eligible (issue 10243), so the half equals the # deeming value (1/6 couple FBR + $10) even when 317:35-5-42(m)(4) deems # nothing and the individual value would apply. - ism = person("ssi_in_kind_support_and_maintenance", period) + ism = person("ssi_in_kind_support_and_maintenance_monthly", period) ism = where(joint, ism / 2, ism) if combined: eligible = person("ok_ssp_eligible", period) diff --git a/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp_spouse_child_allocation.py b/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp_spouse_child_allocation.py index b274e4ea099..669fed5da8d 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp_spouse_child_allocation.py +++ b/policyengine_us/variables/gov/states/ok/dhs/ssp/ok_ssp_spouse_child_allocation.py @@ -8,7 +8,12 @@ class ok_ssp_spouse_child_allocation(Variable): definition_period = MONTH unit = USD reference = "https://www.law.cornell.edu/regulations/oklahoma/OAC-340-15-1-5" - documentation = "The total allocation for the family's children who are not aged, blind or disabled, each reduced by that child's own income. Blended families and children in a TANF case are not modeled separately." + documentation = ( + "The total allocation for the family's unmarried SSI-ineligible children " + "who are not household heads, including full-time students under age 22. " + "Each allocation is reduced by the child's own income. Blended families " + "and children in a TANF case are not modeled separately." + ) def formula(person, period, parameters): p = parameters(period).gov.states.ok.dhs.ssp diff --git a/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement.py b/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement.py index ef835f77dc1..3c3dc5c3485 100644 --- a/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement.py +++ b/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement.py @@ -17,6 +17,6 @@ def formula(person, period, parameters): # Per S.C. Code Regs. 126-910: OSS = max(0, NIL - countable income) p = parameters(period).gov.states.sc.scdhhs.ssi_state_supplement nil = p.net_income_limit - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) ssi = person("ssi", period) return max_(0, nil - countable_income - ssi) diff --git a/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_eligible.py b/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_eligible.py index 56c9168bdd3..1afeb5dd21d 100644 --- a/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_eligible.py @@ -18,7 +18,7 @@ def formula(person, period, parameters): in_facility = person("is_in_residential_care_facility", period) p = parameters(period).gov.states.sc.scdhhs.ssi_state_supplement nil = p.net_income_limit - countable_income = person("ssi_countable_income", period) + countable_income = person("ssi_countable_income_monthly", period) ssi = person("ssi", period) income_eligible = (countable_income + ssi) < nil return is_aged_blind_disabled & in_facility & income_eligible diff --git a/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_pna.py b/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_pna.py index 84716e45cd3..556403e1fcd 100644 --- a/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_pna.py +++ b/policyengine_us/variables/gov/states/sc/scdhhs/ssi_state_supplement/sc_ssi_state_supplement_pna.py @@ -19,5 +19,5 @@ def formula(person, period, parameters): ).gov.states.sc.scdhhs.ssi_state_supplement.personal_needs_allowance # Per S.C. Code Regs. 126-910(G): SSI-only PNA if no other # countable income; higher PNA if other income sources exist - ssi_only = person("ssi_countable_income", period) == 0 + ssi_only = person("ssi_countable_income_monthly", period) == 0 return where(ssi_only, p.ssi_only, p.other_income) diff --git a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py index 9e8ef04d513..7961f6a4b7a 100644 --- a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py @@ -39,8 +39,8 @@ def formula(person, period, parameters): # income, held to their $30 half of the $60 couple standard (H-6000). p = parameters(period).gov.ssa.ssi own_countable_income = max_( - person("ssi_countable_income", period) - - person("ssi_income_deemed_from_ineligible_spouse", period), + person("ssi_countable_income_monthly", period) + - person("ssi_income_deemed_from_ineligible_spouse_monthly", period), 0, ) within_reduced_rate = own_countable_income <= p.amount.institutional.individual 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..3a5aff6dfd1 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 @@ -53,7 +53,9 @@ def formula(person, period, parameters): # although Chapter B section 6 makes the assistance unit the applicant # only. countable = where( - ssi > 0, person("ssi_countable_income", period) + ssi, non_ssi_countable + ssi > 0, + person("ssi_countable_income_monthly", period) + ssi, + non_ssi_countable, ) exclusions = max_(person("va_ag_allowable_income_exclusions", period), 0) return max_(countable - exclusions, 0)