diff --git a/changelog.d/hub-pb-id-premiums-1009.fixed.md b/changelog.d/hub-pb-id-premiums-1009.fixed.md new file mode 100644 index 00000000000..5563b17bd5e --- /dev/null +++ b/changelog.d/hub-pb-id-premiums-1009.fixed.md @@ -0,0 +1 @@ +Fix Idaho's subtraction for health insurance premiums paid by the tax unit's head and spouse, including modeled Medicare premiums for enrolled filers, exclude premiums already deducted or excluded from income, and coordinate the subtraction, deduction election, and derived Idaho itemized deductions using claimant-paid medical expenses while preserving supplied actual medical and itemized deductions. diff --git a/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml b/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml index ee01efbc66d..b2e90d71d34 100644 --- a/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml +++ b/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml @@ -11,6 +11,7 @@ values: - workers_compensation # (13) - id_retirement_benefits_deduction # (15) - id_529_plan_subtraction # Idaho Code 63-3022(n) + - id_health_insurance_premiums_subtraction # Idaho Code 63-3022P 2025-01-01: - salt_refund_income # (2) - us_govt_interest # (3)(a) @@ -23,6 +24,7 @@ values: - id_retirement_benefits_deduction # (15) - id_military_retirement_deduction # (15) per HB 40 - id_529_plan_subtraction # Idaho Code 63-3022(n) + - id_health_insurance_premiums_subtraction # Idaho Code 63-3022P metadata: @@ -34,7 +36,9 @@ metadata: href: https://law.justia.com/codes/idaho/2022/title-63/chapter-30/section-63-3022d/ - title: Individual Income Tax Forms and Instructions 2021 href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_11-15-2021.pdf#page=30 - - title: Idaho Individual Income Tax Instructions Packet 2025 + - title: "Idaho Individual Income Tax Instructions Packet 2025 #page 33,48-49" href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=33 + - title: Idaho Code Section 63-3022P - Health insurance costs + href: https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/ - title: Section 35.01.01.254 - (5) / (3) (a) href: https://regulations.justia.com/states/idaho/35/35-01-01/section-35-01-01-254/ diff --git a/policyengine_us/tests/core/test_id_health_insurance_premiums.py b/policyengine_us/tests/core/test_id_health_insurance_premiums.py new file mode 100644 index 00000000000..b27b2be36ac --- /dev/null +++ b/policyengine_us/tests/core/test_id_health_insurance_premiums.py @@ -0,0 +1,281 @@ +"""Compare Idaho premium election with independently enumerated legal routes.""" + +from importlib.metadata import version + +from hypothesis import example, given, settings, strategies as st +import numpy as np +from packaging.version import Version +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system as SYSTEM + + +PERIOD = "2026" +AMOUNT = st.integers(min_value=0, max_value=50_000) +ROW = st.fixed_dictionaries( + { + "premiums": st.tuples(AMOUNT, AMOUNT, AMOUNT), + "pre_tax": st.tuples(AMOUNT, AMOUNT, AMOUNT), + "above_line": AMOUNT, + "dependent_above_line": AMOUNT, + "medical": AMOUNT, + "standard": AMOUNT, + "itemized": AMOUNT, + "mandatory": st.booleans(), + } +) + + +@settings(max_examples=20, deadline=None) +@given(rows=st.lists(ROW, min_size=2, max_size=5)) +@example( + rows=[ + { + "premiums": (1_000, 500, 1_000), + "pre_tax": (0, 0, 0), + "above_line": 0, + "dependent_above_line": 900, + "medical": 500, + "standard": 10_000, + "itemized": 10_500, + "mandatory": False, + }, + { + "premiums": (0, 0, 500), + "pre_tax": (0, 0, 0), + "above_line": 0, + "dependent_above_line": 0, + "medical": 0, + "standard": 10_000, + "itemized": 1_000, + "mandatory": True, + }, + ] +) +@example( + rows=[ + { + "premiums": (1_000, 0, 500), + "pre_tax": (0, 0, 0), + "above_line": 0, + "dependent_above_line": 0, + "medical": 0, + "standard": 16_100, + "itemized": 0, + "mandatory": False, + }, + { + "premiums": (1_000, 0, 500), + "pre_tax": (0, 0, 0), + "above_line": 0, + "dependent_above_line": 500, + "medical": 0, + "standard": 16_100, + "itemized": 0, + "mandatory": False, + }, + { + "premiums": (1_000, 400, 500), + "pre_tax": (100, 50, 500), + "above_line": 150, + "dependent_above_line": 500, + "medical": 0, + "standard": 16_100, + "itemized": 0, + "mandatory": False, + }, + ] +) +def test_premium_subtraction_and_election_match_best_legal_route(rows): + people = {} + tax_units = {} + households = {} + expected_itemizes = [] + expected_subtractions = [] + expected_reductions = [] + available_premiums = [] + for index, row in enumerate(rows): + members = [f"person_{index}_{member}" for member in range(3)] + for member, person_id in enumerate(members): + people[person_id] = { + "age": {PERIOD: (40, 39, 20)[member]}, + "is_tax_unit_head": {PERIOD: member == 0}, + "is_tax_unit_spouse": {PERIOD: member == 1}, + "is_tax_unit_dependent": {PERIOD: member == 2}, + "health_insurance_premiums": {PERIOD: row["premiums"][member]}, + "pre_tax_health_insurance_premiums": {PERIOD: row["pre_tax"][member]}, + } + tax_units[f"tax_unit_{index}"] = { + "members": members, + "self_employed_health_insurance_ald": {PERIOD: row["above_line"]}, + "dependents_self_employed_health_insurance_ald": { + PERIOD: row["dependent_above_line"] + }, + "medical_expense_deduction": {PERIOD: row["medical"]}, + "standard_deduction": {PERIOD: row["standard"]}, + "id_itemized_deductions": {PERIOD: row["itemized"]}, + "separate_filer_itemizes": {PERIOD: row["mandatory"]}, + } + households[f"household_{index}"] = { + "members": members, + "state_code": {PERIOD: "ID"}, + } + + # Enumerate the standard and itemized routes using scalar arithmetic. + # Their combined reduction, rather than deduction alone, determines + # the favorable election; a spouse's itemization overrides that choice. + # Premium inputs identify the payer: only the head and spouse's + # payments enter this return. The dependent's payments and deductions + # belong on the dependent's own return. Pretax payroll premiums are + # a separate leaf input and never reduce these after-tax payments. + # The explicit pretax example has 1,000 + 400 - 150 = 1,250 available, + # irrespective of its separate 100 + 50 = 150 pretax premiums. + # Its standard route retains 1,250 and reduces income by + # 16,100 + 1,250 = 17,350, exceeding itemized 0 + 1,250 = 1,250. + available = max( + 0, + sum(row["premiums"][:2]) - row["above_line"], + ) + unclaimed_if_itemizing = max(0, available - row["medical"]) + standard_route = (row["standard"] + available, False, available) + itemized_route = ( + row["itemized"] + unclaimed_if_itemizing, + True, + unclaimed_if_itemizing, + ) + elected = ( + itemized_route + if row["mandatory"] + else max((standard_route, itemized_route), key=lambda route: route[0]) + ) + expected_reductions.append(elected[0]) + expected_itemizes.append(elected[1]) + expected_subtractions.append(elected[2]) + available_premiums.append(available) + + simulation = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + }, + ) + subtraction = simulation.calculate( + "id_health_insurance_premiums_subtraction", PERIOD + ) + deductions = simulation.calculate("id_deductions", PERIOD) + + assert simulation.calculate( + "id_qualified_health_insurance_premiums", PERIOD + ).tolist() == pytest.approx(available_premiums) + assert simulation.calculate("id_itemizes", PERIOD).tolist() == expected_itemizes + assert subtraction.tolist() == pytest.approx(expected_subtractions) + assert (deductions + subtraction).tolist() == pytest.approx(expected_reductions) + assert all( + 0 <= actual <= available + for actual, available in zip(subtraction, available_premiums) + ) + + +def test_actual_medical_deduction_is_scoped_to_period_and_branch(): + years = (2025, 2026) + simulation = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": { + "head": { + "age": {year: 40 for year in years}, + "health_insurance_premiums": {year: 3_000 for year in years}, + } + }, + "tax_units": { + "tax_unit": { + "members": ["head"], + "adjusted_gross_income": {year: 30_000 for year in years}, + "medical_expense_deduction": {2026: 3_000}, + } + }, + "households": { + "household": { + "members": ["head"], + "state_code": {year: "ID" for year in years}, + } + }, + }, + ) + # Create branches before calculating the helper so they inherit no + # previously calculated Idaho value. A sibling's input must not change + # the parent's derived claim, but a nested branch inherits that input. + actual_claim = simulation.get_branch("actual_claim") + actual_claim.set_input("medical_expense_deduction", 2025, np.array([3_000])) + nested_claim = actual_claim.get_branch("nested_claim") + deleted_claim = simulation.get_branch("deleted_claim") + deleted_claim.set_input("medical_expense_deduction", 2025, np.array([3_000])) + deleted_claim.delete_arrays("medical_expense_deduction", 2025) + + variable = "id_health_insurance_premiums_medical_deduction" + assert simulation.calculate(variable, 2025).tolist() == [750] + assert simulation.calculate(variable, 2026).tolist() == [3_000] + assert actual_claim.calculate(variable, 2025).tolist() == [3_000] + assert nested_claim.calculate(variable, 2025).tolist() == [3_000] + # Check before federal recalculation, while the deleted input's array is + # absent. Core versions before 3.32.27 retain its input key, so recalculating + # first triggers the provenance defect documented in the PR's limitation. + assert deleted_claim.calculate(variable, 2025).tolist() == [750] + + +@pytest.mark.xfail( + condition=Version(version("policyengine-core")) < Version("3.32.27"), + strict=True, + reason=( + "policyengine-core<3.32.27 retains deleted input provenance and " + "misclassifies a federal medical deduction recalculated before the " + "Idaho helper as supplied; fixed by policyengine-core#561." + ), +) +def test_deleted_medical_input_recalculated_federal_first(): + year = 2025 + simulation = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": { + "head": { + "age": {year: 40}, + "is_tax_unit_head": {year: True}, + "health_insurance_premiums": {year: 3_000}, + }, + "dependent": { + "age": {year: 20}, + "is_tax_unit_dependent": {year: True}, + "health_insurance_premiums": {year: 500}, + }, + }, + "tax_units": { + "tax_unit": { + "members": ["head", "dependent"], + "adjusted_gross_income": {year: 30_000}, + "standard_deduction": {year: 15_750}, + "id_itemized_deductions": {year: 20_000}, + } + }, + "households": { + "household": { + "members": ["head", "dependent"], + "state_code": {year: "ID"}, + } + }, + }, + ) + simulation.set_input("medical_expense_deduction", year, np.array([3_000])) + simulation.delete_arrays("medical_expense_deduction", year) + + # Federal first: 3,000 + 500 - 30,000 * 7.5% = 1,250. + assert simulation.calculate("medical_expense_deduction", year).tolist() == [1_250] + # Idaho excludes the dependent's payment: medical overlap is + # 3,000 - 30,000 * 7.5% = 750, leaving 3,000 - 750 = 2,250. + # Core's provenance defect before 3.32.27 uses 1,250 and returns 1,750. + assert simulation.calculate( + "id_health_insurance_premiums_subtraction", year + ).tolist() == [2_250] diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml index 8528e9d221c..e70f41c30d1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml @@ -7,6 +7,53 @@ output: id_deductions: 1_000 +- name: Standard deduction plus premiums beats a larger itemized deduction + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + medical_expense_deduction: 2_000 + standard_deduction: 16_100 + id_itemized_deductions: 17_000 + output: + id_deductions: 16_100 + id_health_insurance_premiums_subtraction: 6_000 + +- name: Combined reduction tie retains the standard deduction + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + medical_expense_deduction: 2_000 + standard_deduction: 16_100 + id_itemized_deductions: 18_100 + output: + id_deductions: 16_100 + id_health_insurance_premiums_subtraction: 6_000 + +- name: A larger total reduction permits itemization + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + medical_expense_deduction: 2_000 + standard_deduction: 16_100 + id_itemized_deductions: 18_101 + output: + id_deductions: 18_101 + id_health_insurance_premiums_subtraction: 4_000 + +- name: Separate filers must itemize even without premiums + period: 2026 + input: + state_code: ID + separate_filer_itemizes: true + health_insurance_premiums: 0 + standard_deduction: 1_000 + id_itemized_deductions: 500 + output: + id_deductions: 500 + - name: Does not itemize period: 2023 input: @@ -15,3 +62,36 @@ state_code: ID output: id_deductions: 1_000 + +- name: Claimant medical overlap determines the combined deduction election + # Form 39R worksheet: claimant medical deduction is $750, not $1,250. + # Itemized route: $16,750 + $2,250 = $19,000. + # Standard route: $15,750 + $3,000 = $18,750. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + standard_deduction: 15_750 + id_itemized_deductions: 16_750 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 1_250 + id_health_insurance_premiums_medical_deduction: 750 + id_itemizes: true + id_deductions: 16_750 + id_health_insurance_premiums_subtraction: 2_250 + id_taxable_income: 11_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_income_tax_before_refundable_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_income_tax_before_refundable_credits.yaml index bb41c382522..d9e70d30606 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_income_tax_before_refundable_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_income_tax_before_refundable_credits.yaml @@ -15,3 +15,109 @@ id_non_refundable_credits: 500 output: id_income_tax_before_refundable_credits: 0 + +# Audited PolicyBench households, using their supplied person-level facts. +- name: PolicyBench scenario_007 Idaho paid health insurance premiums + absolute_error_margin: 0.01 + period: 2026 + input: + people: + head: + age: 56 + employment_income: 0.0 + bank_account_assets: 71500.0 + employer_sponsored_insurance_premiums: 6309.27587890625 + has_esi: true + health_insurance_premiums_without_medicare_part_b: 2080.0 + hours_worked_last_week: 50.0 + is_tax_unit_head: true + is_tax_unit_spouse: false + other_health_insurance_premiums: 2080.0 + other_medical_expenses: 150.0 + pre_subsidy_rent: 18352.94140625 + social_security_disability: 19285.0 + tax_exempt_private_pension_income: 284.1176452636719 + taxable_private_pension_income: 35280.0 + weekly_hours_worked_before_lsr: 50.0 + takes_up_medicaid_if_eligible: true + takes_up_ssi_if_eligible: true + tax_units: + tax_unit: + members: &id001 + - head + takes_up_aca_if_eligible: true + takes_up_dc_ptc: true + takes_up_eitc: true + would_file_if_eligible_for_refundable_credit: true + would_file_taxes_voluntarily: true + spm_units: + spm_unit: + members: *id001 + takes_up_snap_if_eligible: true + families: + family: + members: *id001 + marital_units: + marital_unit: + members: *id001 + households: + household: + members: *id001 + state_code: ID + household_vehicles_value: 2660.0 + output: + id_income_tax_before_refundable_credits: 645.54 + state_income_tax_before_refundable_credits: 655.54 # Includes the $10 PBF tax. +- name: PolicyBench scenario_053 Idaho paid health insurance premiums + absolute_error_margin: 0.01 + period: 2026 + input: + people: + head: + age: 25 + employment_income: 66968.6796875 + bank_account_assets: 3660.0 + employer_sponsored_insurance_premiums: 3389.27587890625 + has_esi: true + health_insurance_premiums_without_medicare_part_b: 5000.0 + hourly_wage: 32.0 + hours_worked_last_week: 40.0 + is_paid_hourly: true + is_tax_unit_head: true + is_tax_unit_spouse: false + other_health_insurance_premiums: 5000.0 + other_medical_expenses: 400.0 + over_the_counter_health_expenses: 150.0 + pre_subsidy_rent: 37200.0 + weekly_hours_worked_before_lsr: 40.0 + takes_up_medicaid_if_eligible: true + takes_up_ssi_if_eligible: true + tax_units: + tax_unit: + members: &id002 + - head + takes_up_aca_if_eligible: true + takes_up_dc_ptc: true + takes_up_eitc: true + would_file_if_eligible_for_refundable_credit: true + would_file_taxes_voluntarily: true + spm_units: + spm_unit: + members: *id002 + takes_up_snap_if_eligible: true + families: + family: + members: *id002 + marital_units: + marital_unit: + members: *id002 + households: + household: + members: *id002 + state_code: ID + auto_loan_balance: 19000.0 + auto_loan_interest: 950.0 + household_vehicles_value: 7410.0 + output: + id_income_tax_before_refundable_credits: 2170.28 + state_income_tax_before_refundable_credits: 2180.28 # Includes the $10 PBF tax. diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml index b40cca82519..e931e17dbdd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml @@ -18,6 +18,76 @@ output: id_itemized_deductions: 0 +- name: Derived Idaho itemized deductions exclude independently paid dependent medical costs + # 2025 Form 39R worksheet: claimant medical deduction is $750. + # $20,000 interest + $750 medical + $2,250 premiums = $23,000 reduction. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + interest_deduction: 20_000 + salt_deduction: 0 + id_salt_deduction: 0 + standard_deduction: 15_750 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 1_250 + id_health_insurance_premiums_medical_deduction: 750 + itemized_taxable_income_deductions: 21_250 + id_itemized_deductions: 20_750 + id_itemizes: true + id_health_insurance_premiums_subtraction: 2_250 + id_taxable_income: 7_000 + +- name: Corrected Idaho medical costs also reduce the 2026 itemized limitation + # P.L. 119-21 sec. 70111: $70,000 * (1 - 2/37) = $66,216.21622. + # Subtracting $500 from the already-limited federal total would be too low. + absolute_error_margin: 0.01 + period: 2026 + input: + people: + head: + age: 40 + health_insurance_premiums: 100_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + adjusted_gross_income: 800_000 + interest_deduction: 30_000 + salt_deduction: 0 + id_salt_deduction: 0 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 40_500 + id_health_insurance_premiums_medical_deduction: 40_000 + itemized_taxable_income_deductions: 66_689.18919 + id_itemized_deductions: 66_216.21622 + id_itemizes: true + id_health_insurance_premiums_subtraction: 60_000 + - name: Idaho does not add back the federal foreign tax credit when it is claimed as a credit period: 2025 input: @@ -30,3 +100,72 @@ # the itemized deduction (already inside itemized_taxable_income_deductions), # not when it is claimed as a federal credit. id_itemized_deductions: 0 + +- name: A supplied final federal itemized total already excludes dependent-paid medical costs + # The actual $20,000 Schedule A total includes claimant medical expenses; + # subtracting the derived federal medical overstatement again is incorrect. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + itemized_taxable_income_deductions: 20_000 + id_salt_deduction: 0 + standard_deduction: 15_750 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 1_250 + id_health_insurance_premiums_medical_deduction: 750 + id_itemized_deductions: 20_000 + id_itemizes: true + id_health_insurance_premiums_subtraction: 2_250 + id_taxable_income: 7_750 + +- name: A supplied raw federal itemized total receives the 2026 limitation once + # The actual $70,000 already uses claimant medical expenses; applying the + # 2/37 limitation gives $66,216.21622 without a second medical adjustment. + absolute_error_margin: 0.01 + period: 2026 + input: + people: + head: + age: 40 + health_insurance_premiums: 100_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + adjusted_gross_income: 800_000 + interest_deduction: 30_000 + total_itemized_taxable_income_deductions: 70_000 + salt_deduction: 0 + id_salt_deduction: 0 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 40_500 + id_health_insurance_premiums_medical_deduction: 40_000 + itemized_taxable_income_deductions: 66_216.21622 + id_itemized_deductions: 66_216.21622 + id_itemizes: true + id_health_insurance_premiums_subtraction: 60_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/integration.yaml index a96995acc78..4c16b814021 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/integration.yaml @@ -265,4 +265,10 @@ additional_senior_deduction: 6_000 id_additional_senior_deduction: 6_000 id_qualified_business_income_and_federal_schedule_1a_deductions: 6_000 - id_taxable_income: 134_000 + # Idaho Code 63-3022P and 2025 Form 39R line 18 allow Medicare B premiums: + # https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/ + # https://tax.idaho.gov/wp-content/uploads/forms/EFO00088/EFO00088_03-02-2026.pdf#page=11 + # Modeled premiums: 2 * $185 * 12 = $4,440, below the $15,000 medical floor. + # Taxable income: $200,000 - $60,000 - $6,000 - $4,440 = $129,560. + id_health_insurance_premiums_subtraction: 4_440 + id_taxable_income: 129_560 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml new file mode 100644 index 00000000000..b7d9991ce35 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml @@ -0,0 +1,497 @@ +- name: Standard deduction retains all post-tax premiums + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + standard_deduction: 16_100 + id_itemized_deductions: 10_000 + medical_expense_deduction: 2_000 + output: + id_health_insurance_premiums_subtraction: 6_000 + +- name: Pretax premiums are a separate input and never enter the subtraction + # Qualified premiums: $6,000 - $1,000 Schedule 1 deduction = $5,000. + # The separate $2,000 pretax input has no effect; standard retains $5,000. + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 2_000 + self_employed_health_insurance_ald: 1_000 + standard_deduction: 16_100 + id_itemized_deductions: 0 + output: + id_health_insurance_premiums_subtraction: 5_000 + +- name: Itemizers allocate medical deductions to health premiums first + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + self_employed_health_insurance_ald: 1_000 + medical_expense_deduction: 2_000 + standard_deduction: 16_100 + id_itemized_deductions: 20_000 + output: + id_health_insurance_premiums_subtraction: 3_000 + +- name: Medical deduction exceeding available premiums leaves no subtraction + period: 2026 + input: + state_code: ID + health_insurance_premiums: 6_000 + medical_expense_deduction: 10_000 + standard_deduction: 16_100 + id_itemized_deductions: 25_000 + output: + id_health_insurance_premiums_subtraction: 0 + +- name: Excess federal deductions cannot create a negative subtraction + period: 2026 + input: + state_code: ID + health_insurance_premiums: 1_000 + self_employed_health_insurance_ald: 2_000 + output: + id_health_insurance_premiums_subtraction: 0 + +- name: Reported non-Medicare premiums and voluntary Medicare B premiums qualify + period: 2025 + input: + state_code: ID + health_insurance_premiums_without_medicare_part_b: 2_000 + medicare_enrolled: true + medicare_part_b_premium: 1_000 + standard_deduction: 15_750 + id_itemized_deductions: 0 + output: + id_health_insurance_premiums_subtraction: 3_000 + +- name: Medicare-enrolled joint filers qualify for modeled Part B premiums without premium inputs + # Idaho Code 63-3022P; 2025 Form 39R line 18 and worksheet: + # https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/ + # https://tax.idaho.gov/wp-content/uploads/forms/EFO00088/EFO00088_03-02-2026.pdf#page=11 + # Each filer pays the modeled $185 monthly premium: 2 * $185 * 12 = $4,440. + # The $200,000 * 7.5% = $15,000 floor leaves no Schedule A medical overlap. + # Taxable income: $200,000 - $60,000 itemized - $6,000 senior - $4,440. + period: 2025 + input: + people: + head: + age: 70 + taxable_interest_income: 100_000 + medicare_enrolled: true + is_tax_unit_head: true + is_tax_unit_head_or_spouse: true + ssn_card_type: CITIZEN + spouse: + age: 70 + taxable_interest_income: 100_000 + medicare_enrolled: true + is_tax_unit_spouse: true + is_tax_unit_head_or_spouse: true + ssn_card_type: CITIZEN + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + itemized_taxable_income_deductions: 60_000 + id_salt_deduction: 0 + households: + household: + members: [head, spouse] + state_code: ID + output: + medicare_part_b_premium: [2_220, 2_220] + medical_expense_health_insurance_premiums: [2_220, 2_220] + medical_expense_deduction: 0 + id_qualified_health_insurance_premiums: 4_440 + id_health_insurance_premiums_medical_deduction: 0 + id_itemizes: true + id_health_insurance_premiums_subtraction: 4_440 + id_taxable_income: 129_560 + +- name: Premium subtraction is included in the existing 2021 subtraction list + period: 2021 + input: + state_code: ID + health_insurance_premiums: 2_000 + standard_deduction: 12_550 + id_itemized_deductions: 0 + output: + id_health_insurance_premiums_subtraction: 2_000 + id_subtractions: 2_000 + +- name: Subtraction is limited to Idaho + period: 2026 + input: + state_code: WA + health_insurance_premiums: 2_000 + output: + id_health_insurance_premiums_subtraction: 0 + +- name: Premiums paid by a dependent do not qualify on the parent's return + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 1_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + standard_deduction: 15_750 + id_itemized_deductions: 0 + medical_expense_deduction: 0 + households: + household: + members: [head, dependent] + state_code: ID + output: + id_qualified_health_insurance_premiums: 1_000 + id_health_insurance_premiums_subtraction: 1_000 + +- name: Both filers' premiums qualify independently of separate pretax payments + # Qualified premiums: $1,000 head + $2,000 spouse = $3,000. + # The dependent's $500 payment is excluded; both pretax inputs are separate. + # Standard deduction retains the full $3,000 subtraction. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 1_000 + is_tax_unit_head: true + spouse: + age: 40 + health_insurance_premiums: 2_000 + pre_tax_health_insurance_premiums: 500 + is_tax_unit_spouse: true + dependent: + age: 20 + health_insurance_premiums: 500 + pre_tax_health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, dependent] + standard_deduction: 31_500 + id_itemized_deductions: 0 + medical_expense_deduction: 0 + households: + household: + members: [head, spouse, dependent] + state_code: ID + output: + id_qualified_health_insurance_premiums: 3_000 + id_health_insurance_premiums_subtraction: 3_000 + +- name: A dependent's federal deduction cannot reduce the filers' own premiums + period: 2026 + input: + people: + head: + age: 40 + health_insurance_premiums: 1_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + self_employment_income: 5_000 + is_self_employed: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + standard_deduction: 16_100 + id_itemized_deductions: 0 + households: + household: + members: [head, dependent] + state_code: ID + output: + dependents_self_employed_health_insurance_ald: 500 + id_health_insurance_premiums_subtraction: 1_000 + +- name: Dependent-paid premiums cannot reduce the parent's itemized subtraction + # Idaho Code 63-3022P; 2025 Form 39R worksheet, PDF page 49, + # lines 1-6 and 8: $3,000 claimant costs - $2,250 floor = $750 overlap. + # Leave medical_expense_deduction derived to reproduce review r2's P2. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 1_250 + id_health_insurance_premiums_medical_deduction: 750 + id_itemizes: true + id_health_insurance_premiums_subtraction: 2_250 + id_taxable_income: 7_750 + +- name: Dependent payments cannot push claimant medical expenses above the floor + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 2_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 250 + id_health_insurance_premiums_medical_deduction: 0 + id_itemizes: true + id_health_insurance_premiums_subtraction: 2_000 + +- name: Claimant medical overlap includes spouse expenses and excludes dependent expenses + # Claimants paid $5,000 premiums + $2,000 other costs; the floor is $6,000. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + other_medical_expenses: 1_000 + is_tax_unit_head: true + spouse: + age: 40 + health_insurance_premiums: 2_000 + other_medical_expenses: 1_000 + is_tax_unit_spouse: true + dependent: + age: 20 + health_insurance_premiums: 500 + other_medical_expenses: 4_000 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, dependent] + adjusted_gross_income: 80_000 + standard_deduction: 31_500 + id_itemized_deductions: 40_000 + households: + household: + members: [head, spouse, dependent] + state_code: ID + output: + medical_expense_deduction: 5_500 + id_health_insurance_premiums_medical_deduction: 1_000 + id_itemizes: true + id_health_insurance_premiums_subtraction: 4_000 + +- name: Derived claimant medical overlap uses premiums excluding pretax payroll payments + # Pretax payroll premiums are separate; remove only the Schedule 1 amount. + # Qualified premiums: $6,000 - $1,000 = $5,000; floor: $30,000 * 7.5% = $2,250. + # Federal medical: $6,000 + $500 - $2,250 = $4,250 (unchanged). + # Claimant overlap: min($4,250, $5,000 + $500 - $2,250) = $3,250. + # Itemized route: $20,000 + ($5,000 - $3,250) = $21,750, exceeding + # standard route $15,750 + $5,000 = $20,750; subtraction remains $1,750. + period: 2025 + input: + state_code: ID + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 2_000 + self_employed_health_insurance_ald: 1_000 + other_medical_expenses: 500 + adjusted_gross_income: 30_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + output: + medical_expense_deduction: 4_250 + id_qualified_health_insurance_premiums: 5_000 + id_health_insurance_premiums_medical_deduction: 3_250 + id_itemizes: true + id_health_insurance_premiums_subtraction: 1_750 + +- name: A smaller actual federal medical deduction limits claimant overlap + period: 2025 + input: + state_code: ID + health_insurance_premiums: 3_000 + adjusted_gross_income: 30_000 + medical_expense_deduction: 500 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + output: + id_health_insurance_premiums_medical_deduction: 500 + id_health_insurance_premiums_subtraction: 2_500 + +- name: An actual claimant medical deduction can include otherwise unreported expenses + # An explicit Idaho claimant deduction can override the supplied federal + # amount when the caller has a different state-specific medical claim. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + medical_expense_deduction: 4_000 + id_health_insurance_premiums_medical_deduction: 3_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + households: + household: + members: [head, dependent] + state_code: ID + output: + id_health_insurance_premiums_medical_deduction: 3_000 + id_health_insurance_premiums_subtraction: 0 + +- name: A supplied Schedule A medical deduction includes unreported underlying expenses + # Form 39R worksheet lines 6 and 8 use the actual Schedule A deduction. + # The supplied $3,000 includes $2,250 of other bills omitted from inputs; + # reconstructing from premiums alone would incorrectly allow $2,250 twice. + period: 2025 + input: + state_code: ID + health_insurance_premiums: 3_000 + adjusted_gross_income: 30_000 + medical_expense_deduction: 3_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + output: + id_health_insurance_premiums_medical_deduction: 3_000 + id_itemizes: true + id_health_insurance_premiums_subtraction: 0 + id_taxable_income: 10_000 + +- name: A supplied medical deduction for another year cannot override claimant scope + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums: 3_000 + is_tax_unit_head: true + dependent: + age: 20 + health_insurance_premiums: 500 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent] + adjusted_gross_income: 30_000 + medical_expense_deduction: + 2026: 3_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + households: + household: + members: [head, dependent] + state_code: ID + output: + medical_expense_deduction: 1_250 + id_health_insurance_premiums_medical_deduction: 750 + id_health_insurance_premiums_subtraction: 2_250 + +- name: A supplied Schedule A expense subtotal includes unreported claimant bills + # Review r3's P2; Form 39R worksheet, PDF page 49, lines 1-6 and 8. + # $6,000 claimant expenses - $2,250 floor = $3,750 medical deduction, + # exhausting the $3,000 premiums even without person-level other bills. + period: 2025 + input: + state_code: ID + health_insurance_premiums: 3_000 + adjusted_gross_income: 30_000 + itemized_medical_expenses: 6_000 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + output: + medical_expense_deduction: 3_750 + id_health_insurance_premiums_medical_deduction: 3_750 + id_itemizes: true + id_health_insurance_premiums_subtraction: 0 + id_taxable_income: 10_000 + +- name: A supplied Schedule A expense subtotal already excludes disallowed premiums + # The $6,000 premium input already excludes separate pretax payroll premiums. + # Qualified premiums: $6,000 - $1,000 Schedule 1 deduction = $5,000. + # Supplied expenses: $5,000 + $500 unreported other bills = $5,500. + # Federal and claimant medical: $5,500 - $30,000 * 7.5% = $3,250. + # Itemized route: $20,000 + ($5,000 - $3,250) = $21,750, exceeding + # standard route $15,750 + $5,000 = $20,750; subtraction remains $1,750. + period: 2025 + input: + state_code: ID + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 2_000 + self_employed_health_insurance_ald: 1_000 + adjusted_gross_income: 30_000 + itemized_medical_expenses: 5_500 + standard_deduction: 15_750 + id_itemized_deductions: 20_000 + output: + medical_expense_deduction: 3_250 + id_qualified_health_insurance_premiums: 5_000 + id_health_insurance_premiums_medical_deduction: 3_250 + id_itemizes: true + id_health_insurance_premiums_subtraction: 1_750 + +- name: Identical households have identical Idaho outputs with or without pretax premiums + # Each household has $6,000 premiums excluding pretax payroll payments. + # Qualified premiums: $6,000 - $1,000 Schedule 1 deduction = $5,000. + # Claimant overlap: $5,000 + $500 - $30,000 * 7.5% = $3,250. + # Itemized route: $20,000 + ($5,000 - $3,250) = $21,750, exceeding + # standard route $15,750 + $5,000 = $20,750; subtraction is $1,750. + # Deductions: $20,000; taxable income: $30,000 - $20,000 - $1,750 = $8,250. + period: 2025 + input: + state_code: [ID, ID] + health_insurance_premiums: [6_000, 6_000] + pre_tax_health_insurance_premiums: [0, 2_000] + self_employed_health_insurance_ald: [1_000, 1_000] + other_medical_expenses: [500, 500] + adjusted_gross_income: [30_000, 30_000] + standard_deduction: [15_750, 15_750] + id_itemized_deductions: [20_000, 20_000] + output: + id_qualified_health_insurance_premiums: [5_000, 5_000] + id_health_insurance_premiums_medical_deduction: [3_250, 3_250] + id_itemizes: [true, true] + id_health_insurance_premiums_subtraction: [1_750, 1_750] + id_deductions: [20_000, 20_000] + id_taxable_income: [8_250, 8_250] diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index 3762e1dc1e2..5e9b7accd28 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -1,4 +1,5 @@ from policyengine_core.model_api import * +from policyengine_core.periods import period as to_period from policyengine_core.projectors import Projector from policyengine_us.entities import * from policyengine_us.tools.branched_simulation import BranchedSimulation @@ -13,6 +14,31 @@ USD = "currency-USD" +def has_input_for_period(simulation, variable, period): + """Whether the visible array for this exact period was set as an input. + + A nearer branch's stored value shadows its ancestors. Inputs from sibling + branches or other periods do not apply. Only finite definition periods + are supported. + + Core versions before 3.32.27 can retain an input key after deletion. If + that value has already been recalculated, this read-only check cannot + distinguish it from a supplied value. Core 3.32.27 fixes this in #561. + """ + period = to_period(period) + holder = simulation.get_holder(variable) + if holder.variable.definition_period == ETERNITY: + raise ValueError("has_input_for_period does not support eternal variables.") + if holder.variable.is_neutralized: + return False + stored_keys = set(holder.get_known_branch_periods()) + input_keys = getattr(simulation, "_user_input_keys", set()) + for branch_name in simulation._get_visible_branch_names(): + if (branch_name, period) in stored_keys: + return (variable, branch_name, period) in input_keys + return False + + def tax_unit_non_dep_sum(var, tax_unit, period): return tax_unit.sum( tax_unit.members(var, period) diff --git a/policyengine_us/variables/gov/states/id/tax/income/deductions/id_deductions.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/id_deductions.py index d10534624fe..62450596743 100644 --- a/policyengine_us/variables/gov/states/id/tax/income/deductions/id_deductions.py +++ b/policyengine_us/variables/gov/states/id/tax/income/deductions/id_deductions.py @@ -9,13 +9,14 @@ class id_deductions(Variable): definition_period = YEAR reference = ( "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022/", # (j) - "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-01-2023.pdf#page=8", + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=10", ) defined_for = StateCode.ID def formula(tax_unit, period, parameters): itm_ded = tax_unit("id_itemized_deductions", period) std_ded = tax_unit("standard_deduction", period) - # Idaho qualified business income and federal Schedule 1-A deductions - # are modeled separately. - return max_(itm_ded, std_ded) + itemizes = tax_unit("id_itemizes", period) + # The election accounts for the associated health premium subtraction. + # Qualified business income and Schedule 1-A deductions are separate. + return where(itemizes, itm_ded, std_ded) diff --git a/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemized_deductions.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemized_deductions.py index 9d51f1abe43..b406cb6a7ed 100644 --- a/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemized_deductions.py +++ b/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemized_deductions.py @@ -12,10 +12,19 @@ class id_itemized_deductions(Variable): "https://tax.idaho.gov/wp-content/uploads/forms/EFO00089/EFO00089_09-23-2021.pdf", "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022/", # Idaho Code 63-3022 (subtractions/itemized framework) "https://tax.idaho.gov/wp-content/uploads/forms/EFO00088/EFO00088_03-02-2026.pdf#page=13", # Idaho Form 39R - Additions and Subtractions instructions (foreign-tax addback rule) + "https://www.govinfo.gov/content/pkg/PLAW-119publ21/html/PLAW-119publ21.htm", # P.L. 119-21, sec. 70111 (IRC 68, from 2026) ) defined_for = StateCode.ID def formula(tax_unit, period, parameters): + # Supplied Schedule A totals already represent the claimant's + # actual deductions, even if component expenses are incomplete. + # Check their provenance before calculating either aggregate. + supplied_itemized = has_input_for_period( + tax_unit.simulation, "itemized_taxable_income_deductions", period + ) or has_input_for_period( + tax_unit.simulation, "total_itemized_taxable_income_deductions", period + ) # Idaho reduces the federal itemized deductions by the SALT deduction. # When foreign tax is claimed as a federal itemized deduction, it is # already in itemized_taxable_income_deductions; when claimed as a @@ -26,4 +35,36 @@ def formula(tax_unit, period, parameters): # are consistent with no unconditional addback here. id_salt_ded = tax_unit("id_salt_deduction", period) itemized_ded = tax_unit("itemized_taxable_income_deductions", period) - return max_(itemized_ded - id_salt_ded, 0) + if supplied_itemized: + return max_(0, itemized_ded - id_salt_ded) + federal_medical = tax_unit("medical_expense_deduction", period) + claimant_medical = tax_unit( + "id_health_insurance_premiums_medical_deduction", period + ) + medical_adjustment = federal_medical - claimant_medical + + # Correct the medical component of derived itemized totals as well as + # premium overlap, so dependent-paid costs do not remain deducted here. + limitation_adjustment = 0 + p = parameters(period).gov.irs.deductions.itemized.limitation + if p.applies and p.obbb.applies: + # IRC 68 applies after the medical deduction is limited. Removing + # invalid medical costs can also reduce the 2/37 limitation. + total = tax_unit("total_itemized_taxable_income_deductions", period) + corrected_total = max_(0, total - medical_adjustment) + filing_status = tax_unit("filing_status", period) + threshold = parameters(period).gov.irs.income.bracket.thresholds["6"][ + filing_status + ] + agi = tax_unit("adjusted_gross_income", period) + exemptions = tax_unit("exemptions", period) + excess = max_(0, agi - exemptions - threshold) + limitation_adjustment = p.obbb.rate * ( + min_(total, excess) - min_(corrected_total, excess) + ) + # The legacy Pease ceiling excludes medical deductions, so its + # reduction does not change when this medical component is corrected. + return max_( + 0, + itemized_ded - medical_adjustment + limitation_adjustment - id_salt_ded, + ) diff --git a/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemizes.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemizes.py new file mode 100644 index 00000000000..0b719d1bf76 --- /dev/null +++ b/policyengine_us/variables/gov/states/id/tax/income/deductions/id_itemizes.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * + + +class id_itemizes(Variable): + value_type = bool + entity = TaxUnit + label = "Tax unit itemizes deductions for Idaho" + definition_period = YEAR + defined_for = StateCode.ID + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022/", # (j) + "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/", + # PDF pages 10, 48-49 (Form 40 election and Form 39R premiums). + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=10", + ) + documentation = ( + "Choose the larger combined Idaho deduction and health premium " + "subtraction. Itemization removes health premiums already included " + "in the medical deduction; the standard election preserves them. " + "Ties retain the standard deduction, unless itemization is required." + ) + + def formula(tax_unit, period, parameters): + premiums = tax_unit("id_qualified_health_insurance_premiums", period) + medical = tax_unit("id_health_insurance_premiums_medical_deduction", period) + # Form 39R allocates itemized medical deductions to health premiums first. + overlap = min_(premiums, medical) + itemized = tax_unit("id_itemized_deductions", period) + standard = tax_unit("standard_deduction", period) + mandatory = tax_unit("separate_filer_itemizes", period) + return mandatory | (itemized > standard + overlap) diff --git a/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_medical_deduction.py b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_medical_deduction.py new file mode 100644 index 00000000000..04860677ccf --- /dev/null +++ b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_medical_deduction.py @@ -0,0 +1,56 @@ +from policyengine_us.model_api import * + + +class id_health_insurance_premiums_medical_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Claimant medical deduction for Idaho health premium subtraction" + unit = USD + definition_period = YEAR + defined_for = StateCode.ID + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/", + # PDF pages 48-49 (Form 39R, line 18 and worksheet lines 1-8). + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=48", + "https://www.irs.gov/instructions/i1040sca", + ) + documentation = ( + "Medical deduction used to allocate the claimant's health premium " + "overlap. Only costs paid by the head and spouse enter the medical " + "floor calculation, including their payments covering dependents. " + "Premium inputs already exclude pretax payroll premiums; premiums " + "deducted on Schedule 1 are removed first. The " + "actual federal medical deduction limits the derived result. A " + "federal medical deduction or expense subtotal supplied for this " + "period represents the claimant's actual Schedule A amounts and is " + "honored even when its underlying expenses are not represented in " + "person inputs. A supplied Schedule A expense subtotal already " + "excludes pretax premiums and premiums deducted on Schedule 1." + ) + + def formula(tax_unit, period, parameters): + # Actual Schedule A deductions and expense subtotals are authoritative. + # Pretax payroll premiums are separate from the premium inputs. + # Supplied subtotals already exclude Schedule 1 premiums, so do not + # remove those amounts again. Check the source before calculation: + # cached results and other periods or branches do not describe this claim. + supplied_medical = any( + has_input_for_period(tax_unit.simulation, variable, period) + for variable in ( + "medical_expense_deduction", + "itemized_medical_expenses", + ) + ) + federal_medical = max_(0, tax_unit("medical_expense_deduction", period)) + if supplied_medical: + return federal_medical + premiums = tax_unit("id_qualified_health_insurance_premiums", period) + other_medical = tax_unit_non_dep_add( + tax_unit, period, ["other_medical_expenses"] + ) + p = parameters(period).gov.irs.deductions.itemized.medical + medical_floor = p.floor * tax_unit("positive_agi", period) + # Form 39R lines 1-6 use costs claimed on this return, not payments + # independently made by dependents and aggregated by the federal model. + claimant_medical = max_(0, premiums + other_medical - medical_floor) + return min_(federal_medical, claimant_medical) diff --git a/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.py b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.py new file mode 100644 index 00000000000..fbe4fcebf28 --- /dev/null +++ b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class id_health_insurance_premiums_subtraction(Variable): + value_type = float + entity = TaxUnit + label = "Idaho health insurance premiums subtraction" + unit = USD + definition_period = YEAR + defined_for = StateCode.ID + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/", + # PDF pages 48-49 (Form 39R, line 18 and its worksheet). + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=48", + ) + + def formula(tax_unit, period, parameters): + premiums = tax_unit("id_qualified_health_insurance_premiums", period) + medical = tax_unit("id_health_insurance_premiums_medical_deduction", period) + overlap = min_(premiums, medical) + itemizes = tax_unit("id_itemizes", period) + return premiums - where(itemizes, overlap, 0) diff --git a/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py new file mode 100644 index 00000000000..c523305214a --- /dev/null +++ b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class id_qualified_health_insurance_premiums(Variable): + value_type = float + entity = TaxUnit + label = "Idaho health insurance premiums not excluded or deducted elsewhere" + unit = USD + definition_period = YEAR + defined_for = StateCode.ID + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/Title63/T63CH30/SECT63-3022P/", + # PDF pages 48-49 (Form 39R, line 18 and its worksheet). + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=48", + ) + documentation = ( + "Premiums paid by the taxpayer and spouse for themselves and their " + "dependents, before removing the portion used in Idaho itemized " + "deductions. The premium inputs exclude premiums paid through " + "pretax payroll deductions. Those premiums are reported separately " + "in pre_tax_health_insurance_premiums, which never enters this " + "subtraction. " + "Self-employed premiums already deducted on the filers' federal " + "return are excluded. Dependents' own payments are excluded " + "regardless of their federal deduction." + ) + + def formula(tax_unit, period, parameters): + premiums = tax_unit_non_dep_add( + tax_unit, period, ["medical_expense_health_insurance_premiums"] + ) + # Dependents' federal deductions concern their excluded payments, + # so they must not reduce the filers' qualifying premiums again. + deducted_elsewhere = tax_unit("self_employed_health_insurance_ald", period) + return max_(0, premiums - deducted_elsewhere)