From ea3b36bdf21488d0ebaaed91b6c8f676e2e6fbe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:09:32 +0000 Subject: [PATCH 1/5] Keep itemized sales taxes in Utah's taxpayer credit base Utah Code 59-10-1018(1)(f)(i) removes state or local income tax from the federal itemized total, and the TC-40 line 14 instructions say not to remove itemized sales tax. The model removed the combined income-or-sales amount, so a sales-tax election lost its sales tax from the credit base. A strictly larger sales amount (the federal election) now keeps the whole allowed SALT deduction; the income-tax branch is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JnxodpPD6TQDSxpKU --- .../ut-taxpayer-credit-sales-tax.fixed.md | 1 + ...ederal_deductions_for_taxpayer_credit.yaml | 110 +++++++++++++++++- ..._federal_deductions_for_taxpayer_credit.py | 35 +++++- 3 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 changelog.d/ut-taxpayer-credit-sales-tax.fixed.md diff --git a/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md b/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md new file mode 100644 index 00000000000..2fa016f28a8 --- /dev/null +++ b/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md @@ -0,0 +1 @@ +Keep itemized sales taxes in Utah's taxpayer credit base; Utah Code 59-10-1018(1)(f)(i) removes only state and local income taxes. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml index 0b633991a8b..505eadbac24 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml @@ -59,15 +59,123 @@ output: ut_federal_deductions_for_taxpayer_credit: 5_000 -- name: "Case 5: Utah subtracts only state income or sales tax from claimed itemized deductions" +- name: "Case 5: Utah subtracts state income tax from claimed itemized deductions" + # An income-tax election: the $9,000 income tax is removed. A sales-tax + # election keeps its sales tax in the base (Cases 6, 7 and 10). period: 2023 input: filing_status: JOINT state_code: UT tax_unit_itemizes: true itemized_taxable_income_deductions: 15_000 + state_withheld_income_tax: 9_000 + local_income_tax: 0 + state_sales_tax: 0 + local_sales_tax: 0 state_and_local_sales_or_income_tax: 9_000 standard_deduction: 0 output: ut_federal_deductions_for_taxpayer_credit: 6_000 + +- name: "Case 6: allowed sales taxes remain in the Utah credit base" + # Utah Code 59-10-1018(1)(f)(i) removes only state or local income tax from + # the federal itemized total. $6,000 other deductions + $9,000 sales tax. + period: 2025 + input: + state_code: UT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 15_000 + charitable_deduction: 6_000 + real_estate_taxes: 0 + state_withheld_income_tax: 0 + local_income_tax: 0 + state_sales_tax: 9_000 + local_sales_tax: 0 + salt_deduction: 9_000 + ut_personal_exemption: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 15_000 + # 6% x $15,000 + ut_taxpayer_credit_max: 900 + +- name: "Case 7: capped sales taxes still remain in the Utah credit base" + # Allowed SALT $10,000 (real estate $4,000 + sales $6,000 under the cap) + # + other deductions $6,000 = $16,000. + period: 2024 + input: + state_code: UT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 16_000 + charitable_deduction: 6_000 + real_estate_taxes: 4_000 + state_withheld_income_tax: 0 + local_income_tax: 0 + state_sales_tax: 9_000 + local_sales_tax: 0 + salt_deduction: 10_000 + ut_personal_exemption: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 16_000 + # 6% x $16,000 + ut_taxpayer_credit_max: 960 + +- name: "Case 8: income taxes are still removed when they exceed sales taxes" + # Income-tax election: $15,000 - $9,000 income tax = $6,000. + period: 2025 + input: + state_code: UT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 15_000 + charitable_deduction: 6_000 + real_estate_taxes: 0 + state_withheld_income_tax: 9_000 + local_income_tax: 0 + state_sales_tax: 2_000 + local_sales_tax: 0 + salt_deduction: 9_000 + ut_personal_exemption: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 6_000 + +- name: "Case 9: equal sales and income taxes follow the income-tax branch" + # The federal formula takes max(income, sales); only a strictly larger + # sales amount is treated as a sales-tax election. + period: 2025 + input: + state_code: UT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 15_000 + charitable_deduction: 6_000 + real_estate_taxes: 0 + state_withheld_income_tax: 9_000 + local_income_tax: 0 + state_sales_tax: 9_000 + local_sales_tax: 0 + salt_deduction: 9_000 + ut_personal_exemption: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 6_000 + +- name: "Case 10: sales-tax fallback when no federal itemized total is supplied" + # Fallback base: non-SALT itemized deductions ($6,000) + allowed SALT ($9,000). + period: 2025 + input: + state_code: UT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 0 + charitable_deduction: 6_000 + real_estate_taxes: 0 + state_withheld_income_tax: 0 + local_income_tax: 0 + state_sales_tax: 9_000 + local_sales_tax: 0 + salt_deduction: 9_000 + ut_personal_exemption: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 15_000 diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py index 34ffefdcdeb..bde8dc4428f 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py @@ -9,7 +9,14 @@ class ut_federal_deductions_for_taxpayer_credit(Variable): documentation = "These federal deductions are added to the Utah personal exemption to determine the Utah taxpayer credit." definition_period = YEAR defined_for = StateCode.UT - reference = "https://le.utah.gov/xcode/Title59/Chapter10/59-10-S114.html?v=C59-10-S114_2022032320220323" + reference = ( + "https://le.utah.gov/xcode/Title59/Chapter10/59-10-S114.html?v=C59-10-S114_2022032320220323", + # Utah Code 59-10-1018(1)(f)(i): only state or local income tax is + # removed from the federal itemized total. + "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2026050620260506.pdf#page=1", + # 2025 TC-40 instructions, line 14: do not remove itemized sales tax. + "https://files.tax.utah.gov/tax/forms/2025/tc-40inst.pdf#page=10", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.irs.deductions @@ -21,6 +28,13 @@ def formula(tax_unit, period, parameters): raw_state_income_or_sales_tax = tax_unit( "state_and_local_sales_or_income_tax", period ) + # state_and_local_sales_or_income_tax takes the larger of income and + # sales taxes, so a strictly larger sales amount is a sales election. + income_tax = add( + tax_unit, period, ["state_withheld_income_tax", "local_income_tax"] + ) + sales_tax = add(tax_unit, period, ["state_sales_tax", "local_sales_tax"]) + sales_tax_elected = sales_tax > income_tax deductions = [ deduction for deduction in p.itemized_deductions @@ -34,14 +48,25 @@ def formula(tax_unit, period, parameters): deducted_state_income_or_sales_tax = max_( salt_deduction - capped_real_estate_tax, 0 ) - fallback_base = fallback_itemized + capped_real_estate_tax - explicit_itemized_override = itemized > fallback_base - claimed_state_income_or_sales_tax = where( + # Income-tax election: remove the income tax, keep real-estate taxes. + income_fallback_base = fallback_itemized + capped_real_estate_tax + explicit_itemized_override = itemized > income_fallback_base + claimed_state_income_tax = where( salt_deduction > 0, deducted_state_income_or_sales_tax, where(explicit_itemized_override, raw_state_income_or_sales_tax, 0), ) - claimed_base = max_(itemized - claimed_state_income_or_sales_tax, fallback_base) + income_claimed_base = max_( + itemized - claimed_state_income_tax, income_fallback_base + ) + # Sales-tax election: 59-10-1018(1)(f)(i) removes no sales tax, so + # the whole allowed SALT deduction stays in the base. + sales_fallback_base = fallback_itemized + salt_deduction + sales_claimed_base = max_(itemized, sales_fallback_base) + fallback_base = where( + sales_tax_elected, sales_fallback_base, income_fallback_base + ) + claimed_base = where(sales_tax_elected, sales_claimed_base, income_claimed_base) itemized_base = where(itemized > 0, claimed_base, fallback_base) return where( us_itemizing, From b4ca92ba03f1545bd1956499ba0b1150a4913cc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:32:27 +0000 Subject: [PATCH 2/5] Add property test for Utah's sales-tax credit base Checks, for drawn households in 2018 and 2024-2026, that a sales-tax election keeps the whole allowed SALT deduction in the base, that the income-tax branch stays within its bounds, and that moving to a sales-tax election never lowers the base. Main's formula fails the first property. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JnxodpPD6TQDSxpKU --- ...ut_taxpayer_credit_sales_tax_properties.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py diff --git a/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py b/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py new file mode 100644 index 00000000000..771c9fac934 --- /dev/null +++ b/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py @@ -0,0 +1,137 @@ +"""Properties of the federal deductions in Utah's taxpayer credit base. + +Utah Code 59-10-1018(1)(f)(i) starts from the federal itemized deductions +and removes only state or local income tax. A tax unit whose sales taxes +strictly exceed its income taxes takes the sales tax under the federal +formula (state_and_local_sales_or_income_tax is the larger of the two), so: + +1. Sales-tax election: nothing is removed. The base equals the larger of the + federal itemized total and the non-SALT itemized deductions plus the + allowed SALT deduction, so it is never below either. +2. Income-tax election: the base never exceeds the larger of the federal + itemized total and the non-SALT deductions plus capped real-estate tax, + and it never falls below the latter. +3. Raising sales tax above income tax, with every other input fixed, never + lowers the base. + +Before this change (1) and (3) failed whenever the allowed SALT deduction +exceeded capped real-estate taxes: the sales tax was removed from the base. +Each example builds one small household simulation holding every drawn tax +unit twice, once as drawn and once with sales tax raised above income tax. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEARS = (2018, 2024, 2025, 2026) +FILING_STATUSES = ("SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD") +AMOUNT = st.integers(min_value=0, max_value=60_000) + +UNIT = st.fixed_dictionaries( + { + "filing_status": st.sampled_from(FILING_STATUSES), + "charitable": AMOUNT, + "real_estate": AMOUNT, + "income_tax": AMOUNT, + "sales_tax": AMOUNT, + "extra_itemized": AMOUNT, + "supply_itemized": st.booleans(), + } +) + + +def _salt_cap(filing_status, year): + # 26 U.S.C. 164(b)(6) and (b)(7) caps before any phase-down: $10,000 + # ($5,000 separate) through 2024, $40,000 ($20,000) in 2025 and + # $40,400 ($20,200) in 2026. + full = {2018: 10_000, 2024: 10_000, 2025: 40_000, 2026: 40_400}[year] + return full / 2 if filing_status == "SEPARATE" else full + + +def _situation(year, units): + period = str(year) + people, tax_units, households = {}, {}, {} + for i, unit in enumerate(units): + name = f"p{i}" + people[name] = { + "age": {period: 40}, + "real_estate_taxes": {period: unit["real_estate"]}, + } + salt = min( + unit["real_estate"] + max(unit["income_tax"], unit["sales_tax"]), + _salt_cap(unit["filing_status"], year), + ) + itemized = ( + unit["charitable"] + salt + unit["extra_itemized"] + if unit["supply_itemized"] + else 0 + ) + tax_units[f"t{i}"] = { + "members": [name], + "filing_status": {period: unit["filing_status"]}, + "tax_unit_itemizes": {period: True}, + "charitable_deduction": {period: unit["charitable"]}, + "interest_deduction": {period: 0}, + "medical_expense_deduction": {period: 0}, + "casualty_loss_deduction": {period: 0}, + "misc_deduction": {period: 0}, + "state_withheld_income_tax": {period: unit["income_tax"]}, + "local_income_tax": {period: 0}, + "state_sales_tax": {period: unit["sales_tax"]}, + "local_sales_tax": {period: 0}, + "salt_deduction": {period: salt}, + "itemized_taxable_income_deductions": {period: itemized}, + } + households[f"h{i}"] = {"members": [name], "state_code": {period: "UT"}} + return {"people": people, "tax_units": tax_units, "households": households} + + +def _with_sales_election(unit): + return {**unit, "sales_tax": unit["income_tax"] + 1 + unit["sales_tax"]} + + +@settings( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + year=st.sampled_from(YEARS), + units=st.lists(UNIT, min_size=4, max_size=12), +) +def test_utah_credit_base_keeps_sales_tax(year, units): + paired = units + [_with_sales_election(unit) for unit in units] + situation = _situation(year, paired) + sim = Simulation(situation=situation) + base = sim.calculate("ut_federal_deductions_for_taxpayer_credit", year) + period = str(year) + tu = situation["tax_units"] + itemized = np.array( + [ + tu[f"t{i}"]["itemized_taxable_income_deductions"][period] + for i in range(len(paired)) + ] + ) + salt = np.array([tu[f"t{i}"]["salt_deduction"][period] for i in range(len(paired))]) + other = np.array([u["charitable"] for u in paired]) + real_estate = np.array([u["real_estate"] for u in paired]) + cap = np.array([_salt_cap(u["filing_status"], year) for u in paired]) + sales_elected = np.array([u["sales_tax"] > u["income_tax"] for u in paired]) + + # (1) Sales-tax election removes nothing. + sales_base = np.maximum(itemized, other + salt) + assert np.allclose(base[sales_elected], sales_base[sales_elected]) + + # (2) Income-tax election stays within its bounds. + income_floor = other + np.minimum(real_estate, cap) + income = ~sales_elected + assert np.all(base[income] >= income_floor[income] - 1e-6) + assert np.all(base[income] <= np.maximum(itemized, income_floor)[income] + 1e-6) + + # (3) Moving to a sales-tax election never lowers the base. + n = len(units) + assert np.all(base[n:] >= base[:n] - 1e-6) From 0d2afbb31f0a545bb976d89acbe0e360eaa1eb08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 14:02:45 +0000 Subject: [PATCH 3/5] Use the federal itemized total and the federal SALT choice for Utah's base Under a sales-tax election the base was max(itemized, non-SALT + SALT). The second term is the total before section 68, so in 2013-2017 and from 2026 the base could exceed the federal itemized deductions. The statute starts from those deductions and removes no income tax under a sales-tax election, so the base is now the federal itemized total itself (Cases 11, 13 and 14; the 2017 witness had a base of $120,000 instead of $82,845). The election was recomputed from its components, so it could disagree with a supplied state_and_local_sales_or_income_tax, the amount the federal SALT deduction uses. A unit now counts as a sales-tax election only when that amount is the sales tax. A supplied amount matching neither component keeps the income-tax treatment (Case 12, the earlier Case 5 inputs, back at $6,000). Also cite the 2023 edition of 59-10-1018, the text for 2018 through 2025. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011FzNbZSacUp8TcfjZi836q --- ...ederal_deductions_for_taxpayer_credit.yaml | 108 ++++++++++++++++++ ..._federal_deductions_for_taxpayer_credit.py | 17 ++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml index 505eadbac24..bf1e17e4faa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml @@ -179,3 +179,111 @@ ut_personal_exemption: 0 output: ut_federal_deductions_for_taxpayer_credit: 15_000 + +- name: "Case 11: a federal itemized total below the reconstruction is kept under a sales-tax election" + # Section 68 can leave the federal itemized deductions ($14,000 here) below + # the non-SALT deductions plus allowed SALT ($6,000 + $9,000 = $15,000). + # A sales-tax election removes nothing, so the base is $14,000. + period: 2026 + input: + state_code: UT + filing_status: JOINT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 14_000 + charitable_deduction: 6_000 + real_estate_taxes: 0 + state_withheld_income_tax: 0 + local_income_tax: 0 + state_sales_tax: 9_000 + local_sales_tax: 0 + salt_deduction: 9_000 + ut_personal_exemption: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 14_000 + # 6% x $14,000 + ut_taxpayer_credit_max: 840 + +- name: "Case 12: a supplied sales-or-income amount matching neither component follows the income-tax branch" + # The earlier Case 5 inputs. The supplied $9,000 is neither the formula's + # sales tax nor its income tax ($0 with no income), so the unit is not a + # sales-tax election and the $9,000 is removed: $15,000 - $9,000 = $6,000. + period: 2023 + input: + filing_status: JOINT + state_code: UT + tax_unit_itemizes: true + itemized_taxable_income_deductions: 15_000 + state_and_local_sales_or_income_tax: 9_000 + standard_deduction: 0 + output: + ut_federal_deductions_for_taxpayer_credit: 6_000 + +- name: "Case 13: 2026 sales-tax election keeps the section 68 reduction" + # Single filer: $1.5M long-term capital gains (AGI $1,500,000), $100,000 + # cash to charity, $20,000 state sales tax and no income tax. + # Charitable: $100,000 - 0.5% x $1,500,000 floor = $92,500. + # SALT cap: $40,400 - 30% x ($1,500,000 - $505,000) is below the $10,000 + # floor, so allowed SALT is $10,000. Total: $92,500 + $10,000 = $102,500. + # Section 68: 2/37 x min($102,500, $1,500,000 - $640,600) = $5,540.54. + # Federal itemized deductions: $102,500 - $5,540.54 = $96,959.46, and a + # sales-tax election removes nothing from them. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + long_term_capital_gains: 1_500_000 + charitable_cash_donations: 100_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: true + state_withheld_income_tax: 0 + local_income_tax: 0 + state_sales_tax: 20_000 + local_sales_tax: 0 + households: + household: + members: [head] + state_code: UT + output: + total_itemized_taxable_income_deductions: 102_500 + itemized_taxable_income_deductions_reduction: 5_540.54 + itemized_taxable_income_deductions: 96_959.46 + ut_federal_deductions_for_taxpayer_credit: 96_959.46 + +- name: "Case 14: 2017 sales-tax election keeps the section 68 reduction" + # The Case 13 filer in 2017: no SALT cap and no charitable floor, so the + # total is $100,000 + $20,000 = $120,000. + # Section 68: min(3% x ($1,500,000 - $261,500), 80% x $120,000) + # = min($37,155, $96,000) = $37,155. + # Federal itemized deductions: $120,000 - $37,155 = $82,845, and a + # sales-tax election removes nothing from them. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + long_term_capital_gains: 1_500_000 + charitable_cash_donations: 100_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: true + state_withheld_income_tax: 0 + local_income_tax: 0 + state_sales_tax: 20_000 + local_sales_tax: 0 + households: + household: + members: [head] + state_code: UT + output: + total_itemized_taxable_income_deductions: 120_000 + itemized_taxable_income_deductions_reduction: 37_155 + itemized_taxable_income_deductions: 82_845 + ut_federal_deductions_for_taxpayer_credit: 82_845 diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py index bde8dc4428f..b8844a03e49 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py @@ -12,8 +12,12 @@ class ut_federal_deductions_for_taxpayer_credit(Variable): reference = ( "https://le.utah.gov/xcode/Title59/Chapter10/59-10-S114.html?v=C59-10-S114_2022032320220323", # Utah Code 59-10-1018(1)(f)(i): only state or local income tax is - # removed from the federal itemized total. + # removed from the federal itemized total. This edition covers tax + # years from 2026. "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2026050620260506.pdf#page=1", + # Utah Code 59-10-1018, 2023 edition: the text in force from May 8, + # 2018 through tax year 2025. + "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2023050320230503.html", # 2025 TC-40 instructions, line 14: do not remove itemized sales tax. "https://files.tax.utah.gov/tax/forms/2025/tc-40inst.pdf#page=10", ) @@ -34,7 +38,12 @@ def formula(tax_unit, period, parameters): tax_unit, period, ["state_withheld_income_tax", "local_income_tax"] ) sales_tax = add(tax_unit, period, ["state_sales_tax", "local_sales_tax"]) - sales_tax_elected = sales_tax > income_tax + # Sales-tax election only where the SALT amount the federal side used + # is the sales tax; a supplied amount that matches neither keeps the + # income-tax treatment. + sales_tax_elected = (sales_tax > income_tax) & np.isclose( + raw_state_income_or_sales_tax, sales_tax + ) deductions = [ deduction for deduction in p.itemized_deductions @@ -62,7 +71,9 @@ def formula(tax_unit, period, parameters): # Sales-tax election: 59-10-1018(1)(f)(i) removes no sales tax, so # the whole allowed SALT deduction stays in the base. sales_fallback_base = fallback_itemized + salt_deduction - sales_claimed_base = max_(itemized, sales_fallback_base) + # Sales-tax election: nothing is removed from the federal itemized + # deductions (after section 68); itemized_base falls back when zero. + sales_claimed_base = itemized fallback_base = where( sales_tax_elected, sales_fallback_base, income_fallback_base ) From 35d7e732e80a174d2b92c9064cd3af0c9c424c3b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 14:05:21 +0000 Subject: [PATCH 4/5] Draw section 68 cuts in the Utah sales-tax property test The property test always supplied a federal itemized total at or above the reconstruction (non-SALT deductions plus allowed SALT), so section 68 never entered and putting max(itemized, reconstruction) back as the sales-tax base passed it. The draws now cut the supplied total by up to 80%, an explicit example pins a cut below the reconstruction under both elections, and the sales-tax property asserts the base is never above the supplied total. 2017 joins the years drawn. Pairs with a cut are left out of the property that a sales-tax election never lowers the base: the income-tax branch still floors its base at the reconstruction before section 68, which this change does not touch. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011FzNbZSacUp8TcfjZi836q --- ...ut_taxpayer_credit_sales_tax_properties.py | 124 +++++++++++++----- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py b/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py index 771c9fac934..0faea10543a 100644 --- a/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py +++ b/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py @@ -5,28 +5,35 @@ strictly exceed its income taxes takes the sales tax under the federal formula (state_and_local_sales_or_income_tax is the larger of the two), so: -1. Sales-tax election: nothing is removed. The base equals the larger of the - federal itemized total and the non-SALT itemized deductions plus the - allowed SALT deduction, so it is never below either. +1. Sales-tax election: nothing is removed. The base is never above the + supplied federal itemized total, and equals it; with no total supplied, + it equals the non-SALT itemized deductions plus the allowed SALT + deduction. 2. Income-tax election: the base never exceeds the larger of the federal itemized total and the non-SALT deductions plus capped real-estate tax, and it never falls below the latter. 3. Raising sales tax above income tax, with every other input fixed, never - lowers the base. - -Before this change (1) and (3) failed whenever the allowed SALT deduction -exceeded capped real-estate taxes: the sales tax was removed from the base. -Each example builds one small household simulation holding every drawn tax -unit twice, once as drawn and once with sales tax raised above income tax. + lowers the base when the supplied total is not cut below the + reconstruction. + +Section 68 enters through the supplied total: the draws cut it by up to 80% +of the reconstructed total, which can leave it below the non-SALT deductions +plus the allowed SALT deduction. Before this change (1) and (3) failed +whenever the allowed SALT deduction exceeded capped real-estate taxes: the +sales tax was removed from the base. With max(itemized, non-SALT + SALT) as +the sales-tax base, (1) failed whenever the cut left the total below that +reconstruction. Each example builds one small household simulation holding +every drawn tax unit twice, once as drawn and once with sales tax raised +above income tax. """ import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_us import Simulation -YEARS = (2018, 2024, 2025, 2026) +YEARS = (2017, 2018, 2024, 2025, 2026) FILING_STATUSES = ("SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD") AMOUNT = st.integers(min_value=0, max_value=60_000) @@ -38,19 +45,43 @@ "income_tax": AMOUNT, "sales_tax": AMOUNT, "extra_itemized": AMOUNT, + # Percent of the reconstructed total that section 68 removes from the + # supplied federal itemized deductions (at most 80% before 2018). + "section_68_cut": st.integers(min_value=0, max_value=80), "supply_itemized": st.booleans(), } ) def _salt_cap(filing_status, year): - # 26 U.S.C. 164(b)(6) and (b)(7) caps before any phase-down: $10,000 - # ($5,000 separate) through 2024, $40,000 ($20,000) in 2025 and - # $40,400 ($20,200) in 2026. - full = {2018: 10_000, 2024: 10_000, 2025: 40_000, 2026: 40_400}[year] + # 26 U.S.C. 164(b)(6) and (b)(7) caps before any phase-down: none in + # 2017, $10,000 ($5,000 separate) from 2018 through 2024, $40,000 + # ($20,000) in 2025 and $40,400 ($20,200) in 2026. + full = { + 2017: np.inf, + 2018: 10_000, + 2024: 10_000, + 2025: 40_000, + 2026: 40_400, + }[year] return full / 2 if filing_status == "SEPARATE" else full +def _salt(unit, year): + return min( + unit["real_estate"] + max(unit["income_tax"], unit["sales_tax"]), + _salt_cap(unit["filing_status"], year), + ) + + +def _itemized(unit, year): + if not unit["supply_itemized"]: + return 0 + total = unit["charitable"] + _salt(unit, year) + unit["extra_itemized"] + # Whole dollars keep the values exact in float32. + return total - total * unit["section_68_cut"] // 100 + + def _situation(year, units): period = str(year) people, tax_units, households = {}, {}, {} @@ -60,15 +91,6 @@ def _situation(year, units): "age": {period: 40}, "real_estate_taxes": {period: unit["real_estate"]}, } - salt = min( - unit["real_estate"] + max(unit["income_tax"], unit["sales_tax"]), - _salt_cap(unit["filing_status"], year), - ) - itemized = ( - unit["charitable"] + salt + unit["extra_itemized"] - if unit["supply_itemized"] - else 0 - ) tax_units[f"t{i}"] = { "members": [name], "filing_status": {period: unit["filing_status"]}, @@ -82,8 +104,8 @@ def _situation(year, units): "local_income_tax": {period: 0}, "state_sales_tax": {period: unit["sales_tax"]}, "local_sales_tax": {period: 0}, - "salt_deduction": {period: salt}, - "itemized_taxable_income_deductions": {period: itemized}, + "salt_deduction": {period: _salt(unit, year)}, + "itemized_taxable_income_deductions": {period: _itemized(unit, year)}, } households[f"h{i}"] = {"members": [name], "state_code": {period: "UT"}} return {"people": people, "tax_units": tax_units, "households": households} @@ -103,6 +125,34 @@ def _with_sales_election(unit): year=st.sampled_from(YEARS), units=st.lists(UNIT, min_size=4, max_size=12), ) +# Section 68 cuts the supplied total below the reconstruction under both +# elections: $15,000 - 10% = $13,500 against $6,000 + $9,000 for the first +# unit, so (1) holds only if the base stays at $13,500. +@example( + year=2026, + units=[ + { + "filing_status": "JOINT", + "charitable": 6_000, + "real_estate": 0, + "income_tax": 0, + "sales_tax": 9_000, + "extra_itemized": 0, + "section_68_cut": 10, + "supply_itemized": True, + }, + { + "filing_status": "SINGLE", + "charitable": 6_000, + "real_estate": 4_000, + "income_tax": 9_000, + "sales_tax": 2_000, + "extra_itemized": 0, + "section_68_cut": 10, + "supply_itemized": True, + }, + ], +) def test_utah_credit_base_keeps_sales_tax(year, units): paired = units + [_with_sales_election(unit) for unit in units] situation = _situation(year, paired) @@ -122,9 +172,14 @@ def test_utah_credit_base_keeps_sales_tax(year, units): cap = np.array([_salt_cap(u["filing_status"], year) for u in paired]) sales_elected = np.array([u["sales_tax"] > u["income_tax"] for u in paired]) - # (1) Sales-tax election removes nothing. - sales_base = np.maximum(itemized, other + salt) - assert np.allclose(base[sales_elected], sales_base[sales_elected]) + # (1) Sales-tax election removes nothing: never above the supplied + # federal itemized total, and equal to it, or to the fallback when no + # total is supplied. + supplied = sales_elected & (itemized > 0) + assert np.all(base[supplied] <= itemized[supplied] + 1e-6) + assert np.allclose(base[supplied], itemized[supplied]) + not_supplied = sales_elected & (itemized == 0) + assert np.allclose(base[not_supplied], (other + salt)[not_supplied]) # (2) Income-tax election stays within its bounds. income_floor = other + np.minimum(real_estate, cap) @@ -132,6 +187,13 @@ def test_utah_credit_base_keeps_sales_tax(year, units): assert np.all(base[income] >= income_floor[income] - 1e-6) assert np.all(base[income] <= np.maximum(itemized, income_floor)[income] + 1e-6) - # (3) Moving to a sales-tax election never lowers the base. + # (3) Moving to a sales-tax election never lowers the base. A pair whose + # supplied total is cut is left out: the income-tax branch floors its + # base at the reconstruction before section 68, which the sales-tax + # branch does not. n = len(units) - assert np.all(base[n:] >= base[:n] - 1e-6) + uncut = np.array( + [u["section_68_cut"] == 0 or not u["supply_itemized"] for u in units], + dtype=bool, + ) + assert np.all(base[n:][uncut] >= base[:n][uncut] - 1e-6) From ac417e0352fb20d6c0303892b2bbf0b2954adce3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 15:55:25 +0000 Subject: [PATCH 5/5] Keep a supplied sales-tax election when the sales tax is below the income tax The review found that the election test required sales tax above income tax, so a supplied federal SALT amount equal to the sales tax was treated as an income-tax election and the sales tax was removed. A filer can make that election, and 59-10-1018 then keeps the sales tax in the base. The test now asks only whether the amount equals the sales tax and not the income tax. A tie still takes the income-tax branch (Case 9), and so does a supplied amount that matches neither (Case 12). Case 15 is the review's witness: a 2025 joint return with $3,600 of sales tax, $3,700 of income tax and a supplied $3,600. Its base is now $39,600 and its credit $1,809.54, where the old test gave $36,000 and $1,593.54. The references add the pre-2018 edition, whose rule sits in (2)(a)(ii)(A)(II), and drop 59-10-114, which covers additions to and subtractions from income and not this credit. The changelog and the property test's docstring no longer put the pre-2018 rule in (1)(f)(i). Two adjacent comments merge into one, and Case 9 gives its reason. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N2tyHT12N9L3WVu8rJokf1 --- .../ut-taxpayer-credit-sales-tax.fixed.md | 2 +- ...ut_taxpayer_credit_sales_tax_properties.py | 11 +++-- ...ederal_deductions_for_taxpayer_credit.yaml | 49 +++++++++++++++++-- ..._federal_deductions_for_taxpayer_credit.py | 36 +++++++------- 4 files changed, 73 insertions(+), 25 deletions(-) diff --git a/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md b/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md index 2fa016f28a8..52d9a540b75 100644 --- a/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md +++ b/changelog.d/ut-taxpayer-credit-sales-tax.fixed.md @@ -1 +1 @@ -Keep itemized sales taxes in Utah's taxpayer credit base; Utah Code 59-10-1018(1)(f)(i) removes only state and local income taxes. +Keep itemized sales taxes, including a sales-tax election shown by a supplied sales-or-income amount, in Utah's taxpayer credit base; Utah Code 59-10-1018 removes only state and local income taxes. diff --git a/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py b/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py index 0faea10543a..5f829b3f463 100644 --- a/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py +++ b/policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py @@ -1,9 +1,12 @@ """Properties of the federal deductions in Utah's taxpayer credit base. -Utah Code 59-10-1018(1)(f)(i) starts from the federal itemized deductions -and removes only state or local income tax. A tax unit whose sales taxes -strictly exceed its income taxes takes the sales tax under the federal -formula (state_and_local_sales_or_income_tax is the larger of the two), so: +Utah Code 59-10-1018 starts from the federal itemized deductions and removes +only state or local income tax: in (2)(a)(ii)(A) of the edition superseded on +May 8, 2018, which the 2017 draws use, and after that in the definition of +the Utah itemized deduction ((1)(f)(i) in the 2021, 2023 and 2026 editions). +A tax unit whose sales taxes strictly exceed its income taxes takes the +sales tax under the federal formula (state_and_local_sales_or_income_tax is +the larger of the two), so: 1. Sales-tax election: nothing is removed. The base is never above the supplied federal itemized total, and equals it; with no total supplied, diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml index bf1e17e4faa..f9c9a1f39b3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml @@ -142,8 +142,8 @@ ut_federal_deductions_for_taxpayer_credit: 6_000 - name: "Case 9: equal sales and income taxes follow the income-tax branch" - # The federal formula takes max(income, sales); only a strictly larger - # sales amount is treated as a sales-tax election. + # The federal SALT amount equals both taxes, so it doesn't show which one + # the return deducted; the conservative reading removes it as income tax. period: 2025 input: state_code: UT @@ -260,7 +260,8 @@ # Section 68: min(3% x ($1,500,000 - $261,500), 80% x $120,000) # = min($37,155, $96,000) = $37,155. # Federal itemized deductions: $120,000 - $37,155 = $82,845, and a - # sales-tax election removes nothing from them. + # sales-tax election removes nothing from them: the 2017 text, + # 59-10-1018(2)(a)(ii)(A)(II), removes only state or local income taxes. period: 2017 absolute_error_margin: 0.01 input: @@ -287,3 +288,45 @@ itemized_taxable_income_deductions_reduction: 37_155 itemized_taxable_income_deductions: 82_845 ut_federal_deductions_for_taxpayer_credit: 82_845 + +- name: "Case 15: a supplied sales-tax election stands when the sales tax is below the income tax" + # The supplied $3,600 federal SALT amount equals the sales tax and not the + # $3,700 income tax, so the return deducted sales tax (Schedule A line 5a) + # and reports no income tax. 59-10-1018(1)(e)-(f) removes only income tax + # the claimant reports on the federal return, so nothing is removed. + # Federal itemized deductions: $36,000 + $3,600 = $39,600 (no section 68 + # reduction in 2025). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 80_000 + spouse: + age: 40 + tax_units: + tax_unit: + members: [head, spouse] + tax_unit_itemizes: true + charitable_deduction: 36_000 + state_withheld_income_tax: 3_700 + local_income_tax: 0 + state_sales_tax: 3_600 + local_sales_tax: 0 + state_and_local_sales_or_income_tax: 3_600 + households: + household: + members: [head, spouse] + state_code: UT + output: + salt_deduction: 3_600 + itemized_taxable_income_deductions: 39_600 + ut_federal_deductions_for_taxpayer_credit: 39_600 + # TC-40 line 16: 6% x $39,600. + ut_taxpayer_credit_max: 2_376 + # Lines 17 to 19: 1.3% x ($80,000 - $36,426) = $566.46. Line 20: + # $2,376 - $566.46. Before this fix the base was $36,000, the credit + # maximum $2,160 and the credit $1,593.54, $216 less. + ut_taxpayer_credit_reduction: 566.46 + ut_taxpayer_credit: 1_809.54 diff --git a/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py b/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py index b8844a03e49..cd915f94c18 100644 --- a/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py +++ b/policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py @@ -10,14 +10,18 @@ class ut_federal_deductions_for_taxpayer_credit(Variable): definition_period = YEAR defined_for = StateCode.UT reference = ( - "https://le.utah.gov/xcode/Title59/Chapter10/59-10-S114.html?v=C59-10-S114_2022032320220323", - # Utah Code 59-10-1018(1)(f)(i): only state or local income tax is - # removed from the federal itemized total. This edition covers tax - # years from 2026. - "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2026050620260506.pdf#page=1", - # Utah Code 59-10-1018, 2023 edition: the text in force from May 8, - # 2018 through tax year 2025. + # Utah Code 59-10-1018(2)(a)(ii)(A), the edition superseded on May 8, + # 2018: the federal itemized deduction less the state or local income + # taxes deducted as an itemized deduction. + "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_1800010118000101.html", + # Utah Code 59-10-1018(1)(e) and (1)(f)(i), 2023 edition: only state + # or local income tax is removed from the federal itemized total. The + # 2021 edition reads the same, and the edition effective May 8, 2018 + # has the text at (1)(g) and (1)(h)(i). "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2023050320230503.html", + # Utah Code 59-10-1018(1)(e) and (1)(f)(i), the edition for tax years + # from 2026. + "https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2026050620260506.pdf#page=1", # 2025 TC-40 instructions, line 14: do not remove itemized sales tax. "https://files.tax.utah.gov/tax/forms/2025/tc-40inst.pdf#page=10", ) @@ -32,18 +36,17 @@ def formula(tax_unit, period, parameters): raw_state_income_or_sales_tax = tax_unit( "state_and_local_sales_or_income_tax", period ) - # state_and_local_sales_or_income_tax takes the larger of income and - # sales taxes, so a strictly larger sales amount is a sales election. income_tax = add( tax_unit, period, ["state_withheld_income_tax", "local_income_tax"] ) sales_tax = add(tax_unit, period, ["state_sales_tax", "local_sales_tax"]) # Sales-tax election only where the SALT amount the federal side used - # is the sales tax; a supplied amount that matches neither keeps the - # income-tax treatment. - sales_tax_elected = (sales_tax > income_tax) & np.isclose( + # equals the sales tax and not the income tax, whether the formula + # (the larger of the two) or an input supplied it. A tie or a supplied + # amount that matches neither keeps the income-tax treatment. + sales_tax_elected = np.isclose( raw_state_income_or_sales_tax, sales_tax - ) + ) & ~np.isclose(raw_state_income_or_sales_tax, income_tax) deductions = [ deduction for deduction in p.itemized_deductions @@ -68,11 +71,10 @@ def formula(tax_unit, period, parameters): income_claimed_base = max_( itemized - claimed_state_income_tax, income_fallback_base ) - # Sales-tax election: 59-10-1018(1)(f)(i) removes no sales tax, so - # the whole allowed SALT deduction stays in the base. + # Sales-tax election: 59-10-1018 removes no sales tax. The base is the + # federal itemized deductions after section 68; when that is zero, the + # fallback is the non-SALT deductions plus the allowed SALT deduction. sales_fallback_base = fallback_itemized + salt_deduction - # Sales-tax election: nothing is removed from the federal itemized - # deductions (after section 68); itemized_base falls back when zero. sales_claimed_base = itemized fallback_base = where( sales_tax_elected, sales_fallback_base, income_fallback_base