diff --git a/changelog.d/mortgage-atomic-stage3.removed.md b/changelog.d/mortgage-atomic-stage3.removed.md new file mode 100644 index 00000000000..f67c2d84d32 --- /dev/null +++ b/changelog.d/mortgage-atomic-stage3.removed.md @@ -0,0 +1 @@ +Removed the deprecated first_home_mortgage_interest and second_home_mortgage_interest inputs; the person-level home_mortgage_interest input is now the only mortgage-interest input, while the per-mortgage balance and origination-year inputs are unchanged. API callers that pass either removed input now receive an unknown-variable error and should supply home_mortgage_interest on the filer(s) instead. Datasets that still carry the removed columns load with policyengine-core's unknown-column warning, and any tax unit whose interest appeared only in those columns now computes zero mortgage interest; the certified default dataset has no such tax units. diff --git a/policyengine_us/tests/core/test_uprating_series.py b/policyengine_us/tests/core/test_uprating_series.py index fb7d4e7aec6..12b7649fbf2 100644 --- a/policyengine_us/tests/core/test_uprating_series.py +++ b/policyengine_us/tests/core/test_uprating_series.py @@ -69,9 +69,8 @@ # Benefits adjusted by the Social Security cost-of-living index. "veterans_benefits": COLA, # Consumption expenses, and the interest on debts, follow prices. The - # structured first and second mortgage inputs stay on the default: two are - # deprecated (issue #9275) and the file's formulas are outside the - # selective coverage job. + # per-mortgage balance and origination-year inputs stay on the default: + # the file's formulas are outside the selective coverage job. "pre_subsidy_rent": CPI_U, "pre_subsidy_care_expenses": CPI_U, "spm_unit_pre_subsidy_childcare_expenses": CPI_U, diff --git a/policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py b/policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py new file mode 100644 index 00000000000..209cad5d210 --- /dev/null +++ b/policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py @@ -0,0 +1,64 @@ +""" +Datasets built before issue #9275's stage 3 can still carry the removed +tax-unit ``first_home_mortgage_interest`` / ``second_home_mortgage_interest`` +columns. Loading one must name the ignored columns (policyengine-core's +unknown-column warning) and compute mortgage interest from the person-level +``home_mortgage_interest`` input alone. +""" + +import logging + +import pandas as pd + +from policyengine_us import Microsimulation +from policyengine_us.data.dataset_schema import USSingleYearDataset + + +def _dataset_with_removed_columns() -> USSingleYearDataset: + return USSingleYearDataset( + person=pd.DataFrame( + { + "person_id": [1, 2], + "age": [40.0, 40.0], + "employment_income": [50_000.0, 0.0], + "person_household_id": [1, 1], + "person_tax_unit_id": [1, 1], + "person_spm_unit_id": [1, 1], + "person_family_id": [1, 1], + "person_marital_unit_id": [1, 1], + "home_mortgage_interest": [900.0, 100.0], + } + ), + household=pd.DataFrame( + {"household_id": [1], "household_weight": [1.0], "state_fips": [6]} + ), + tax_unit=pd.DataFrame( + { + "tax_unit_id": [1], + # Deliberately different from the person-level total so the + # test proves the removed columns are ignored. + "first_home_mortgage_interest": [5_000.0], + "second_home_mortgage_interest": [2_000.0], + } + ), + spm_unit=pd.DataFrame({"spm_unit_id": [1]}), + family=pd.DataFrame({"family_id": [1]}), + marital_unit=pd.DataFrame({"marital_unit_id": [1]}), + time_period=2024, + ) + + +def test_removed_mortgage_interest_columns_warn_and_are_ignored(caplog): + with caplog.at_level(logging.WARNING): + sim = Microsimulation(dataset=_dataset_with_removed_columns()) + interest = sim.calculate("home_mortgage_interest_tax_unit", 2024) + + warnings = [ + record.getMessage() + for record in caplog.records + if "do not match any variable" in record.getMessage() + ] + assert len(warnings) == 1 + assert "first_home_mortgage_interest" in warnings[0] + assert "second_home_mortgage_interest" in warnings[0] + assert interest.values.tolist() == [1_000.0] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml index c437e3030f3..4e844efa46b 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml @@ -5,7 +5,7 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 900_000 - first_home_mortgage_interest: 45_000 + home_mortgage_interest: 45_000 first_home_mortgage_origination_year: 2017 output: deductible_mortgage_interest_tax_unit: 45_000 @@ -19,12 +19,14 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 900_000 - first_home_mortgage_interest: 45_000 + home_mortgage_interest: 45_000 first_home_mortgage_origination_year: 2018 output: deductible_mortgage_interest_tax_unit: 37_500 interest_deduction: 37_500 mortgage_interest: 45_000 + # 45,000 x (1 - 750,000 / 900,000) + non_deductible_mortgage_interest_tax_unit: 7_500 - name: Mixed-vintage mortgages use the combined statutory balance limit period: 2024 @@ -34,10 +36,9 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 800_000 - first_home_mortgage_interest: 40_000 + home_mortgage_interest: 55_000 first_home_mortgage_origination_year: 2017 second_home_mortgage_balance: 300_000 - second_home_mortgage_interest: 15_000 second_home_mortgage_origination_year: 2018 output: deductible_mortgage_interest_tax_unit: 40_000 @@ -51,14 +52,14 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 500_000 - first_home_mortgage_interest: 25_000 + home_mortgage_interest: 25_000 first_home_mortgage_origination_year: 2018 output: deductible_mortgage_interest_tax_unit: 18_750 interest_deduction: 18_750 mortgage_interest: 25_000 -- name: No structural mortgage inputs yields zero deductible interest +- name: No mortgage inputs yields zero deductible interest period: 2024 input: filing_status: SINGLE @@ -68,7 +69,7 @@ deductible_mortgage_interest_tax_unit: 0 non_deductible_mortgage_interest_tax_unit: 0 -- name: Person-level home mortgage interest is deductible when no structural inputs are provided +- name: Person-level home mortgage interest is the single interest input period: 2024 input: people: @@ -85,62 +86,96 @@ interest_deduction: 10_000 mortgage_interest: 10_000 -- name: Structural interest with no reported balance is fully deductible - period: 2024 - input: - filing_status: SINGLE - tax_unit_itemizes: true - standard_deduction: 0 - first_home_mortgage_interest: 10_000 - output: - deductible_mortgage_interest_tax_unit: 10_000 - -- name: Regression guard - post-TCJA balance above the cap is still limited - period: 2024 - input: - filing_status: SINGLE - tax_unit_itemizes: true - standard_deduction: 0 - first_home_mortgage_balance: 900_000 - first_home_mortgage_interest: 45_000 - first_home_mortgage_origination_year: 2018 - output: - deductible_mortgage_interest_tax_unit: 37_500 - -- name: Person-level input is canonical when both input styles are supplied (issue #9275) +- name: Joint filers' interest sums across spouses and is allocated back by reported shares period: 2024 input: people: head: - home_mortgage_interest: 12_000 + age: 45 + home_mortgage_interest: 30_000 + spouse: + age: 45 + home_mortgage_interest: 15_000 tax_units: tax_unit: - members: [head] - filing_status: SINGLE + members: [head, spouse] + filing_status: JOINT tax_unit_itemizes: true standard_deduction: 0 - first_home_mortgage_interest: 10_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2018 output: - # Person-level wins; the deprecated structured input is ignored when - # person-level interest is reported. - home_mortgage_interest_tax_unit: 12_000 - deductible_mortgage_interest_tax_unit: 12_000 - interest_deduction: 12_000 + home_mortgage_interest_tax_unit: 45_000 + # 45,000 x 750,000 / 900,000 = 37,500, split 2:1 by reported interest + deductible_mortgage_interest_tax_unit: 37_500 + non_deductible_mortgage_interest_tax_unit: 7_500 + deductible_mortgage_interest: [25_000, 12_500] + non_deductible_mortgage_interest: [5_000, 2_500] -- name: Person-level interest still passes through the acquisition-debt caps +- name: Interest reported only by a dependent is claimed by the filers in equal shares period: 2024 input: people: head: - home_mortgage_interest: 45_000 + age: 45 + spouse: + age: 45 + child: + age: 10 + home_mortgage_interest: 6_000 tax_units: tax_unit: - members: [head] - filing_status: SINGLE + members: [head, spouse, child] + filing_status: JOINT tax_unit_itemizes: true standard_deduction: 0 - first_home_mortgage_balance: 900_000 - first_home_mortgage_origination_year: 2018 output: - # Post-TCJA cap: 45,000 x 750,000 / 900,000 = 37,500 + home_mortgage_interest_tax_unit: 6_000 + deductible_mortgage_interest_tax_unit: 6_000 + deductible_mortgage_interest: [3_000, 3_000, 0] + +- name: Smaller grandfathered loan reduces the post-TCJA limit on a larger new loan + period: 2024 + input: + filing_status: JOINT + tax_unit_itemizes: true + standard_deduction: 0 + home_mortgage_interest: 45_000 + first_home_mortgage_balance: 200_000 + first_home_mortgage_origination_year: 2017 + second_home_mortgage_balance: 700_000 + second_home_mortgage_origination_year: 2018 + output: + # 26 USC 163(h)(3)(F)(iii): 200,000 grandfathered + min(700,000, 750,000 - 200,000) + # = 750,000 qualified of 900,000; 45,000 x 750 / 900 = 37,500 deductible_mortgage_interest_tax_unit: 37_500 + +- name: Married filing separately keeps the 500,000 grandfathered cap + period: 2024 + input: + filing_status: SEPARATE + tax_unit_itemizes: true + standard_deduction: 0 + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 2016 + output: + # 30,000 x 500,000 / 600,000 + deductible_mortgage_interest_tax_unit: 25_000 + non_deductible_mortgage_interest_tax_unit: 5_000 + +- name: Combined grandfathered balances above 1,000,000 are limited in total + period: 2024 + input: + filing_status: JOINT + tax_unit_itemizes: true + standard_deduction: 0 + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 800_000 + first_home_mortgage_origination_year: 2015 + second_home_mortgage_balance: 400_000 + second_home_mortgage_origination_year: 2016 + output: + # 60,000 x 1,000,000 / 1,200,000 + deductible_mortgage_interest_tax_unit: 50_000 + non_deductible_mortgage_interest_tax_unit: 10_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml index dc32eb9c434..e6030e9b0ab 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml @@ -237,7 +237,6 @@ - head - spouse first_home_mortgage_balance: 1000000.0 - first_home_mortgage_interest: 2743.0 takes_up_aca_if_eligible: true takes_up_dc_ptc: true takes_up_eitc: true @@ -587,7 +586,6 @@ members: &id007 - head first_home_mortgage_balance: 273000.0 - first_home_mortgage_interest: 7566.0 takes_up_aca_if_eligible: true takes_up_dc_ptc: true takes_up_eitc: true @@ -660,9 +658,7 @@ members: &id008 - head first_home_mortgage_balance: 794797.6875 - first_home_mortgage_interest: 2975.722412109375 second_home_mortgage_balance: 205202.3125 - second_home_mortgage_interest: 768.277587890625 takes_up_aca_if_eligible: true takes_up_dc_ptc: true takes_up_eitc: true diff --git a/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py b/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py index 7cb833e0b3f..3f567c3e097 100644 --- a/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py +++ b/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py @@ -7,9 +7,12 @@ class home_mortgage_interest_share(Variable): label = "Share of tax-unit home mortgage interest" definition_period = YEAR documentation = ( - "Allocates tax-unit mortgage interest across filers using reported " - "person-level home mortgage interest when available, otherwise evenly " - "across head and spouse." + "Allocates the tax unit's mortgage interest (and its deductible and " + "non-deductible parts) to the filers, who claim the deduction: in " + "proportion to the head's and spouse's own reported interest, or " + "evenly between them when only other members (such as dependents) " + "report interest. Shares sum to one across head and spouse, so the " + "person amounts always add back to the tax-unit totals." ) def formula(person, period, parameters): diff --git a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py index fed7ca75751..f161583577c 100644 --- a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py +++ b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py @@ -52,38 +52,6 @@ class second_home_mortgage_balance(Variable): ) -class first_home_mortgage_interest(Variable): - value_type = float - entity = TaxUnit - label = "First home mortgage interest" - unit = USD - definition_period = YEAR - default_value = 0 - documentation = ( - "DEPRECATED (issue #9275): use the person-level home_mortgage_interest " - "input instead; the deduction only ever uses the first+second sum, and " - "this input is read only when no person-level interest is reported. " - "Kept temporarily so existing datasets that supply it keep working; " - "removal is scheduled once certified microdata stops exporting it." - ) - - -class second_home_mortgage_interest(Variable): - value_type = float - entity = TaxUnit - label = "Second home mortgage interest" - unit = USD - definition_period = YEAR - default_value = 0 - documentation = ( - "DEPRECATED (issue #9275): use the person-level home_mortgage_interest " - "input instead; the deduction only ever uses the first+second sum, and " - "this input is read only when no person-level interest is reported. " - "Kept temporarily so existing datasets that supply it keep working; " - "removal is scheduled once certified microdata stops exporting it." - ) - - class first_home_mortgage_origination_year(Variable): value_type = int entity = TaxUnit @@ -111,20 +79,12 @@ class home_mortgage_interest_tax_unit(Variable): unit = USD definition_period = YEAR documentation = ( - "Total home mortgage interest. The person-level home_mortgage_interest " - "input is canonical; the deprecated structured first/second interest " - "inputs are used only when no person-level interest is reported " - "(existing datasets still supply them — see issue #9275)." + "Total home mortgage interest, summed from the person-level " + "home_mortgage_interest input — the single mortgage-interest input " + "(issue #9275). The per-mortgage balances and origination years remain " + "tax-unit inputs because the acquisition-debt caps need them per loan." ) - - def formula(tax_unit, period, parameters): - reported_interest = add(tax_unit, period, ["home_mortgage_interest"]) - structured_interest = add( - tax_unit, - period, - ["first_home_mortgage_interest", "second_home_mortgage_interest"], - ) - return where(reported_interest > 0, reported_interest, structured_interest) + adds = ["home_mortgage_interest"] class deductible_mortgage_interest_tax_unit(Variable): @@ -145,8 +105,6 @@ def formula(tax_unit, period, parameters): first_year = tax_unit("first_home_mortgage_origination_year", period) second_year = tax_unit("second_home_mortgage_origination_year", period) total_balance = first_balance + second_balance - # Falls back to reported person-level interest when the structured - # first/second inputs are absent. total_interest = tax_unit("home_mortgage_interest_tax_unit", period) filing_status = tax_unit("filing_status", period)