diff --git a/changelog.d/hub-premium-input-contract-1010.fixed.md b/changelog.d/hub-premium-input-contract-1010.fixed.md new file mode 100644 index 00000000000..b8f2ac1c020 --- /dev/null +++ b/changelog.d/hub-premium-input-contract-1010.fixed.md @@ -0,0 +1 @@ +Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from SSI wages and North Dakota renter-refund income. Preserve Minnesota Supplemental Aid gross income for eligibility and guardian fees while applying SSI salary-reduction exclusions to countable income. diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py new file mode 100644 index 00000000000..7c33e9e9004 --- /dev/null +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -0,0 +1,232 @@ +"""Premium payment methods are disjoint inputs with consumer-specific treatment. + +Moving employee-paid premiums from non-pretax payments to pretax payroll +deductions leaves total medical spending unchanged. SNAP, HUD, +child-care deductions, Orange County General Relief and New Jersey +medical expenses count both payment methods. Michigan's wage exclusion +and non-pretax premium deduction likewise leave household resources unchanged +with other benefit amounts held fixed. +North Dakota excludes pretax premiums from income and deducts non-pretax +premiums as medical expenses, also leaving renters' refund income unchanged. + +Federal taxable and FICA wages, ACA/Medicaid MAGI, and tax deductions that +exclude pretax premiums distinguish the payment methods. SSI also excludes +qualified salary-reduction premiums from wages before its earned-income +disregard. The SGA work-earnings test retains payroll premium deductions. +Program premiums and +eligibility are held fixed here: changing MAGI can otherwise legitimately change +Marketplace, CHIP or Medicaid premiums, obscuring the spending invariant. + +Each generated batch uses fresh vectorized simulations across several states; +no calculated arrays are shared between the two situations or across examples. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import system + + +YEAR = 2026 +MONTH = "2026-01" +STATES = ("CA", "MO", "PA", "NJ", "OH", "MI", "ND") + + +@st.composite +def premium_transfers(draw): + total = draw(st.integers(0, 12_000)) + pretax = draw(st.integers(0, total)) + transfer = draw(st.integers(0, total - pretax)) + return total, pretax, transfer + + +def _situation(batch, moved): + people = {} + tax_units = {} + spm_units = {} + households = {} + marital_units = {} + for row, (total, initial_pretax, transfer) in enumerate(batch): + pretax = initial_pretax + (transfer if moved else 0) + non_pretax = total - pretax + for state, size in ((state, size) for state in STATES for size in (1, 2)): + key = f"{state}_{size}_{row}" + people[key] = { + "age": {YEAR: 40}, + "is_tax_unit_head": {YEAR: True}, + "is_ssi_aged_blind_disabled": {YEAR: True}, + "employment_income": {YEAR: 250_000}, + "pre_tax_health_insurance_premiums": {YEAR: pretax}, + # These are alternative representations used by different + # consumers, not additive premiums within the non-pretax set. + "health_insurance_premiums": {YEAR: non_pretax}, + "health_insurance_premiums_without_medicare_part_b": {YEAR: non_pretax}, + "other_health_insurance_premiums": {YEAR: non_pretax}, + "other_medical_expenses": {YEAR: 300}, + "over_the_counter_health_expenses": {YEAR: 100}, + "medicare_enrolled": {YEAR: False}, + "is_medicare_eligible": {YEAR: False}, + "medicare_part_a_premium": {YEAR: 0}, + "medicare_part_b_premium": {YEAR: 0}, + "income_adjusted_part_d_premium_surcharge": {YEAR: 0}, + "employer_sponsored_insurance_premiums": {YEAR: 0}, + "self_employed_health_insurance_ald_person": {YEAR: 0}, + "general_assistance": {YEAR: 0}, + # Hold other benefits fixed for the Michigan invariant. + "ssi": {YEAR: 0}, + "ca_oc_general_relief_countable_earned_income": {MONTH: 5_000}, + "ca_oc_general_relief_gross_unearned_income": {MONTH: 0}, + "ca_oc_general_relief_receives_other_cash_assistance": {MONTH: False}, + } + members = [key] + if size == 2: + spouse = f"{key}_spouse" + people[spouse] = { + **people[key], + "is_tax_unit_head": {YEAR: False}, + "is_tax_unit_spouse": {YEAR: True}, + } + members.append(spouse) + marital_units[key] = {"members": members} + tax_units[key] = { + "members": members, + "chip_premium": {YEAR: 0}, + "medicaid_premium": {YEAR: 0}, + "marketplace_net_premium": {YEAR: 0}, + "above_the_line_deductions": {YEAR: 0}, + "self_employed_health_insurance_ald": {YEAR: 0}, + "dependents_self_employed_health_insurance_ald": {YEAR: 0}, + "medical_expense_deduction": {YEAR: 0}, + "tax_unit_itemizes": {YEAR: False}, + "taxable_income": {YEAR: 300_000}, + "nj_agi": {YEAR: 50_000}, + "oh_employer_subsidized_health_plan_eligible": {YEAR: True}, + } + spm_units[key] = { + "members": members, + "tanf": {YEAR: 0}, + "mo_ccs_countable_income": {MONTH: 5_000}, + "pa_ccw_countable_income": {YEAR: 60_000}, + "pa_ccw_stepparent_deduction": {MONTH: 0}, + } + households[key] = { + "members": members, + "state_code": {YEAR: state}, + "in_oc": {YEAR: state == "CA"}, + } + return { + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": households, + "marital_units": marital_units, + "families": { + key: {"members": unit["members"]} for key, unit in marital_units.items() + }, + } + + +@settings( + max_examples=8, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@example(batch=[(0, 0, 0), (12_000, 0, 12_000), (12_000, 6_000, 6_000)]) +@example(batch=[(12_000, 12_000, 0), (1, 0, 1)]) +@given(batch=st.lists(premium_transfers(), min_size=1, max_size=3)) +def test_premium_transfer_respects_each_consumers_tax_treatment(batch): + # Each batch contains both single adults and two-person married units. + # Reusing the reference system avoids rebuilding the country per example. + baseline = Simulation(situation=_situation(batch, False), tax_benefit_system=system) + moved = Simulation(situation=_situation(batch, True), tax_benefit_system=system) + sizes = np.tile((1, 2), len(batch) * len(STATES)) + transfer = np.repeat([row[2] for row in batch], len(STATES) * 2) + total = np.repeat([row[0] for row in batch], len(STATES) * 2) * sizes + states = np.tile(np.repeat(STATES, 2), len(batch)) + person_transfer = np.repeat(transfer, sizes) + person_states = np.repeat(states, sizes) + unit_transfer = transfer * sizes + + np.testing.assert_allclose( + baseline.calculate("spm_unit_medical_out_of_pocket_expenses", YEAR), + total + 400 * sizes, + rtol=0, + atol=0.02, + ) + # North Dakota starts from raw market wages, excludes the pretax + # contribution once, and deducts only the remaining medical expenses. + np.testing.assert_allclose( + baseline.calculate("nd_renters_refund_income", YEAR), + (250_000 * sizes - total - 300 * sizes) * (states == "ND"), + rtol=0, + atol=0.02, + ) + + invariant_consumers = ( + ("spm_unit_health_insurance_premiums", YEAR), + ("spm_unit_medical_out_of_pocket_expenses", YEAR), + ("snap_allowable_medical_expenses", YEAR), + ("hud_medical_expenses", YEAR), + ("mo_ccs_adjusted_income", MONTH), + ("pa_ccw_medical_expenses", YEAR), + ("pa_ccw_adjusted_income", YEAR), + ("ca_oc_general_relief_countable_income_person", MONTH), + ("nj_medical_expense_deduction", YEAR), + ("mi_household_resources", YEAR), + ("nd_renters_refund_income", YEAR), + ("ssi_engaged_in_sga", YEAR), + ) + for variable, period in invariant_consumers: + np.testing.assert_allclose( + moved.calculate(variable, period), + baseline.calculate(variable, period), + rtol=0, + atol=0.02, + err_msg=variable, + ) + + tax_status_sensitive_consumers = ( + "irs_employment_income", + "payroll_tax_gross_wages", + "ssi_earned_income", + "adjusted_gross_income", + "aca_magi", + "medicaid_magi", + "medicaid_magi_person", + "medical_expense_health_insurance_premiums", + "medicaid_medically_needy_medical_expenses", + ) + for variable in tax_status_sensitive_consumers: + np.testing.assert_allclose( + baseline.calculate(variable, YEAR) - moved.calculate(variable, YEAR), + person_transfer + if system.variables[variable].entity.key == "person" + else unit_transfer, + rtol=0, + atol=0.02, + err_msg=variable, + ) + + # These categorically eligible individuals remain above the earned-income + # exclusions. SSI's 50% earned-income disregard halves the wage change. + np.testing.assert_allclose( + baseline.calculate("ssi_countable_income", YEAR) + - moved.calculate("ssi_countable_income", YEAR), + person_transfer / 2, + rtol=0, + atol=0.02, + ) + + for variable, state in ( + ("oh_insured_unreimbursed_medical_care_expense_amount", "OH"), + ("mo_qualified_health_insurance_premiums", "MO"), + ): + np.testing.assert_allclose( + baseline.calculate(variable, YEAR) - moved.calculate(variable, YEAR), + person_transfer * (person_states == state), + rtol=0, + atol=0.02, + err_msg=variable, + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml new file mode 100644 index 00000000000..4cf3c9c0b0f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml @@ -0,0 +1,44 @@ +- name: SSI-based spenddown expenses do not repeat the pretax wage exclusion. + period: 2026 + input: + health_insurance_premiums: 1_800 + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + medicare_enrolled: false + output: + medicaid_medically_needy_medical_expenses: 2_100 + +- name: SSI-based spenddown retains decomposed non-pretax Medicare premiums. + period: 2026 + input: + health_insurance_premiums_without_medicare_part_b: 600 + medicare_part_b_premium: 1_200 + medicare_enrolled: true + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + output: + medicaid_medically_needy_medical_expenses: 2_100 + +- name: Kansas spenddown does not deduct pretax premiums again from countable income. + period: 2026 + input: + age: 65 + is_in_medicaid_medically_needy_category: true + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + # Isolate spenddown from the separate SSI-recipient Medicaid pathway. + is_ssi_recipient_for_medicaid: false + employment_income: 27_020 + ssi_countable_resources: 1_000 + state_code: KS + health_insurance_premiums: 2_000 + pre_tax_health_insurance_premiums: 4_000 + medicare_enrolled: false + output: + # (27,020 - 4,000 - 12 * (20 + 65)) / 2 = 11,000. + is_ssi_aged_blind_disabled: true + ssi_earned_income: 23_020 + ssi_countable_income: 11_000 + medicaid_medically_needy_medical_expenses: 2_000 + is_ssi_recipient_for_medicaid: false + is_medically_needy_for_medicaid: false 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 985a44b41e4..fe17f4085ea 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 @@ -19,6 +19,50 @@ output: medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [6_006, 0] +- name: Optional Medicaid parental deeming inherits the SSI exclusion for qualified pretax premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 12 + is_ssi_disabled: true + sibling: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + parent: + age: 40 + is_parent: true + employment_income: 36_000 + pre_tax_health_insurance_premiums: 3_600 + tax_units: + tax_unit: + members: [recipient, sibling, parent] + # A parent and children are not a married couple. Explicit singleton + # units prevent default grouping from also applying spousal deeming. + marital_units: + recipient_unit: + members: [recipient] + sibling_unit: + members: [sibling] + parent_unit: + members: [parent] + households: + household: + members: [recipient, sibling, parent] + state_code: NC + output: + # 42 USC 1396a(m) uses SSI methodology for this optional pathway. + # POMS SI 00820.102C.2: parent wages 32,400; sibling wages 1,200. + # 20 CFR 416.1165(b), (d): sibling allocation = 4,596, so + # (32,400 - 4,596 - 20 * 12 - 65 * 12) / 2 - 967 * 12 = 1,788 deemed. + # Under (e)(1), apply child's general exclusion: 1,788 - 20 * 12 = 1,548. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [1_788, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false] + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: [0, 0, 0] + medicaid_optional_senior_or_disabled_countable_income: [1_548, 0, 0] + # Missouri MHDC deems only a natural or adoptive parent's income, never a # stepparent's (DSS MHABD Manual ยง 0805.020.15), and divides the deemed # amount equally among the disabled children applying. Parent status 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 10e06a9d943..1724495a64d 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 @@ -84,3 +84,39 @@ # 1,450 - 20 = 1,430 monthly, or 17,160 annually. medicaid_optional_senior_or_disabled_countable_income: 17_160 is_optional_senior_or_disabled_income_eligible: false + +- name: Optional Medicaid spouse deeming inherits the SSI exclusion for qualified pretax premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 70 + is_tax_unit_head: true + spouse: + age: 60 + is_tax_unit_spouse: true + employment_income: 24_000 + pre_tax_health_insurance_premiums: 2_400 + child: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + tax_units: + tax_unit: + members: [recipient, spouse, child] + marital_units: + marital_unit: + members: [recipient, spouse] + households: + household: + members: [recipient, spouse, child] + state_code: NC + output: + # 42 USC 1396a(m) uses SSI methodology for this optional pathway. + # POMS SI 00820.102C.2: spouse wages 21,600; child wages 1,200. + # 20 CFR 416.1163(b), (d): allocation = (1,450 - 967) * 12 + # - 1,200 = 4,596; remaining spouse wages = 17,004 > 5,796. + # (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] 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 6172d39ff87..1c823197a3b 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 @@ -74,3 +74,42 @@ # Person2 own income also $12,000 earned, same exclusions => $457.50/mo. # Both under 100% FPL (~$1,255/mo) so person1 still qualifies for QMB. msp_countable_income: [457.50, 457.50] + +- name: MSP excludes qualified salary-reduction health premiums before SSI income exclusions. + period: 2025-01 + absolute_error_margin: 0.01 + input: + age: 70 + employment_income: 12_000 + pre_tax_health_insurance_premiums: 2_400 + output: + # 42 USC 1396d(p)(1)(B) uses SSI income methodology. + # POMS SI 00820.102C.2: (12,000 - 2,400) / 12 = 800 SSI wages. + # 20 CFR 416.1112(c)(4), (5), (7): (800 - 20 - 65) / 2 = 357.50. + msp_countable_income: 357.50 + +- name: MSP combines both Medicare spouses' wages after excluding their pretax premiums. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 68 + employment_income: 12_000 + pre_tax_health_insurance_premiums: 2_400 + person2: + age: 66 + employment_income: 24_000 + pre_tax_health_insurance_premiums: 3_600 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + output: + # POMS SI 00820.102C.2 removes each spouse's salary reduction. + # 42 USC 1396d(p)(1)(B) and 20 CFR 416.1160(a)(1) combine the wages: + # (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] diff --git a/policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml new file mode 100644 index 00000000000..b46ec776815 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml @@ -0,0 +1,38 @@ +- name: HUD medical expenses sum disjoint premiums across household members. + period: 2026 + input: + people: + head: + health_insurance_premiums: 1_800 + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + medicare_enrolled: false + spouse: + health_insurance_premiums: 600 + pre_tax_health_insurance_premiums: 1_200 + other_medical_expenses: 200 + medicare_enrolled: false + spm_units: + spm_unit: + members: [head, spouse] + output: + hud_medical_expenses: 6_500 + +- name: HUD adds payroll premiums to decomposed non-pretax Medicare premiums. + period: 2026 + input: + health_insurance_premiums_without_medicare_part_b: 600 + medicare_part_b_premium: 1_200 + medicare_enrolled: true + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + output: + hud_medical_expenses: 4_500 + +- name: HUD medical expenses include premiums paid entirely through payroll. + period: 2026 + input: + pre_tax_health_insurance_premiums: 2_400 + medicare_enrolled: false + output: + hud_medical_expenses: 2_400 diff --git a/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml b/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml index f2ebaf09e15..329bc59713f 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml @@ -37,3 +37,22 @@ output: # Earned 1,000 x (1 - 0.2) = 800; less 100 spousal support = 700. ca_oc_general_relief_countable_income_person: 700 + +- name: Pretax and after-tax premiums reduce countable gross earned income once. + period: 2026-01 + input: + employment_income: 1_000 * 12 + health_insurance_premiums: 40 * 12 + pre_tax_health_insurance_premiums: 60 * 12 + in_oc: true + output: + ca_oc_general_relief_countable_income_person: 700 + +- name: Moving all premiums to pretax payroll leaves countable income unchanged. + period: 2026-01 + input: + employment_income: 1_000 * 12 + pre_tax_health_insurance_premiums: 100 * 12 + in_oc: true + output: + ca_oc_general_relief_countable_income_person: 700 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 745c14c4690..96f3e7376a9 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 @@ -74,3 +74,45 @@ members: [jacks_mother, jacks_father] output: ssi_countable_income: [372, 0, 0, 0, 0] # 31 * 12 + +- name: Parental deeming excludes parent's and sibling's pretax premiums before allocations. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 12 + is_ssi_disabled: true + sibling: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + parent: + age: 40 + is_parent: true + employment_income: 36_000 + pre_tax_health_insurance_premiums: 3_600 + tax_units: + tax_unit: + members: [recipient, sibling, parent] + # A parent and children are not a married couple. Explicit singleton + # units prevent default grouping from also applying spousal deeming. + marital_units: + recipient_unit: + members: [recipient] + sibling_unit: + members: [sibling] + parent_unit: + members: [parent] + output: + # POMS SI 00820.102C.2: parent wages 32,400; sibling wages 1,200. + # 20 CFR 416.1165(b): sibling allocation = (1,450 - 967) * 12 + # - 1,200 = 4,596, leaving parent wages of 27,804. + # Under (d), (27,804 - 20 * 12 - 65 * 12) / 2 - 967 * 12 = 1,788. + # Under (e)(1), child's countable unearned income = 1,788 - 20 * 12 = 1,548. + ssi_earned_income: [0, 1_200, 32_400] + ssi_ineligible_child_allocation: [0, 4_596, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [1_788, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false] + ssi_income_deemed_from_ineligible_spouse: [0, 0, 0] + ssi_countable_income: [1_548, 0, 0] 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 5f286c46b0c..64125417f99 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 @@ -57,3 +57,39 @@ ssi_countable_income: [1_776, 0] # 148 * 12 # Example 4 skipped as it relates to sponsored aliens. + +- name: Spousal deeming excludes spouse and child's qualified pretax premiums before allocations. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 70 + is_tax_unit_head: true + spouse: + age: 60 + is_tax_unit_spouse: true + employment_income: 24_000 + pre_tax_health_insurance_premiums: 2_400 + child: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + tax_units: + tax_unit: + members: [recipient, spouse, child] + marital_units: + marital_unit: + members: [recipient, spouse] + output: + # POMS SI 00820.102C.2: spouse wages 21,600; child wages 1,200. + # 20 CFR 416.1163(b): child allocation = (1,450 - 967) * 12 + # - 1,200 = 4,596; spouse's remaining wages = 21,600 - 4,596 = 17,004. + # Under (d)(2), 17,004 > 5,796 and couple exclusions apply: + # (17,004 - 20 * 12 - 65 * 12) / 2 = 7,992. + ssi_earned_income: [0, 21_600, 1_200] + ssi_ineligible_child_allocation: [0, 0, 4_596] + ssi_earned_income_deemed_from_ineligible_spouse: [17_004, 0, 0] + 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] 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 cf2a42e0de8..76164837620 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 @@ -4,3 +4,16 @@ age: 14 output: ssi_ineligible_child_allocation: 5_040 # (1,261 - 841) * 12 + +- name: Ineligible child's pretax premiums do not reduce the child allocation as wages. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + output: + # POMS SI 00820.102C.2: wages for SSI = 2,400 - 1,200 = 1,200. + # 20 CFR 416.1163(b)(1)-(2): (1,450 - 967) * 12 - 1,200 = 4,596. + ssi_earned_income: 1_200 + ssi_ineligible_child_allocation: 4_596 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml index a576a272015..2e3b42da0f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml @@ -63,3 +63,43 @@ ssi_earned_income: 20_000 output: ssi_blind_or_disabled_working_student_exclusion: 433.33 # 5,200/12 + +- name: Student earned-income exclusion applies to wages after qualified pretax premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 20 + is_disabled: true + is_ssi_disabled: true + meets_ssi_disability_criteria: true + is_full_time_student: true + employment_income: 9_600 + pre_tax_health_insurance_premiums: 2_400 + output: + # POMS SI 00820.102C.2: SSI wages = 9,600 - 2,400 = 7,200. + # 20 CFR 416.1112(c)(3): min(600, 2,350) * 12 = 7,200, + # below the 2025 annual cap of 9,460. No earned income remains. + ssi_earned_income: 7_200 + is_ssi_blind_or_disabled_working_student_exclusion_eligible: true + ssi_blind_or_disabled_working_student_exclusion: 7_200 + ssi_countable_income: 0 + +- name: Student with pretax premiums reaches the annual exclusion cap before standard exclusions. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 20 + is_disabled: true + is_ssi_disabled: true + meets_ssi_disability_criteria: true + is_full_time_student: true + employment_income: 18_000 + pre_tax_health_insurance_premiums: 2_400 + output: + # POMS SI 00820.102C.2: SSI wages = 18,000 - 2,400 = 15,600. + # 20 CFR 416.1112(c)(3)-(7): student exclusion is capped at 9,460, + # then (15,600 - 9,460 - 20 * 12 - 65 * 12) / 2 = 2,560. + ssi_earned_income: 15_600 + is_ssi_blind_or_disabled_working_student_exclusion_eligible: true + ssi_blind_or_disabled_working_student_exclusion: 9_460 + ssi_countable_income: 2_560 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml index cc9acb9d8db..a8185d00ffe 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml @@ -18,3 +18,56 @@ sstb_self_employment_income: 100_000 output: ssi_earned_income: 100_000 + +- name: After-tax health premiums remain part of SSI wages. + period: 2025 + input: + employment_income: 12_000 + health_insurance_premiums: 1_200 + output: + ssi_earned_income: 12_000 + +- name: Pretax payroll health premiums are excluded from SSI wages. + period: 2025 + input: + employment_income: 12_000 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 10_800 + +- name: Only the pretax portion of disjoint premiums is excluded from SSI wages. + period: 2025 + input: + employment_income: 12_000 + health_insurance_premiums: 600 + pre_tax_health_insurance_premiums: 600 + output: + ssi_earned_income: 11_400 + +- name: Excess pretax premiums cannot reduce SSI self-employment income. + period: 2025 + input: + employment_income: 1_000 + self_employment_income: 500 + sstb_self_employment_income: 250 + pre_tax_health_insurance_premiums: 2_000 + output: + ssi_earned_income: 750 + +- name: Traditional retirement deferrals remain SSI wages after excluding health premiums. + period: 2025 + input: + employment_income: 12_000 + traditional_401k_contributions: 2_400 + traditional_403b_contributions: 1_200 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 10_800 + +- name: Employer-paid premiums do not reduce SSI wages. + period: 2025 + input: + employment_income: 12_000 + employer_sponsored_insurance_premiums: 1_200 + output: + ssi_earned_income: 12_000 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml index 4e0cf90c2e8..ecc98e6faa3 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml @@ -21,3 +21,35 @@ is_blind: true output: ssi_engaged_in_sga: false + +- name: Pretax health premiums remain in SGA earnings across the threshold. + period: 2026 + input: + employment_income: 21_600 + pre_tax_health_insurance_premiums: 2_400 + is_blind: false + output: + # SSI payment wages are 1,600 monthly; SGA earnings remain 1,800. + ssi_earned_income: 19_200 + ssi_engaged_in_sga: true + +- name: After-tax health premiums also remain in SGA earnings. + period: 2026 + input: + employment_income: 21_600 + health_insurance_premiums: 2_400 + is_blind: false + output: + ssi_earned_income: 21_600 + ssi_engaged_in_sga: true + +- name: Excess pretax premiums cannot raise SGA earnings above gross wages. + period: 2026 + input: + employment_income: 12_000 + pre_tax_health_insurance_premiums: 24_000 + is_blind: false + output: + # Only 12,000 of excluded wages is restored, below the SGA threshold. + ssi_earned_income: 0 + ssi_engaged_in_sga: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml new file mode 100644 index 00000000000..b654cf7d663 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml @@ -0,0 +1,88 @@ +# Salary-reduction premiums are excluded from inherited SSI earned income: +# https://secure.ssa.gov/poms.nsf/lnx/0500820102 (SI 00820.102C.2). +# CT earned and unearned disregard rules: UPM 5030.10 and 5030.15; +# https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/ct.html +# Current disregard and need amounts: +# https://portal.ct.gov/dss/-/media/departments-and-agencies/dss/fact-sheets-and-issue-briefs/fact-sheets/dss-program-standards-chart-effective-010126.pdf + +- name: Connecticut SSP applies its earned disregard after the inherited pretax exclusion + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 7_200 + pre_tax_health_insurance_premiums: 1_200 + rent: 12_000 + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: CT + output: + ssi_earned_income: 500 # $600 - $100. + ssi: 786.50 # $994 - ($500 - $20 - $65)/2. + ct_ssp_gross_income: 1_286.50 # $500 SSI wages + $786.50 SSI. + ct_ssp_earned_income_disregard: 282.50 # $65 + ($500 - $65)/2. + ct_ssp_unearned_income_disregard: 562 + # ($786.50 - $562) + ($500 - $282.50) = $442. + ct_ssp_countable_income: 442 + ct_ssp_need_standard: 597.35 # $400 shelter + $197.35 personal needs. + ct_ssp_income_eligible: true + ct_ssp_eligible_person: true + ct_ssp_person: 155.35 # $597.35 - $442. + ct_ssp: 155.35 + +- name: Connecticut SSP blind earned disregard uses earnings after the pretax exclusion + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 50 + is_blind: true + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 3_600 + pre_tax_health_insurance_premiums: 1_200 + ct_ssp_lives_with_others: true + ct_ssp_has_therapeutic_diet: true + rent: 6_000 + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: CT + output: + ssi_earned_income: 200 # $300 - $100. + ssi: 936.50 # $994 - ($200 - $20 - $65)/2. + ct_ssp_gross_income: 1_136.50 # $200 SSI wages + $936.50 SSI. + ct_ssp_earned_income_disregard: 142.50 # $85 + ($200 - $85)/2. + ct_ssp_unearned_income_disregard: 629.90 + # ($936.50 - $629.90) + ($200 - $142.50) = $364.10. + ct_ssp_countable_income: 364.10 + # $200 shelter + $197.35 personal needs + $36.20 therapeutic diet. + ct_ssp_need_standard: 433.55 + ct_ssp_income_eligible: true + ct_ssp_eligible_person: true + ct_ssp_person: 69.45 # $433.55 - $364.10. + ct_ssp: 69.45 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml new file mode 100644 index 00000000000..4b95f69554c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml @@ -0,0 +1,145 @@ +# SSI salary-reduction treatment: POMS SI 00820.102C.2. +# MSA countable exclusions: Minn. Stat. 256D.435 subds. 4a, 5, and 6. +# MSA gross income: Minn. Stat. 256D.35 subd. 10. Rule 9500.1206 subp. 15a +# describes GA conventions, adopted for non-SSI MSA recipients by CM 0017. +# Guardian cap: Minn. Stat. 256D.44 subd. 5(d). +# https://secure.ssa.gov/poms.nsf/lnx/0500820102 +# https://www.revisor.mn.gov/statutes/cite/256D.435 +# https://www.revisor.mn.gov/statutes/cite/256D.35 +# https://www.revisor.mn.gov/rules/9500.1206/ +# https://www.revisor.mn.gov/statutes/cite/256D.44 + +- name: MSA excludes pretax premiums from countable income and restores them for guardian fees + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 20_400 # $1,700/month before salary reduction. + pre_tax_health_insurance_premiums: 1_200 + social_security: 3_000 + mn_msa_has_guardian: true + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: MN + output: + ssi_earned_income: 1_600 # $1,700 - $100. + # Countable = ($1,600 - $65)/2 + ($250 - $20) = $997.50/month. + ssi_countable_income: 997.50 + ssi: 0 # Countable $997.50 exceeds the $994 FBR. + mn_msa_gross_income: 1_950 # $1,700 gross wages + $250 Social Security. + mn_msa_gross_income_eligible: true # $1,950 <= 3 * $994. + mn_msa_guardian_fee: 97.50 # min(5% * $1,950, $100). + mn_msa_net_income_eligible: true + mn_msa_eligible_person: true + mn_msa_assistance_standard: 1_055 + mn_msa_person: 155 # $1,055 + $97.50 - $997.50. + mn_msa: 155 + +- name: MSA SSI track substitutes the FBR and keeps pretax premiums in guardian gross income + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 7_200 + pre_tax_health_insurance_premiums: 1_200 + mn_msa_has_guardian: true + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: MN + output: + ssi_earned_income: 500 # $600 - $100. + ssi: 786.50 # $994 - ($500 - $20 - $65)/2. + # CM 0017 uses the full FBR: gross = $600 + $994, rather than the SSI check. + mn_msa_gross_income: 1_594 + mn_msa_gross_income_eligible: true + mn_msa_guardian_fee: 79.70 # 5% * $1,594. + mn_msa_net_income_eligible: true + mn_msa_eligible_person: true + mn_msa_person: 160.70 # $1,055 + $79.70 - ($994 - $20). + mn_msa: 160.70 + +- name: MSA gross test includes pretax premiums at the individual limit + period: 2026-01 + input: + age: 70 + state_code: MN + employment_income: 35_784 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 2_882 # $2,982 - $100. + ssi: 0 + mn_msa_gross_income: 2_982 + mn_msa_gross_income_eligible: true # $2,982 = 3 * $994. + +- name: MSA gross test includes pretax premiums one dollar above the individual limit + period: 2026-01 + input: + age: 70 + state_code: MN + employment_income: 35_796 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 2_883 # $2,983 - $100. + ssi: 0 + mn_msa_gross_income: 2_983 + mn_msa_gross_income_eligible: false # $2,983 > 3 * $994. + +- name: MSA gross couple limit includes both spouses salary reductions + period: 2026-01 + input: + people: + person1: + age: 70 + employment_income: 35_796 + pre_tax_health_insurance_premiums: 1_200 + person2: + age: 68 + employment_income: 35_784 + pre_tax_health_insurance_premiums: 1_200 + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MN + output: + ssi_earned_income: [2_883, 2_882] + ssi: [0, 0] + mn_msa_gross_income: [2_983, 2_982] + # Both are aged: $5,965 gross exceeds the couple limit 6 * $994 = $5,964. + mn_msa_gross_income_eligible: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml index 10ac417a1b4..30c47ef1f99 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml @@ -61,3 +61,24 @@ output: # Countable 500/mo; premiums 1,000/mo; max(500 - 1,000, 0) = 0. mo_ccs_adjusted_income: 0 + +- name: Pretax and after-tax premiums are each deducted once from gross wages. + period: 2026-01 + input: + state_code: MO + employment_income: 36_000 + health_insurance_premiums: 2_400 + pre_tax_health_insurance_premiums: 3_600 + output: + mo_ccs_countable_income: 3_000 + mo_ccs_adjusted_income: 2_500 + +- name: Moving all premiums to pretax payroll leaves adjusted income unchanged. + period: 2026-01 + input: + state_code: MO + employment_income: 36_000 + pre_tax_health_insurance_premiums: 6_000 + output: + mo_ccs_countable_income: 3_000 + mo_ccs_adjusted_income: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml index 9dcdb1e7932..a8a8257979a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml @@ -63,6 +63,64 @@ output: nd_renters_refund_income: 12_000 +- name: Case 4, after-tax premiums reduce renter refund income as medical expenses. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 0 + output: + market_income: 40_000 + itemized_medical_expenses: 6_000 + nd_renters_refund_income: 34_000 + +- name: Case 5, mixed premiums reduce renter refund income once through each disjoint route. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 3_000 + pre_tax_health_insurance_premiums: 3_000 + output: + market_income: 40_000 + irs_employment_income: 37_000 + itemized_medical_expenses: 3_000 + nd_renters_refund_income: 34_000 + +- name: Case 6, pretax premiums reduce renter refund income as excluded wage income. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 6_000 + output: + market_income: 40_000 + irs_employment_income: 34_000 + itemized_medical_expenses: 0 + nd_renters_refund_income: 34_000 + +- name: Case 7, employer-paid premiums do not reduce renter refund income. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 0 + employer_sponsored_insurance_premiums: 6_000 + output: + market_income: 40_000 + nd_renters_refund_income: 40_000 + - name: North Dakota renter refund deducts medical expenses actually paid from gross income period: 2025 input: @@ -136,3 +194,21 @@ output: itemized_medical_expenses_applied_exclusion: 0 nd_renters_refund_income: 30_000 + +- name: North Dakota combines the pretax income exclusion with paid SE medical costs + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + state_code: ND + employment_income: 10_000 + self_employment_income: 30_000 + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 3_000 + other_medical_expenses: 500 + self_employed_health_insurance_ald: 1_000 + output: + itemized_medical_expenses: 5_500 + itemized_medical_expenses_applied_exclusion: 1_000 + # 40,000 - 3,000 - (5,500 + 1,000) = 30,500. + nd_renters_refund_income: 30_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml index 6aa0d6ff82c..5e3482dc63a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml @@ -82,6 +82,61 @@ nj_agi: 50_000 nj_medical_expense_deduction: 1_000 +- name: New Jersey allows after-tax premiums above two percent of gross income + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 0 + state_code: NJ + output: + nj_agi: 40_000 + nj_medical_expense_deduction: 5_200 + +- name: New Jersey counts disjoint payroll and after-tax premiums once each + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 3_000 + pre_tax_health_insurance_premiums: 3_000 + state_code: NJ + output: + nj_agi: 40_000 + irs_employment_income: 37_000 + nj_medical_expense_deduction: 5_200 + +- name: New Jersey allows federally pretax payroll premiums included in state wages + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 6_000 + state_code: NJ + output: + nj_agi: 40_000 + irs_employment_income: 34_000 + nj_medical_expense_deduction: 5_200 + +- name: Employer-paid premiums are not employee medical expenses in New Jersey + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 0 + employer_sponsored_insurance_premiums: 6_000 + state_code: NJ + output: + nj_agi: 40_000 + nj_medical_expense_deduction: 0 + - name: New Jersey counts self-employed premiums once across its medical deductions period: 2025 input: @@ -151,3 +206,21 @@ # Separate SE deduction 1,000 + general medical premiums 1,000 = 2,000. # https://pub.njleg.gov/bills/9899/PL99/222_.HTM nj_medical_expense_deduction: 2_000 + +- name: New Jersey combines pretax premiums with separately deducted SE premiums + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + state_code: NJ + employment_income: 10_000 + self_employment_income: 30_000 + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 3_000 + other_medical_expenses: 500 + self_employed_health_insurance_ald: 1_000 + output: + itemized_medical_expenses: 5_500 + nj_agi: 40_000 + # 1,000 + (5,500 + 3,000 - 0.02 * 40,000) = 8,700. + nj_medical_expense_deduction: 8_700 diff --git a/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml index be942e60178..2424d08202e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml @@ -141,3 +141,25 @@ # Adjusted monthly: 5,000 - 587 = 4,413 # Annual: 4,413 * 12 = 52,956 pa_ccw_adjusted_income: 52_956 + +- name: Pretax and after-tax premiums both count toward the medical deduction. + period: 2026 + input: + state_code: PA + employment_income: 36_000 + health_insurance_premiums: 2_400 + pre_tax_health_insurance_premiums: 3_600 + output: + pa_ccw_medical_expenses: 6_000 + # Gross wages 36,000 less premiums 6,000 above the 3,600 threshold. + pa_ccw_adjusted_income: 33_600 + +- name: Moving all premiums to pretax payroll leaves the medical deduction unchanged. + period: 2026 + input: + state_code: PA + employment_income: 36_000 + pre_tax_health_insurance_premiums: 6_000 + output: + pa_ccw_medical_expenses: 6_000 + pa_ccw_adjusted_income: 33_600 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml new file mode 100644 index 00000000000..3e2d3b7f9e7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml @@ -0,0 +1,29 @@ +- name: SNAP medical expenses include disjoint non-pretax and payroll premiums. + period: 2026 + input: + health_insurance_premiums: 1_800 + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + over_the_counter_health_expenses: 900 + medicare_enrolled: false + output: + snap_allowable_medical_expenses: 4_500 + +- name: SNAP adds payroll premiums to decomposed non-pretax Medicare premiums. + period: 2026 + input: + health_insurance_premiums_without_medicare_part_b: 600 + medicare_part_b_premium: 1_200 + medicare_enrolled: true + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + output: + snap_allowable_medical_expenses: 4_500 + +- name: SNAP medical expenses include premiums paid entirely through payroll. + period: 2026 + input: + pre_tax_health_insurance_premiums: 2_400 + medicare_enrolled: false + output: + snap_allowable_medical_expenses: 2_400 diff --git a/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml b/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml index 0d5aee02b6f..86061b7a0d9 100644 --- a/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml @@ -100,3 +100,37 @@ marketplace_net_premium: 5_000 spm_unit_health_insurance_premiums: 5_000 spm_unit_medical_out_of_pocket_expenses: 5_100 + +- name: SPM medical expenses include disjoint payroll and non-pretax premiums. + period: 2026 + input: + pre_tax_health_insurance_premiums: 2_400 + other_health_insurance_premiums: 600 + chip_premium: 0 + medicaid_premium: 0 + marketplace_net_premium: 0 + medicare_part_a_premium: 0 + medicare_part_b_premium: 0 + income_adjusted_part_d_premium_surcharge: 0 + other_medical_expenses: 300 + over_the_counter_health_expenses: 100 + output: + spm_unit_health_insurance_premiums: 3_000 + spm_unit_medical_out_of_pocket_expenses: 3_400 + +- name: Moving all premiums to pretax payroll leaves SPM medical expenses unchanged. + period: 2026 + input: + pre_tax_health_insurance_premiums: 3_000 + other_health_insurance_premiums: 0 + chip_premium: 0 + medicaid_premium: 0 + marketplace_net_premium: 0 + medicare_part_a_premium: 0 + medicare_part_b_premium: 0 + income_adjusted_part_d_premium_surcharge: 0 + other_medical_expenses: 300 + over_the_counter_health_expenses: 100 + output: + spm_unit_health_insurance_premiums: 3_000 + spm_unit_medical_out_of_pocket_expenses: 3_400 diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py index cd95bdbce86..e4702532c46 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py @@ -6,9 +6,20 @@ class medicaid_medically_needy_medical_expenses(Variable): entity = Person label = "Medicaid medically needy medical expenses" unit = USD - documentation = "Medical expenses used for Medicaid medically needy spenddown." + documentation = ( + "Annual expense proxy for SSI-based Medicaid medically needy spenddown. " + "Includes non-pretax health premiums and other medical expenses. " + "Qualified pretax payroll premiums are excluded through SSI earnings " + "rather than deducted again here. State-specific income and expense " + "sequencing, including states using gross-income payroll deductions, " + "is not modeled by this proxy." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/42/part-435/subpart-D" + reference = ( + "https://www.ecfr.gov/current/title-42/section-435.831#p-435.831(d)", + "https://www.ecfr.gov/current/title-42/section-435.831#p-435.831(e)(1)", + "https://khap.kdhe.ks.gov/KEESM/July_2026_Output/7000/7500-Determination-of-Financial-Eligibility.htm", + ) adds = [ "medical_expense_health_insurance_premiums", diff --git a/policyengine_us/variables/gov/hud/hud_medical_expenses.py b/policyengine_us/variables/gov/hud/hud_medical_expenses.py index bea476968e1..af53ee69dbd 100644 --- a/policyengine_us/variables/gov/hud/hud_medical_expenses.py +++ b/policyengine_us/variables/gov/hud/hud_medical_expenses.py @@ -6,11 +6,20 @@ class hud_medical_expenses(Variable): entity = SPMUnit label = "HUD medical expenses" unit = USD - documentation = "Medical expenses considered in HUD adjusted income." + documentation = ( + "Medical expenses considered in HUD adjusted income, including " + "unreimbursed employee-paid health insurance premiums whether paid " + "through pretax payroll deductions or from non-pretax funds." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/24/5.611" + reference = ( + "https://www.ecfr.gov/current/title-24/section-5.603", + "https://www.ecfr.gov/current/title-24/section-5.611", + "https://www.hud.gov/sites/dfiles/OCHCO/documents/2023-27pihn.pdf#page=37", + ) adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py b/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py index 66b1c325fff..9155d19ebf9 100644 --- a/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py +++ b/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py @@ -8,8 +8,8 @@ class ca_oc_general_relief_countable_income_person(Variable): unit = USD definition_period = MONTH defined_for = "in_oc" - # Sections 70.2.o through 70.2.q (pp. 4-5). - reference = "https://www.ssa.ocgov.com/sites/ssa/files/2025-03/Income.pdf#page=4" + # Sections 70.2.a(1), .o(4), and .p(1): PDF pages 1, 4. + reference = "https://www.ssa.ocgov.com/sites/ssa/files/2025-03/Income.pdf#page=1" def formula(person, period, parameters): # NOTE: Section 70.2.p also allows mandatory federal/state income tax @@ -24,11 +24,15 @@ def formula(person, period, parameters): "ca_oc_general_relief_gross_unearned_income", ], ) + # Section 70.2.o(4) and .p(1) allow medical insurance payments; + # 70.2.a(1) counts earnings diverted to a third party as gross income. + # Employee pretax premiums therefore remain deductible here. deductions = add( person, period, [ "health_insurance_premiums", + "pre_tax_health_insurance_premiums", "child_support_expense", "alimony_expense", ], diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py index 8b84426daf1..5c493c0a7c8 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py @@ -7,5 +7,22 @@ class ssi_earned_income(Variable): label = "SSI earned income" unit = USD definition_period = YEAR + reference = "https://secure.ssa.gov/poms.nsf/lnx/0500820102" + documentation = ( + "Earned income for SSI, excluding health insurance premiums paid " + "through pretax payroll salary reductions under a qualified " + "cafeteria plan. The disjoint after-tax premium inputs do not " + "reduce SSI wages." + ) - adds = "gov.ssa.ssi.income.sources.earned" + def formula(person, period, parameters): + sources = parameters(period).gov.ssa.ssi.income.sources.earned + earned_income = add(person, period, sources) + if "employment_income" not in sources: + return earned_income + # POMS SI 00820.102C.2 excludes salary-reduction premiums from + # wages; C.4 counts ordinary payroll deductions. Limit the exclusion + # to wages so it cannot reduce self-employment income. + wages = max_(person("employment_income", period), 0) + pretax_premiums = max_(person("pre_tax_health_insurance_premiums", period), 0) + return earned_income - min_(wages, pretax_premiums) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py index 5f41d4d0bca..d116dabb369 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py @@ -6,10 +6,28 @@ class ssi_engaged_in_sga(Variable): entity = Person label = "Income less than the SGA limit" definition_period = YEAR - reference = "https://www.ssa.gov/OP_Home/cfr20/416/416-0971.htm" + reference = ( + "https://www.ssa.gov/OP_Home/cfr20/416/416-0971.htm", + "https://secure.ssa.gov/poms.nsf/lnx/0410505010", + ) + documentation = ( + "Whether non-blind work earnings exceed the SGA limit. Payroll " + "health insurance premiums remain in these earnings, including " + "pretax salary reductions excluded from SSI payment income." + ) def formula(person, period, parameters): income = person("ssi_earned_income", period) + # POMS DI 10505.010A counts payroll insurance premiums for SGA. + # Restore exactly the wage-limited premium exclusion made by + # ssi_earned_income, preserving the prior gross work-earnings base. + sources = parameters(period).gov.ssa.ssi.income.sources.earned + if "employment_income" in sources: + wages = max_(person("employment_income", period), 0) + pretax_premiums = max_( + person("pre_tax_health_insurance_premiums", period), 0 + ) + income = income + min_(wages, pretax_premiums) monthly_income = income / MONTHS_IN_YEAR p = parameters(period).gov.ssa.sga diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py index e13a80161b3..27de6fc0bc3 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py @@ -22,20 +22,7 @@ def formula(person, period, parameters): ssi_individual_fbr = parameters(period).gov.ssa.ssi.amount.individual categorically_eligible = person("is_ssi_aged_blind_disabled", period.this_year) both_eligible = person.marital_unit.sum(categorically_eligible) == 2 - income = add( - person, - period, - [ - "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 = person("ssi", period) - ssi_fbr = person("ssi_amount_if_eligible", period) - gross = income + where(ssi > 0, ssi_fbr, 0) + gross = person("mn_msa_gross_income", period) au_income = where(both_eligible, person.marital_unit.sum(gross), gross) cap = ssi_individual_fbr * where( both_eligible, p.couple_fbr_multiplier, p.individual_fbr_multiplier diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py index 7543e4d17a8..bd8a807be09 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 @@ -11,6 +11,7 @@ class mn_msa_net_income_eligible(Variable): definition_period = MONTH defined_for = StateCode.MN reference = ( + "https://www.revisor.mn.gov/statutes/cite/256D.435", "https://www.revisor.mn.gov/statutes/cite/256D.44", "https://www.dhs.state.mn.us/main/groups/county_access/documents/pub/mndhs-073585.pdf#page=2", ) @@ -26,6 +27,8 @@ def formula(person, period, parameters): # (federal SSI already consumed the $65 + 1/2 earned disregards). # Non-SSI track recipients get the standard $20 + $65 + 1/2 # disregards on actual earned and unearned income. + # Section 256D.435 Subd. 4a adopts SSI income exclusions, including + # the salary-reduction premium exclusion already in ssi_earned_income. # FLA-D recipients are exempt from the $20 disregard so the formula # collapses to FBR + unearned vs PNA. arrangement = person("mn_msa_payment_category", period) 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 new file mode 100644 index 00000000000..a6438e3c287 --- /dev/null +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * + + +class mn_msa_gross_income(Variable): + value_type = float + entity = Person + label = "Minnesota Supplemental Aid gross income" + unit = USD + definition_period = MONTH + defined_for = StateCode.MN + # Section 256D.35 directly defines MSA gross income. Rule 9500.1206 + # describes GA conventions, which CM 0017 adopts for non-SSI recipients. + reference = ( + "https://www.revisor.mn.gov/statutes/cite/256D.35#stat.256D.35.10", + "https://www.revisor.mn.gov/rules/9500.1206/#rule.9500.1206.15a", + "https://www.dhs.state.mn.us/main/idcplg?IdcService=GET_DYNAMIC_CONVERSION&RevisionSelectionMethod=LatestReleased&dDocName=cm_0017", + ) + + def formula(person, period, parameters): + income = add( + person, + period, + [ + "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", + ], + ) + # MSA gross income is measured before deductions and disregards. + # Its net-income calculation adopts SSI exclusions (256D.435), + # but the gross test and guardian-fee cap must restore the employee's + # salary-reduction premium excluded from their own SSI wages. + # Keep the SSI financial-responsibility rules for deemed income. + sources = parameters(period).gov.ssa.ssi.income.sources.earned + if "employment_income" in sources: + wages = max_(person("employment_income", period.this_year), 0) + premiums = max_( + person("pre_tax_health_insurance_premiums", period.this_year), 0 + ) + income += min_(wages, premiums) / MONTHS_IN_YEAR + + # CM 0017 substitutes the full FBR for an SSI recipient's payment. + ssi = person("ssi", period) + ssi_fbr = person("ssi_amount_if_eligible", period) + return income + where(ssi > 0, ssi_fbr, 0) 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 997a757ec09..4019169214c 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 @@ -12,6 +12,7 @@ class mn_msa_person(Variable): definition_period = MONTH defined_for = "mn_msa_eligible_person" reference = ( + "https://www.revisor.mn.gov/statutes/cite/256D.435", "https://www.revisor.mn.gov/statutes/cite/256D.44", "https://www.house.mn.gov/hrd/pubs/pap_MSA.pdf#page=2", "https://www.dhs.state.mn.us/main/groups/county_access/documents/pub/mndhs-073585.pdf#page=4", @@ -24,6 +25,8 @@ def formula(person, period, parameters): # mn_msa_net_income_eligible). Keep the two formulas in sync when # changing income treatment. # MSA inherits federal SSI's $20 / $65 / 1/2 disregards (CM 0018.18). + # Section 256D.435 Subd. 4a also adopts SSI income exclusions; + # ssi_earned_income already excludes salary-reduction premiums. # The SSI track substitutes the federal SSI FBR for the recipient's # post-disregard SSI payment so MSA tops up against the FBR rather # than the federal payment. FLA-D recipients are exempt from the $20 diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py index 87d919c7654..1cb654a81cd 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py @@ -22,20 +22,7 @@ def formula(person, period, parameters): # the marital unit before applying the cap. p = parameters(period).gov.states.mn.dhs.msa.special_needs.guardian_fee has_guardian = person("mn_msa_has_guardian", period) - income = add( - person, - period, - [ - "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 = person("ssi", period) - ssi_fbr = person("ssi_amount_if_eligible", period) - gross = income + where(ssi > 0, ssi_fbr, 0) + gross = person("mn_msa_gross_income", period) unit_gross = person.marital_unit.sum(gross) unit_fee = min_(unit_gross * p.rate, p.max_amount) return has_guardian * unit_fee diff --git a/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py b/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py index af0a1d0c7d1..e28cd45ad1e 100644 --- a/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py +++ b/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py @@ -8,12 +8,19 @@ class mo_ccs_adjusted_income(Variable): definition_period = MONTH unit = USD defined_for = StateCode.MO - reference = "https://www.law.cornell.edu/regulations/missouri/5-CSR-25-200-050" + reference = ( + "https://www.sos.mo.gov/CMSImages/AdRules/csr/current/5csr/5c25-200.pdf#page=2" + ) def formula(spm_unit, period, parameters): countable_income = spm_unit("mo_ccs_countable_income", period) # Medical insurance premiums are the only deduction from monthly gross # income (5 CSR 25-200.050(2); Manual sec. 5.7 Deductions from Gross - # Income). - premiums = add(spm_unit, period, ["health_insurance_premiums"]) + # Income). The rule includes premiums paid by household members, + # whether through pretax payroll deductions or after-tax payments. + premiums = add( + spm_unit, + period, + ["health_insurance_premiums", "pre_tax_health_insurance_premiums"], + ) return max_(countable_income - premiums, 0) diff --git a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py index 7f9aaeaab05..4c0fe2b3557 100644 --- a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py +++ b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py @@ -24,6 +24,12 @@ class nd_renters_refund_income(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.nd.tax.property.renters_refund + # NDCC 57-02-08.1(5)(c) excludes income excluded by federal or state + # law. market_income includes wages before payroll deductions, so + # subtract the separate pretax premium input once from this base. + # These premiums are disjoint from itemized_medical_expenses, + # whose medical definition follows state income tax law. + excluded_premiums = add(tax_unit, period, ["pre_tax_health_insurance_premiums"]) # Restore only premiums excluded from a computed federal subtotal. medical_expenses = add( tax_unit, @@ -34,6 +40,8 @@ def formula(tax_unit, period, parameters): ], ) return max_( - add(tax_unit, period, p.income_sources) - medical_expenses, + add(tax_unit, period, p.income_sources) + - excluded_premiums + - medical_expenses, 0, ) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py index b5d778fcfef..2ac5a575634 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py @@ -20,6 +20,8 @@ class nj_medical_expense_deduction(Variable): ) reference = ( "https://pub.njleg.gov/bills/9899/PL99/222_.HTM", + "https://www.nj.gov/treasury/taxation/njit13.shtml", + "https://www.nj.gov/treasury/taxation/pdf/pubs/tb/tb39r.pdf#page=1", "https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=25", ) @@ -44,8 +46,14 @@ def formula(tax_unit, period, parameters): person("self_employed_health_insurance_ald_excluded_premiums", period) * person("is_tax_unit_dependent", period) ) + # New Jersey includes salary-reduction cafeteria-plan premiums in + # taxable wages (TB-39(R)), so these employee-paid premiums also + # qualify for the medical expense deduction. The separate payroll + # input is disjoint from the after-tax premium inputs. medical_expenses = ( - tax_unit("itemized_medical_expenses", period) - excluded_dependent_premiums + tax_unit("itemized_medical_expenses", period) + - excluded_dependent_premiums + + add(tax_unit, period, ["pre_tax_health_insurance_premiums"]) ) agi = tax_unit("nj_agi", period) floor = p.rate * agi diff --git a/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py b/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py index d8e0b7ad524..e2a50d71760 100644 --- a/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py +++ b/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py @@ -6,12 +6,20 @@ class pa_ccw_medical_expenses(Variable): entity = SPMUnit label = "Pennsylvania CCW medical expenses" unit = USD - documentation = "Medical expenses deducted from Pennsylvania CCW income." + documentation = ( + "Medical expenses deducted from Pennsylvania CCW income, including " + "employee-paid health premiums paid through pretax payroll deductions " + "and after-tax payments." + ) definition_period = YEAR defined_for = StateCode.PA - reference = "https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter3042/055_3042.pdf#page=18" + # PDF pages 59, 61. + reference = "https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter3042/055_3042.pdf#page=59" + # Appendix A Part I(A) counts wages before health insurance deductions; + # Part II(C) permits unreimbursed health care premiums without a tax test. adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py b/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py index fddfa2f03a5..f9767827dd6 100644 --- a/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py +++ b/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py @@ -10,6 +10,7 @@ class snap_allowable_medical_expenses(Variable): reference = [ "https://www.law.cornell.edu/uscode/text/7/2014#e_5", "https://www.law.cornell.edu/cfr/text/7/273.9#d_3", + "https://www.ecfr.gov/current/title-7/section-273.9#p-273.9(d)(3)(iv)", ] documentation = ( "Medical expenses allowable for SNAP's excess medical expense " @@ -21,10 +22,13 @@ class snap_allowable_medical_expenses(Variable): "insurance premiums, Medicare premiums, cost sharing, medical " "supplies, transportation, and related services. Current modeling " "uses health insurance premiums and other medical expenses, excluding " - "general over-the-counter health expenses." + "general over-the-counter health expenses. Employee-paid premiums " + "include both pretax payroll deductions and the disjoint non-pretax " + "premium amounts." ) adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/household/expense/health/health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/health_insurance_premiums.py index 936031f7fc9..e7ed3b9f651 100644 --- a/policyengine_us/variables/household/expense/health/health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/health_insurance_premiums.py @@ -9,7 +9,11 @@ class health_insurance_premiums(Variable): definition_period = YEAR uprating = "calibration.gov.hhs.cms.moop_per_capita" documentation = ( - "Person-level health insurance premiums supplied directly as an input. " - "SPM MOOP and statutory medical expense definitions use decomposed " - "premium aggregates." + "Annual person-paid health insurance premiums supplied directly as an " + "input, excluding premiums paid through pretax payroll deductions and " + "employer-paid premiums. Pretax payroll payments belong only in " + "pre_tax_health_insurance_premiums; the two sets are disjoint and total " + "person-paid premiums equal their sum. This direct non-pretax amount is " + "an alternative to the applicable decomposed non-pretax inputs, not an " + "additional expense." ) diff --git a/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py b/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py index 92be6e51a96..b9ded858ef9 100644 --- a/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py +++ b/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py @@ -7,4 +7,12 @@ class health_insurance_premiums_without_medicare_part_b(Variable): label = "Health insurance premiums without Medicare Part B premiums" unit = USD definition_period = YEAR + documentation = ( + "Annual person-paid health insurance premiums excluding Medicare Part B, " + "pretax payroll deductions, and employer-paid premiums. Pretax payroll " + "payments belong only in pre_tax_health_insurance_premiums. The pretax " + "and non-pretax sets are disjoint; total person-paid premiums equal " + "pretax payments plus the applicable non-pretax aggregate. This input is " + "the non-Medicare component of the medical expense premium aggregate." + ) uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py index 71a9825e4ef..27826f81fc4 100644 --- a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py @@ -7,4 +7,12 @@ class long_term_health_insurance_premiums(Variable): label = "Long-term health insurance premiums" unit = USD definition_period = YEAR + documentation = ( + "Annual person-paid long-term care insurance premiums excluding pretax " + "payroll deductions and employer-paid premiums. Pretax payroll payments " + "belong only in pre_tax_health_insurance_premiums. The pretax and non- " + "pretax sets are disjoint; total person-paid premiums equal pretax " + "payments plus the applicable non-pretax aggregate. This component must " + "be counted only once when included in a broader premium input." + ) uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py index dfb010bfed6..307305f4176 100644 --- a/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py @@ -8,10 +8,13 @@ class medical_expense_health_insurance_premiums(Variable): unit = USD definition_period = YEAR documentation = ( - "Person-level health insurance premiums counted by statutory medical " - "expense definitions. Uses a direct premium input when supplied; " - "otherwise combines reported non-Medicare premiums with modeled " - "Medicare Part B premiums." + "Person-level non-pretax health insurance premiums for medical expense " + "definitions. Uses a nonzero direct health_insurance_premiums input; " + "otherwise combines non-Medicare premiums with modeled Medicare Part B " + "premiums for enrollees. Excludes the disjoint " + "pre_tax_health_insurance_premiums input. Consumers whose rules count " + "pretax payroll payments add that separate input once; tax deductions " + "that exclude them use this aggregate alone." ) def formula(person, period, parameters): diff --git a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py index 500970c641d..09a07b4c37e 100644 --- a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py +++ b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py @@ -6,5 +6,14 @@ class medicare_part_b_premiums_reported(Variable): entity = Person label = "Medicare Part B premiums (reported)" definition_period = YEAR + documentation = ( + "Annual reported person-paid Medicare Part B premiums excluding pretax " + "payroll deductions and employer-paid premiums. Pretax payroll payments " + "belong only in pre_tax_health_insurance_premiums. The pretax and non- " + "pretax sets are disjoint; total person-paid premiums equal pretax " + "payments plus the applicable non-pretax aggregate. A reported component " + "must not be added again when already included in a broader premium " + "input." + ) unit = USD uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py index 1f90a92ff02..e409c2e03a9 100644 --- a/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py @@ -9,7 +9,11 @@ class other_health_insurance_premiums(Variable): definition_period = YEAR uprating = "calibration.gov.hhs.cms.moop_per_capita" documentation = ( - "Person-level health insurance premiums not otherwise represented by " - "modeled Marketplace, CHIP, Medicaid, Medicare Part A or Part B " - "premiums, or the Part D IRMAA surcharge." + "Annual person-paid health insurance premiums not otherwise represented " + "by modeled Marketplace, CHIP, Medicaid, Medicare Part A or Part B " + "premiums, or the Part D IRMAA surcharge. Excludes pretax payroll " + "deductions and employer-paid premiums. Pretax payroll payments belong " + "only in pre_tax_health_insurance_premiums. The pretax and non-pretax " + "sets are disjoint; total person-paid premiums equal their sum, with " + "modeled non-pretax components counted once." ) diff --git a/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py index 1f1a9e813e4..d908d9bb22c 100644 --- a/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py @@ -7,9 +7,12 @@ class pre_tax_health_insurance_premiums(Variable): label = "Pre-tax health insurance premiums" unit = USD documentation = ( - "Health insurance premiums paid through pre-tax payroll deductions. " - "This excludes Medicare Part B premiums and other post-tax medical " - "out-of-pocket premiums. These are employees' shares of " + "Annual employee-paid health insurance premiums paid through pretax " + "payroll deductions. Supply these only here: health_insurance_premiums " + "and the applicable decomposed premium inputs exclude these payments. The " + "pretax and non-pretax sets are disjoint; total employee-paid premiums " + "equal their sum. Excludes employer-paid premiums, Medicare Part B " + "premiums and other post-tax medical premiums. These are employees' shares of " "employer-sponsored insurance premiums, so they are uprated by CMS's " "employer-sponsored insurance premium per enrollee, holding the " "number of people covered per policy and the employee's share of the " diff --git a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py index 903b966d923..b548d91392b 100644 --- a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py @@ -6,7 +6,14 @@ class self_employed_health_insurance_premiums(Variable): entity = Person label = "Self-employed health insurance premiums" unit = USD - documentation = "Health insurance premiums for plans covering individuals who are not covered by any employer-sponsored health insurance." + documentation = ( + "Annual person-paid health insurance premiums for plans covering " + "individuals not covered by employer-sponsored health insurance. Excludes " + "pretax payroll deductions and employer-paid premiums; pretax payroll " + "payments belong only in pre_tax_health_insurance_premiums. The pretax " + "and non-pretax sets are disjoint. Defaults to the direct non-pretax " + "health_insurance_premiums input for self-employed people." + ) definition_period = YEAR defined_for = "is_self_employed" adds = ["health_insurance_premiums"] diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py index 6be89db1ae8..9d31a40245b 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py @@ -7,14 +7,21 @@ class spm_unit_health_insurance_premiums(Variable): label = "SPM unit health insurance premiums" unit = USD definition_period = YEAR + reference = ( + "https://www2.census.gov/programs-surveys/supplemental-poverty-measure/datasets/spm/spm_techdoc.pdf#page=18", + "https://www2.census.gov/library/working-papers/2026/adrm/cbsm/rsm2026-04.pdf#page=65", + ) documentation = ( "Health insurance premium expenses for an SPM unit, combining a " "data-imputed other premium component with modeled premium components " "that can respond to policy reforms, including Medicare Part A and " - "Part B premiums and the Part D IRMAA surcharge paid out of pocket." + "Part B premiums and the Part D IRMAA surcharge paid out of pocket. " + "Includes employee-paid pretax payroll premiums in addition to the " + "disjoint non-pretax premium components; excludes employer contributions." ) adds = [ + "pre_tax_health_insurance_premiums", "other_health_insurance_premiums", "chip_premium", "medicaid_premium", diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py index f06025f553f..ba4c6c0cd64 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py @@ -10,8 +10,9 @@ class spm_unit_medical_out_of_pocket_expenses(Variable): documentation = ( "Total medical out-of-pocket expenses at the SPM unit level, " "combining health insurance premiums with non-premium medical " - "expenses. Health insurance premiums include other health insurance " - "premiums plus modeled Marketplace, CHIP, Medicaid, and Medicare " + "expenses. Health insurance premiums include pretax payroll premiums " + "and disjoint other health insurance premiums plus modeled " + "Marketplace, CHIP, Medicaid, and Medicare " "Part A and Part B premiums net of Medicare Savings Program coverage " "where modeled, plus the Part D IRMAA surcharge. " "Non-premium expenses include other medical expenses and " diff --git a/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py b/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py index bf21c62d1a2..a727a33f829 100644 --- a/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py +++ b/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py @@ -7,7 +7,9 @@ class employer_sponsored_insurance_premiums(Variable): label = "Employer-sponsored insurance premiums" documentation = ( "Annual employer-paid health insurance premiums. CBO treats this " - "as part of household market income. Uprated by CMS's " + "as part of household market income. Excludes employee-paid premiums, " + "both pretax payroll deductions in pre_tax_health_insurance_premiums " + "and non-pretax payments in the other premium inputs. Uprated by CMS's " "employer-sponsored insurance premium per enrollee, so each " "policyholder's premium grows with premiums rather than wages. This " "is a premium-growth proxy: it holds the number of people covered "