diff --git a/changelog.d/or-federal-tax-subtraction-worksheet.fixed.md b/changelog.d/or-federal-tax-subtraction-worksheet.fixed.md new file mode 100644 index 00000000000..33f2b52f04b --- /dev/null +++ b/changelog.d/or-federal-tax-subtraction-worksheet.fixed.md @@ -0,0 +1 @@ +Reduce Oregon's federal tax liability subtraction by the premium tax credit and only the other refundable credits on the Publication OR-17 federal tax worksheet, and leave out the net investment income tax. diff --git a/policyengine_us/parameters/gov/states/or/tax/income/subtractions/federal_tax_liability/refundable_credits.yaml b/policyengine_us/parameters/gov/states/or/tax/income/subtractions/federal_tax_liability/refundable_credits.yaml new file mode 100644 index 00000000000..402c412bdac --- /dev/null +++ b/policyengine_us/parameters/gov/states/or/tax/income/subtractions/federal_tax_liability/refundable_credits.yaml @@ -0,0 +1,50 @@ +description: Oregon reduces the federal income tax liability it subtracts by these federal refundable credits. +values: + # Federal tax worksheet, Part A, of each year's Publication OR-17. The earned + # income credit is never listed (ORS 316.685(1)(b)), and outside 2021 neither + # is the additional child tax credit. + 2017-01-01: + - refundable_american_opportunity_credit # Line 6 + - assigned_aca_ptc # Line 7 (Form 8962, line 24) + 2020-01-01: + - refundable_american_opportunity_credit # Line 6 + - assigned_aca_ptc # Line 7 (Form 8962, line 24) + - recovery_rebate_credit # Line 8, with the stimulus payments received + 2021-01-01: + # Line 8 is the child tax credit on Schedule 8812, line 14e. Line 2 adds + # back the nonrefundable part that line 1 already nets out. + - refundable_ctc + - refundable_american_opportunity_credit # Line 9 + - recovery_rebate_credit # Line 10, with the stimulus payments received + - assigned_aca_ptc # Line 11 (Form 8962, line 24) + - cdcc # Line 12 (Schedule 3, line 13g) + 2022-01-01: + - refundable_american_opportunity_credit # Line 6 + - assigned_aca_ptc # Line 7 (Form 8962, line 24) + # From 2025, line 7 is the refundable adoption credit (Form 1040, line 30), + # which is not modeled, and the premium tax credit moves to line 8. +metadata: + unit: list + period: year + label: Oregon federal tax liability subtraction refundable credits + reference: + - title: ORS 316.680(1)(b) and 316.685 + href: https://www.oregonlegislature.gov/bills_laws/ors/ors316.html + - title: "2017 Publication OR-17, federal tax worksheet, Part A, lines 6 to 8 #page 63" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2017.pdf#page=63 + - title: "2018 Publication OR-17, federal tax worksheet, Part A, lines 6 to 8 #page 68" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2018.pdf#page=68 + - title: "2019 Publication OR-17, federal tax worksheet, Part A, lines 6 to 8 #page 63" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2019.pdf#page=63 + - title: "2020 Publication OR-17, federal tax worksheet, Part A, lines 6 to 9 #page 65-66" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2020.pdf#page=66 + - title: "2021 Publication OR-17, federal tax worksheet, Part A, lines 2 and 8 to 13 #page 72-73" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2021.pdf#page=72 + - title: "2022 Publication OR-17, federal tax worksheet, Part A, lines 6 to 8 #page 73" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2022.pdf#page=73 + - title: "2023 Publication OR-17, federal tax worksheet, Part A, lines 6 to 8 #page 73" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2023.pdf#page=73 + - title: "2024 Publication OR-17, federal tax worksheet, Part A, lines 6 to 8 #page 71" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2024.pdf#page=71 + - title: "2025 Publication OR-17, federal tax worksheet, Part A, lines 6 to 9 #page 71-72" + href: https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2025.pdf#page=71 diff --git a/policyengine_us/tests/core/test_or_federal_tax_liability_subtraction_invariants.py b/policyengine_us/tests/core/test_or_federal_tax_liability_subtraction_invariants.py new file mode 100644 index 00000000000..a6244e1992b --- /dev/null +++ b/policyengine_us/tests/core/test_or_federal_tax_liability_subtraction_invariants.py @@ -0,0 +1,119 @@ +"""Oregon's federal tax liability subtraction follows the OR-17 worksheet. + +ORS 316.680(1)(b) and 316.685; Publication OR-17, federal tax worksheet, +Part A. The subtraction is federal tax after nonrefundable credits plus other +income taxes, less only the refundable credits the worksheet lists, floored at +zero and capped by federal AGI. These properties hold for every input: + +1. 0 <= subtraction <= min(federal tax before refundable credits, cap). +2. The earned income credit never changes it (ORS 316.685(1)(b)), and outside + 2021 neither does the additional child tax credit. +3. Raising a listed credit (the premium tax credit) by d lowers it by at most d. +4. Raising federal tax before refundable credits by d raises it by at most d. + +Each Hypothesis example evaluates a batch of tax units and their variants in +one vectorized simulation. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 +FILING_STATUSES = ("SINGLE", "JOINT", "HEAD_OF_HOUSEHOLD", "SEPARATE") +CREDITS = ( + "eitc", + "refundable_ctc", + "refundable_american_opportunity_credit", + "assigned_aca_ptc", + "recovery_rebate_credit", + "cdcc", +) +VARIANTS = ("base", "eitc", "refundable_ctc", "assigned_aca_ptc", "tax") + + +@st.composite +def tax_unit(draw): + return { + "filing_status": draw(st.sampled_from(FILING_STATUSES)), + "adjusted_gross_income": draw(st.integers(0, 300_000)), + "income_tax_before_refundable_credits": draw(st.integers(0, 20_000)), + "net_investment_income_tax": draw(st.integers(0, 2_000)), + "unreported_payroll_tax": draw(st.integers(0, 1_000)), + **{credit: draw(st.integers(0, 6_000)) for credit in CREDITS}, + "delta": draw(st.integers(1, 5_000)), + } + + +def _run(units, year): + situation = {"people": {}, "tax_units": {}, "households": {}} + rows = [] + for i, unit in enumerate(units): + for variant in VARIANTS: + inputs = {k: v for k, v in unit.items() if k != "delta"} + if variant == "tax": + inputs["income_tax_before_refundable_credits"] += unit["delta"] + elif variant != "base": + inputs[variant] += unit["delta"] + name = f"{i}_{variant}" + situation["people"][name] = {"age": {year: 40}} + situation["tax_units"][name] = { + "members": [name], + **{k: {year: v} for k, v in inputs.items()}, + } + situation["households"][name] = { + "members": [name], + "state_code": {year: "OR"}, + } + rows.append((i, variant)) + simulation = Simulation(situation=situation) + values = simulation.calculate("or_federal_tax_liability_subtraction", year) + result = {} + for (i, variant), value in zip(rows, values): + result.setdefault(i, {})[variant] = float(value) + # The largest cap, at zero AGI, from the simulation's own parameters. + p = ( + simulation.tax_benefit_system.parameters(f"{year}-01-01") + .gov.states["or"] + .tax.income.subtractions.federal_tax_liability.cap + ) + zero = np.array([0.0]) + cap = { + "SINGLE": p.single.calc(zero)[0], + "JOINT": p.joint.calc(zero)[0], + "HEAD_OF_HOUSEHOLD": p.head_of_household.calc(zero)[0], + "SEPARATE": p.separate.calc(zero)[0], + } + return result, cap + + +@pytest.mark.parametrize("year", [2020, 2021, 2025]) +@settings( + max_examples=4, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(units=st.lists(tax_unit(), min_size=6, max_size=6)) +def test_or_federal_tax_liability_subtraction_invariants(year, units): + result, cap = _run(units, year) + for i, unit in enumerate(units): + r = result[i] + base, delta = r["base"], unit["delta"] + # 1. Bounds. + assert -TOLERANCE <= base + assert base <= unit["income_tax_before_refundable_credits"] + TOLERANCE + assert base <= cap[unit["filing_status"]] + TOLERANCE + # 2. Credits the worksheet never lists. + assert r["eitc"] == pytest.approx(base, abs=TOLERANCE) + if year == 2021: + assert base - delta - TOLERANCE <= r["refundable_ctc"] <= base + TOLERANCE + else: + assert r["refundable_ctc"] == pytest.approx(base, abs=TOLERANCE) + # 3. A listed credit lowers the subtraction by at most its increase. + assert base - delta - TOLERANCE <= r["assigned_aca_ptc"] <= base + TOLERANCE + # 4. More federal tax raises it by at most the increase. + assert base - TOLERANCE <= r["tax"] <= base + delta + TOLERANCE diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.yaml index 4e8190d3f01..cd07ffd2cd2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.yaml @@ -4,7 +4,7 @@ state_code: OR filing_status: SINGLE adjusted_gross_income: 80_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: or_federal_tax_liability_subtraction: 7_050 @@ -15,7 +15,8 @@ state_code: OR filing_status: SINGLE adjusted_gross_income: 80_000 - income_tax: 6_000 + # 6,000 of federal income tax plus the 1,000 EITC it nets out. + income_tax_before_refundable_credits: 7_000 eitc: 1_000 output: or_federal_tax_liability_subtraction: 7_000 @@ -26,7 +27,7 @@ state_code: OR filing_status: SINGLE adjusted_gross_income: 145_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: or_federal_tax_liability_subtraction: 0 @@ -37,7 +38,7 @@ state_code: OR filing_status: SEPARATE adjusted_gross_income: 80_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: or_federal_tax_liability_subtraction: 3_525 @@ -48,7 +49,7 @@ state_code: OR filing_status: SEPARATE adjusted_gross_income: 145_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: or_federal_tax_liability_subtraction: 0 @@ -59,8 +60,8 @@ state_code: OR filing_status: JOINT adjusted_gross_income: 80_000 - income_tax: 10_000 - eitc: 0 + income_tax_before_refundable_credits: 10_000 + eitc: 0 output: or_federal_tax_liability_subtraction: 7_050 @@ -70,7 +71,7 @@ state_code: OR filing_status: JOINT adjusted_gross_income: 290_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: or_federal_tax_liability_subtraction: 0 @@ -85,7 +86,7 @@ state_code: OR filing_status: SINGLE adjusted_gross_income: 80_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: or_federal_tax_liability_subtraction: 8_750 @@ -96,7 +97,7 @@ state_code: OR filing_status: JOINT adjusted_gross_income: 255_000 - income_tax: 30_000 + income_tax_before_refundable_credits: 30_000 eitc: 0 output: or_federal_tax_liability_subtraction: 7_000 @@ -107,8 +108,123 @@ state_code: OR filing_status: SEPARATE adjusted_gross_income: 80_000 - income_tax: 10_000 + income_tax_before_refundable_credits: 10_000 eitc: 0 output: # 50% x 8,750 or_federal_tax_liability_subtraction: 4_375 + +# Federal tax worksheet, Part A, 2025 Publication OR-17, PDF pages 71-72: line 5 +# (federal tax after nonrefundable credits plus other income taxes) less the +# American Opportunity, refundable adoption and premium tax credits (lines 6 to +# 9), floored at zero (line 10), then capped (line 12). +- name: Case 11, 2025 premium tax credit reduces the subtraction. + period: 2025 + input: + state_code: OR + filing_status: SINGLE + adjusted_gross_income: 45_000 + income_tax_before_refundable_credits: 4_000 + assigned_aca_ptc: 1_500 + eitc: 0 + output: + # 4,000 - 1,500 + or_federal_tax_liability_subtraction: 2_500 + +- name: Case 12, 2025 additional child tax credit does not reduce the subtraction. + period: 2025 + input: + state_code: OR + filing_status: JOINT + adjusted_gross_income: 60_000 + # Form 1040 line 22 is 0; a 1,000 additional tax on an early retirement + # plan distribution is Schedule 2, line 8 (worksheet line 4). + income_tax_before_refundable_credits: 1_000 + refundable_ctc: 3_000 + assigned_aca_ptc: 0 + eitc: 500 + output: + or_federal_tax_liability_subtraction: 1_000 + +- name: Case 13, 2025 listed credits above the liability floor the subtraction at zero. + period: 2025 + input: + state_code: OR + filing_status: SINGLE + adjusted_gross_income: 40_000 + income_tax_before_refundable_credits: 2_000 + refundable_american_opportunity_credit: 1_000 + assigned_aca_ptc: 1_500 + eitc: 0 + output: + # max(0, 2,000 - 1,000 - 1,500) + or_federal_tax_liability_subtraction: 0 + +- name: Case 14, 2025 net investment income tax and unreported payroll tax are not subtracted. + period: 2025 + input: + state_code: OR + filing_status: SINGLE + adjusted_gross_income: 90_000 + income_tax_before_refundable_credits: 5_000 + # Schedule 2, lines 12 and 5, which worksheet line 4 leaves out. + net_investment_income_tax: 600 + unreported_payroll_tax: 400 + assigned_aca_ptc: 0 + eitc: 0 + output: + # 5,000 - 600 - 400 + or_federal_tax_liability_subtraction: 4_000 + +# 2021 Publication OR-17, PDF page 72: lines 8 to 12 subtract the refundable +# child tax credit, the American Opportunity credit, the recovery rebate credit +# with stimulus payments received, the premium tax credit and the refundable +# child and dependent care credit. +- name: Case 15, 2021 refundable child tax, recovery rebate and child care credits reduce the subtraction. + period: 2021 + input: + state_code: OR + filing_status: JOINT + adjusted_gross_income: 80_000 + income_tax_before_refundable_credits: 9_000 + refundable_ctc: 3_000 + recovery_rebate_credit: 2_800 + cdcc: 1_000 + assigned_aca_ptc: 0 + eitc: 0 + output: + # 9,000 - 3,000 - 2,800 - 1,000 + or_federal_tax_liability_subtraction: 2_200 + +# 2020 Publication OR-17, PDF page 66: line 8 subtracts the stimulus payments +# received plus the recovery rebate credit; the additional child tax credit is +# not listed. +- name: Case 16, 2020 recovery rebate credit reduces the subtraction but the additional child tax credit does not. + period: 2020 + input: + state_code: OR + filing_status: JOINT + adjusted_gross_income: 50_000 + income_tax_before_refundable_credits: 3_000 + recovery_rebate_credit: 2_400 + refundable_ctc: 1_000 + assigned_aca_ptc: 0 + eitc: 0 + output: + # 3,000 - 2,400 + or_federal_tax_liability_subtraction: 600 + +- name: Case 17, 2026 premium tax credit and American Opportunity credit reduce the subtraction. + period: 2026 + input: + state_code: OR + filing_status: HEAD_OF_HOUSEHOLD + adjusted_gross_income: 70_000 + income_tax_before_refundable_credits: 6_000 + refundable_american_opportunity_credit: 1_000 + assigned_aca_ptc: 2_000 + refundable_ctc: 1_700 + eitc: 0 + output: + # 6,000 - 1,000 - 2,000 + or_federal_tax_liability_subtraction: 3_000 diff --git a/policyengine_us/variables/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.py b/policyengine_us/variables/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.py index 43168c0596f..e2409c59d60 100644 --- a/policyengine_us/variables/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.py +++ b/policyengine_us/variables/gov/states/or/tax/income/subtractions/or_federal_tax_liability_subtraction.py @@ -8,24 +8,40 @@ class or_federal_tax_liability_subtraction(Variable): unit = USD definition_period = YEAR reference = ( - "https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2021.pdf#page=71", - "https://www.oregonlegislature.gov/bills_laws/ors/ors316.html", # Subsection 316.800 + # ORS 316.680(1)(b), 316.685 and 316.695(3). + "https://www.oregonlegislature.gov/bills_laws/ors/ors316.html", + # Publication OR-17, federal tax worksheet, Part A: PDF pages 71-72 + # in 2025 and 72-73 in 2021. + "https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2025.pdf#page=71", + "https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2021.pdf#page=72", ) defined_for = StateCode.OR def formula(tax_unit, period, parameters): - # calculate Oregon concept of federal income tax - federal_itax = tax_unit("income_tax", period) - federal_eitc = tax_unit("eitc", period) - or_federal_income_tax = max_(0, federal_itax + federal_eitc) - # limit subtraction based on caps scaled to federal AGI - filing_status = tax_unit("filing_status", period) - status = filing_status.possible_values p = ( parameters(period) .gov.states["or"] - .tax.income.subtractions.federal_tax_liability.cap + .tax.income.subtractions.federal_tax_liability + ) + # Federal tax worksheet, line 5: federal income tax after nonrefundable + # credits plus the other taxes on Schedule 2, lines 8, 16 and 17. The + # worksheet leaves out the net investment income tax (Schedule 2, line + # 12) and Social Security and Medicare taxes on unreported tips and + # wages (lines 5 and 6; ORS 316.685(3)(a)). + tax_before_refundable_credits = tax_unit( + "income_tax_before_refundable_credits", period + ) - add( + tax_unit, period, ["net_investment_income_tax", "unreported_payroll_tax"] ) + # Only the refundable credits the worksheet lists reduce the liability. + # The earned income credit never does (ORS 316.685(1)(b)). + refundable_credits = add(tax_unit, period, p.refundable_credits) + or_federal_income_tax = max_( + 0, tax_before_refundable_credits - refundable_credits + ) + # limit subtraction based on caps scaled to federal AGI + filing_status = tax_unit("filing_status", period) + status = filing_status.possible_values federal_agi = tax_unit("adjusted_gross_income", period) cap = select( [ @@ -36,11 +52,11 @@ def formula(tax_unit, period, parameters): filing_status == status.SURVIVING_SPOUSE, ], [ - p.single.calc(federal_agi), - p.joint.calc(federal_agi), - p.head_of_household.calc(federal_agi), - p.separate.calc(federal_agi), - p.surviving_spouse.calc(federal_agi), + p.cap.single.calc(federal_agi), + p.cap.joint.calc(federal_agi), + p.cap.head_of_household.calc(federal_agi), + p.cap.separate.calc(federal_agi), + p.cap.surviving_spouse.calc(federal_agi), ], ) return min_(or_federal_income_tax, cap)