diff --git a/changelog.d/federal-claim-of-right-1341.added.md b/changelog.d/federal-claim-of-right-1341.added.md new file mode 100644 index 00000000000..1ece6e26c6d --- /dev/null +++ b/changelog.d/federal-claim-of-right-1341.added.md @@ -0,0 +1 @@ +Add the federal claim of right repayment deduction and section 1341 credit (26 U.S.C. 1341), North Carolina's bar on its claim of right deduction when federal tax is computed under section 1341(a)(5), and North Carolina's tax payment for restored income (G.S. 105-266.2). diff --git a/changelog.d/nc-claim-of-right-deduction.added.md b/changelog.d/nc-claim-of-right-deduction.added.md new file mode 100644 index 00000000000..d9a675b5026 --- /dev/null +++ b/changelog.d/nc-claim-of-right-deduction.added.md @@ -0,0 +1 @@ +Add North Carolina's itemized deduction for repaying income received under a claim of right (G.S. 105-153.5(a)(2)d), with a new claim_of_right_repayment input. diff --git a/policyengine_us/parameters/gov/household/household_refundable_credits.yaml b/policyengine_us/parameters/gov/household/household_refundable_credits.yaml index 5938f3e3b76..1cf574d021c 100644 --- a/policyengine_us/parameters/gov/household/household_refundable_credits.yaml +++ b/policyengine_us/parameters/gov/household/household_refundable_credits.yaml @@ -27,7 +27,7 @@ values: - mo_refundable_credits # Missouri. - ms_refundable_credits # Mississippi. - mt_refundable_credits # Montana. - # Skip NC which has no refundable credits. + - nc_refundable_credits # North Carolina. - nd_refundable_credits # North Dakota. - ne_refundable_credits # Nebraska. - nh_refundable_credits # New Hampshire. diff --git a/policyengine_us/parameters/gov/household/household_refundable_state_credits.yaml b/policyengine_us/parameters/gov/household/household_refundable_state_credits.yaml index 2fe90dbccf3..4bdde27f54f 100644 --- a/policyengine_us/parameters/gov/household/household_refundable_state_credits.yaml +++ b/policyengine_us/parameters/gov/household/household_refundable_state_credits.yaml @@ -26,7 +26,7 @@ values: - mo_refundable_credits # Missouri. - ms_refundable_credits # Mississippi. - mt_refundable_credits # Montana. - # Skip NC which has no refundable credits. + - nc_refundable_credits # North Carolina. - nd_refundable_credits # North Dakota. - ne_refundable_credits # Nebraska. - nh_refundable_credits # New Hampshire. diff --git a/policyengine_us/parameters/gov/household/household_tax_before_refundable_credits.yaml b/policyengine_us/parameters/gov/household/household_tax_before_refundable_credits.yaml index 656d4b40bf3..eb236bac70b 100644 --- a/policyengine_us/parameters/gov/household/household_tax_before_refundable_credits.yaml +++ b/policyengine_us/parameters/gov/household/household_tax_before_refundable_credits.yaml @@ -29,7 +29,7 @@ values: - mo_income_tax_before_refundable_credits - ms_income_tax_before_credits_unit - mt_income_tax_before_refundable_credits_unit - - nc_income_tax # NC has no refundable credits. + - nc_income_tax_before_refundable_credits - nd_income_tax_before_refundable_credits - ne_income_tax_before_refundable_credits - nh_income_tax_before_refundable_credits diff --git a/policyengine_us/parameters/gov/irs/credits/refundable.yaml b/policyengine_us/parameters/gov/irs/credits/refundable.yaml index 7676cc12363..1f4a2415d29 100644 --- a/policyengine_us/parameters/gov/irs/credits/refundable.yaml +++ b/policyengine_us/parameters/gov/irs/credits/refundable.yaml @@ -6,6 +6,7 @@ values: - refundable_ctc - recovery_rebate_credit - refundable_payroll_tax_credit + - claim_of_right_credit # Currently, the premium tax credit is not included in the refundable tax credits. # Add CDCC in 2021. 2021-01-01: @@ -14,6 +15,7 @@ values: - refundable_ctc - recovery_rebate_credit - refundable_payroll_tax_credit + - claim_of_right_credit # Currently, the premium tax credit is not included in the refundable tax credits. - cdcc # Remove CDCC in 2022. @@ -23,6 +25,7 @@ values: - refundable_ctc - recovery_rebate_credit - refundable_payroll_tax_credit + - claim_of_right_credit # Currently, the premium tax credit is not included in the refundable tax credits. metadata: unit: list diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/claim_of_right/threshold.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/claim_of_right/threshold.yaml new file mode 100644 index 00000000000..ec07a3987f1 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/claim_of_right/threshold.yaml @@ -0,0 +1,16 @@ +description: The IRS computes tax for the year of repayment as the lesser of the tax with a deduction for the repayment and the tax without it less the resulting decrease in prior-year tax, when the repayment of income held under a claim of right exceeds this amount. +values: + 1954-01-01: 3_000 +metadata: + unit: currency-USD + period: year + label: Claim of right repayment threshold + reference: + - title: 26 U.S. Code § 1341(a)(3) + href: https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm + - title: 26 U.S. Code § 67(b)(9) + href: https://www.law.cornell.edu/uscode/text/26/67#b_9 + - title: 26 CFR 1.1341-1(a)(3) + href: https://www.ecfr.gov/current/title-26/chapter-I/subchapter-A/part-1/subject-group-ECFR4c74bc8cf7c11fb/section-1.1341-1 + - title: "Publication 525 (2025), Repayments #page 36-37" + href: https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36 diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml index 14d369f6999..d0dc9c77b9c 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized_deductions.yaml @@ -7,6 +7,7 @@ values: - medical_expense_deduction - casualty_loss_deduction - misc_deduction + - claim_of_right_deduction 2020-01-01: - charitable_deduction - charitable_deduction_for_non_itemizers @@ -15,6 +16,7 @@ values: - medical_expense_deduction - casualty_loss_deduction - misc_deduction + - claim_of_right_deduction 2022-01-01: - charitable_deduction - interest_deduction @@ -22,6 +24,7 @@ values: - medical_expense_deduction - casualty_loss_deduction - misc_deduction + - claim_of_right_deduction metadata: unit: list label: itemized deductions diff --git a/policyengine_us/parameters/gov/states/household/state_income_tax_before_refundable_credits.yaml b/policyengine_us/parameters/gov/states/household/state_income_tax_before_refundable_credits.yaml index c3877dbb5d1..612fc062e93 100644 --- a/policyengine_us/parameters/gov/states/household/state_income_tax_before_refundable_credits.yaml +++ b/policyengine_us/parameters/gov/states/household/state_income_tax_before_refundable_credits.yaml @@ -27,7 +27,7 @@ values: - mo_income_tax_before_refundable_credits - ms_income_tax_before_credits_unit - mt_income_tax_before_refundable_credits_unit - - nc_income_tax + - nc_income_tax_before_refundable_credits - nd_income_tax_before_refundable_credits - ne_income_tax_before_refundable_credits - nh_income_tax_before_refundable_credits diff --git a/policyengine_us/parameters/gov/states/household/state_refundable_credits.yaml b/policyengine_us/parameters/gov/states/household/state_refundable_credits.yaml index 75c3f95eee0..4dc2984441f 100644 --- a/policyengine_us/parameters/gov/states/household/state_refundable_credits.yaml +++ b/policyengine_us/parameters/gov/states/household/state_refundable_credits.yaml @@ -26,7 +26,7 @@ values: - mo_refundable_credits # Missouri. - ms_refundable_credits # Mississippi. - mt_refundable_credits # Montana. - # Skip NC which has no refundable credits. + - nc_refundable_credits # North Carolina. - nd_refundable_credits # North Dakota. - ne_refundable_credits # Nebraska. - nh_refundable_credits # New Hampshire. diff --git a/policyengine_us/parameters/gov/states/mo/tax/income/deductions/federal_income_tax/ignored_credits.yaml b/policyengine_us/parameters/gov/states/mo/tax/income/deductions/federal_income_tax/ignored_credits.yaml index f82c34178f1..e32c3b86053 100644 --- a/policyengine_us/parameters/gov/states/mo/tax/income/deductions/federal_income_tax/ignored_credits.yaml +++ b/policyengine_us/parameters/gov/states/mo/tax/income/deductions/federal_income_tax/ignored_credits.yaml @@ -4,23 +4,27 @@ description: Missouri ignores the following credits in federal income tax deduct # Part I Line 3, and the net premium tax credit (Schedule 3 Line 9). Credits # claimed on Line 28 (refundable CTC) and Line 30 (recovery rebate), and the # 2021 refundable CDCC (Schedule 3 Line 13g), never reduce Line 22, so they -# are added back here. The EIC is not added back: the Line 9 instructions say +# are added back here. So is the section 1341 credit for repaid claim of right +# income (Schedule 3 Line 13b). The EIC is not added back: the Line 9 instructions say # "If you have an earned income credit, you must subtract the credit from the # tax on your federal return." values: 2018-01-01: - recovery_rebate_credit - refundable_ctc + - claim_of_right_credit # The CDCC is a refundable credit only in 2021 (ARPA); in other years it # already reduces income tax before refundable credits, so adding it back # would overstate the base. 2021-01-01: - recovery_rebate_credit - refundable_ctc + - claim_of_right_credit - cdcc 2022-01-01: - recovery_rebate_credit - refundable_ctc + - claim_of_right_credit metadata: unit: list label: Missouri ignored credits in federal income tax deduction diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml index c4744269768..99830bfe248 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/misc_deductions.yaml @@ -6,6 +6,8 @@ description: Montana counts these sources as miscellaneous deductions. values: 2021-01-01: - casualty_loss_deduction + # Form 2 itemized deductions line 18 follows federal Schedule A line 16. + - mt_claim_of_right_deduction 2024-01-01: [] metadata: @@ -13,6 +15,8 @@ metadata: label: Montana miscellaneous deductions period: year reference: + - title: 2023 Montana Form 2 instructions, itemized deductions line 18 + href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=39 - title: Montana Individual Income Tax Return (Form 2) 2024 href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=8 - title: Montana Individual Income Tax Return (Form 2) 2022 diff --git a/policyengine_us/parameters/gov/states/nc/tax/income/credits/refundable.yaml b/policyengine_us/parameters/gov/states/nc/tax/income/credits/refundable.yaml new file mode 100644 index 00000000000..8d654cd21be --- /dev/null +++ b/policyengine_us/parameters/gov/states/nc/tax/income/credits/refundable.yaml @@ -0,0 +1,13 @@ +description: North Carolina provides these refundable income tax credits and payments. +values: + 1995-01-01: + - nc_claim_of_right_payment +metadata: + unit: list + period: year + label: North Carolina refundable income tax credits + reference: + - title: N.C. Gen. Stat. § 105-266.2 + href: https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-266.2.html + - title: S.L. 1997-213, Section 2 (effective for taxable years beginning on or after January 1, 1995) + href: https://www.ncleg.gov/EnactedLegislation/SessionLaws/HTML/1997-1998/SL1997-213.html diff --git a/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/applicable.yaml b/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/applicable.yaml index 2c9e4102ca5..27407e3bc2e 100644 --- a/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/applicable.yaml +++ b/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/applicable.yaml @@ -7,11 +7,15 @@ values: - real_estate_taxes - casualty_loss_deduction - va_capped_state_and_local_sales_or_income_tax + # Schedule A line 16b, other itemized deductions. + - claim_of_right_deduction metadata: unit: list period: year label: Virginia itemized deductions reduction applicable deduction sources reference: + - title: "2025 Virginia Schedule A Itemized Deductions, line 16b and Limited Itemized Deduction Worksheet line 1 #page 1-2" + href: https://www.tax.virginia.gov/sites/default/files/taxforms/individual-income-tax/2025/schedule-2025.pdf#page=1 - title: 2024 Virginia Schedule A Itemized Deductions href: https://www.tax.virginia.gov/sites/default/files/taxforms/individual-income-tax/2024/schedule-2024.pdf#page=2 - title: 2023 Virginia Schedule A Itemized Deductions diff --git a/policyengine_us/reforms/states/nc/cdcc/nc_cdcc_reform.py b/policyengine_us/reforms/states/nc/cdcc/nc_cdcc_reform.py index 94e4efaf931..5ef1728228b 100644 --- a/policyengine_us/reforms/states/nc/cdcc/nc_cdcc_reform.py +++ b/policyengine_us/reforms/states/nc/cdcc/nc_cdcc_reform.py @@ -23,41 +23,35 @@ class nc_refundable_credits(Variable): unit = USD definition_period = YEAR defined_for = StateCode.NC + # Baseline computes this via `adds`; replacing it with a + # formula requires clearing the inherited computation mode + # (the core engine rejects `formula` + `adds`/`subtracts`). + adds = None + subtracts = None def formula(tax_unit, period, parameters): + # Preserve the baseline refundable credits and ADD the new + # CDCC (see #8775). Baseline nc_income_tax subtracts + # nc_refundable_credits and excludes nc_use_tax, which + # state_use_tax already counts, so no override of it is needed. + baseline_credits = parameters( + period + ).gov.states.nc.tax.income.credits.refundable + total = add(tax_unit, period, list(baseline_credits)) + tax_unit( + "nc_cdcc", period + ) # Stack with nc_eitc when the NC EITC contrib reform is also # active, so enabling both NC contrib credits sums rather than # overwrites. - total = tax_unit("nc_cdcc", period) variables = tax_unit.simulation.tax_benefit_system.variables if "nc_eitc" in variables: total = total + tax_unit("nc_eitc", period) return total - class nc_income_tax(Variable): - value_type = float - entity = TaxUnit - label = "North Carolina income tax" - unit = USD - definition_period = YEAR - defined_for = StateCode.NC - - def formula(tax_unit, period, parameters): - tax_before_credits = add( - tax_unit, period, ["nc_income_tax_before_credits", "nc_use_tax"] - ) - non_refundable_credits = tax_unit("nc_non_refundable_credits", period) - tax_before_refundable = max_(0, tax_before_credits - non_refundable_credits) - - refundable_credits = tax_unit("nc_refundable_credits", period) - - return tax_before_refundable - refundable_credits - class reform(Reform): def apply(self): self.update_variable(nc_cdcc) self.update_variable(nc_refundable_credits) - self.update_variable(nc_income_tax) return reform diff --git a/policyengine_us/reforms/states/nc/eitc/nc_eitc_reform.py b/policyengine_us/reforms/states/nc/eitc/nc_eitc_reform.py index c483b1dea26..31eff7aba40 100644 --- a/policyengine_us/reforms/states/nc/eitc/nc_eitc_reform.py +++ b/policyengine_us/reforms/states/nc/eitc/nc_eitc_reform.py @@ -23,35 +23,28 @@ class nc_refundable_credits(Variable): unit = USD definition_period = YEAR defined_for = StateCode.NC + # Baseline computes this via `adds`; replacing it with a + # formula requires clearing the inherited computation mode + # (the core engine rejects `formula` + `adds`/`subtracts`). + adds = None + subtracts = None def formula(tax_unit, period, parameters): - return tax_unit("nc_eitc", period) - - class nc_income_tax(Variable): - value_type = float - entity = TaxUnit - label = "North Carolina income tax" - unit = USD - definition_period = YEAR - defined_for = StateCode.NC - - def formula(tax_unit, period, parameters): - # Mirror baseline nc_income_tax, which uses - # nc_income_tax_before_credits and does NOT add nc_use_tax. This - # reform's only change is subtracting the new refundable EITC; - # adding use tax here would raise tax for every filer (even those - # with no EITC). See #8775. - tax_before_credits = tax_unit("nc_income_tax_before_credits", period) - non_refundable_credits = tax_unit("nc_non_refundable_credits", period) - tax_before_refundable = max_(0, tax_before_credits - non_refundable_credits) - refundable_credits = tax_unit("nc_refundable_credits", period) - return tax_before_refundable - refundable_credits + # Preserve the baseline refundable credits and ADD the new + # EITC. Baseline nc_income_tax subtracts nc_refundable_credits + # and excludes nc_use_tax, so no override of it is needed. + # See #8775. + baseline_credits = parameters( + period + ).gov.states.nc.tax.income.credits.refundable + return add(tax_unit, period, list(baseline_credits)) + tax_unit( + "nc_eitc", period + ) class reform(Reform): def apply(self): self.update_variable(nc_eitc) self.update_variable(nc_refundable_credits) - self.update_variable(nc_income_tax) return reform diff --git a/policyengine_us/tests/core/test_override_branches.py b/policyengine_us/tests/core/test_override_branches.py index 9e46a63644e..7abea0e424a 100644 --- a/policyengine_us/tests/core/test_override_branches.py +++ b/policyengine_us/tests/core/test_override_branches.py @@ -29,7 +29,10 @@ on the seeded sample and on random households (Hypothesis). 3. Each comparison branch equals a fresh simulation that sets the overridden input before anything is calculated (a differential test of the branch - against the reference path). + against the reference path). These households repay no claim of right + income; with a qualifying repayment the itemizing branches also fix the + claim of right method, so a reference must fix it too + (``test_claim_of_right_section_1341_properties.py`` covers that case). 4. The same holds when the parent has already calculated the overridden input, and when an earlier year was calculated first. 5. When the branch drops what it copied, it keeps exactly the values set as diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_credit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_credit.yaml new file mode 100644 index 00000000000..511377a29dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_credit.yaml @@ -0,0 +1,26 @@ +- name: Case 1, the credit is the prior-year decrease when tax is computed under section 1341(a)(5). + period: 2025 + input: + claim_of_right_section_1341_eligible: true + claim_of_right_credit_applies: true + claim_of_right_prior_year_tax_decrease: 1_234 + output: + claim_of_right_credit: 1_234 + +- name: Case 2, no credit under section 1341(a)(4). + period: 2025 + input: + claim_of_right_section_1341_eligible: true + claim_of_right_credit_applies: false + claim_of_right_prior_year_tax_decrease: 1_234 + output: + claim_of_right_credit: 0 + +- name: Case 3, no credit without a qualifying repayment. + period: 2025 + input: + claim_of_right_section_1341_eligible: false + claim_of_right_credit_applies: true + claim_of_right_prior_year_tax_decrease: 1_234 + output: + claim_of_right_credit: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_credit_applies.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_credit_applies.yaml new file mode 100644 index 00000000000..d380d895243 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_credit_applies.yaml @@ -0,0 +1,128 @@ +# 26 CFR 1.1341-1(b)(3): when the two computations give the same tax, tax is +# computed under 1341(a)(4), with the deduction. The household is +# Publication 525 (2025) Example 40: $5,155.50 with the deduction and $5,903 +# without it. +- name: Case 1, equal tax under both methods keeps the deduction. + period: 2025 + input: + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + claim_of_right_repayment: 5_000 + # $5,903 - $747.50 = $5,155.50, the tax with the deduction. + claim_of_right_prior_year_tax_decrease: 747.5 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + income_tax_if_claiming_claim_of_right_credit: 5_155.5 + income_tax_if_claiming_claim_of_right_deduction: 5_155.5 + claim_of_right_credit_applies: false + income_tax: 5_155.5 + +- name: Case 2, a prior-year decrease 50 cents larger makes the credit apply. + period: 2025 + input: + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + claim_of_right_repayment: 5_000 + claim_of_right_prior_year_tax_decrease: 748 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_credit_applies: true + claim_of_right_deduction: 0 + claim_of_right_credit: 748 + income_tax: 5_155 + +- name: Case 3, a repayment of $3,000 cannot use section 1341. + period: 2025 + input: + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + claim_of_right_repayment: 3_000 + claim_of_right_prior_year_tax_decrease: 2_000 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_credit_applies: false + claim_of_right_deduction: 0 + claim_of_right_credit: 0 + # Section 67(g) (now 67(h)) disallows the miscellaneous deduction, so tax is + # unchanged at the tax on $49,950. + income_tax: 5_903 + +- name: Case 4, with no prior-year decrease the deduction applies. + period: 2025 + input: + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + # $3,001 is the smallest whole-dollar repayment that qualifies. + claim_of_right_repayment: 3_001 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_credit_applies: false + claim_of_right_deduction: 3_001 + # $65,700 - $15,750 - $3,001 = $46,949; $1,192.50 + 12% x $35,024. + taxable_income: 46_949 + income_tax: 5_395.38 + +- name: Case 5, the credit applies when it gives one cent less tax. + period: 2025 + input: + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + claim_of_right_repayment: 5_000 + # $5,903 - $747.51 = $5,155.49, one cent below the tax with the deduction. + claim_of_right_prior_year_tax_decrease: 747.51 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_credit_applies: true + claim_of_right_credit: 747.51 + income_tax: 5_155.49 + +- name: Case 6, the method is the same when itemizing is decided by comparing deductions. + # Publication 525 (2025) Example 40 with + # gov.simulation.branch_to_determine_itemization off. + period: 2025 + input: + gov.simulation.branch_to_determine_itemization: false + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + claim_of_right_repayment: 5_000 + claim_of_right_prior_year_tax_decrease: 568 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + income_tax_if_claiming_claim_of_right_deduction: 5_155.5 + income_tax_if_claiming_claim_of_right_credit: 5_335 + claim_of_right_credit_applies: false + income_tax: 5_155.5 + +- name: Case 7, the credit applies for a standard deduction filer when itemizing is decided by comparing deductions. + period: 2025 + input: + gov.simulation.branch_to_determine_itemization: false + age: 40 + employment_income: 40_000 + claim_of_right_repayment: 10_000 + claim_of_right_prior_year_tax_decrease: 2_200 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_credit_applies: true + claim_of_right_credit: 2_200 + income_tax: 471.5 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_section_1341_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_section_1341_eligible.yaml new file mode 100644 index 00000000000..701c7120800 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/claim_of_right_section_1341_eligible.yaml @@ -0,0 +1,51 @@ +# 26 U.S.C. 1341(a)(3): the deduction must exceed $3,000. Publication 525 +# (2025), PDF page 36: "consider the total amount being repaid on the +# return. Each instance of repayment isn't considered separately." +- name: Case 1, a repayment of exactly $3,000 does not qualify. + period: 2025 + input: + tax_unit_claim_of_right_repayment: 3_000 + output: + claim_of_right_section_1341_eligible: false + +- name: Case 2, a repayment of $3,000.01 qualifies. + period: 2025 + input: + tax_unit_claim_of_right_repayment: 3_000.01 + output: + claim_of_right_section_1341_eligible: true + +- name: Case 3, no repayment. + period: 2025 + input: + tax_unit_claim_of_right_repayment: 0 + output: + claim_of_right_section_1341_eligible: false + +- name: Case 4, two repayments of $2,000 on a joint return qualify together. + period: 2025 + input: + people: + person1: + age: 45 + claim_of_right_repayment: 2_000 + person2: + age: 44 + claim_of_right_repayment: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + tax_unit_claim_of_right_repayment: 4_000 + claim_of_right_section_1341_eligible: true + +- name: Case 5, the $3,000 threshold applied before 2018 too. + period: 2017 + input: + tax_unit_claim_of_right_repayment: 3_001 + output: + claim_of_right_section_1341_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/integration.yaml new file mode 100644 index 00000000000..4d457ec6996 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/integration.yaml @@ -0,0 +1,161 @@ +# Publication 525, Repayments, Example 40, in three editions. A single filer +# repaid $5,000 of income included in the prior year's income under a claim +# of right. Taxable income is $49,950 without the deduction and $44,950 with +# it; excluding the $5,000 cut the prior year's tax by $568 (2025, 2024) or +# $750 (2018). In each edition method 1, the deduction, gives less tax. +# Wages and charitable gifts are chosen to reproduce those taxable incomes: +# without the repayment, itemized deductions equal the standard deduction. +# Publication 525 figures tax with the Tax Table in 2024 and 2018 (the +# midpoint of each $50 row); PolicyEngine applies the rate schedule. + +- name: Case 1, Publication 525 (2025) Example 40, the deduction gives less tax. + # https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36 + period: 2025 + input: + age: 40 + employment_income: 65_700 + charitable_cash_donations: 15_750 + claim_of_right_repayment: 5_000 + claim_of_right_prior_year_tax_decrease: 568 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_section_1341_eligible: true + # Method 1: tax on $44,950 = $1,192.50 + 12% x ($44,950 - $11,925). + # Publication 525 rounds $5,155.50 to $5,156. + income_tax_if_claiming_claim_of_right_deduction: 5_155.5 + # Method 2: tax on $49,950, $5,903, less the $568 prior-year decrease. + income_tax_if_claiming_claim_of_right_credit: 5_335 + claim_of_right_credit_applies: false + claim_of_right_deduction: 5_000 + claim_of_right_credit: 0 + taxable_income: 44_950 + income_tax: 5_155.5 + +- name: Case 2, Publication 525 (2024) Example 40, the deduction gives less tax. + # https://www.irs.gov/pub/irs-prior/p525--2024.pdf#page=37 + period: 2024 + input: + age: 40 + employment_income: 64_550 + charitable_cash_donations: 14_600 + claim_of_right_repayment: 5_000 + claim_of_right_prior_year_tax_decrease: 568 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + # $1,160 + 12% x ($44,950 - $11,600) = $5,162; the Tax Table gives + # $5,165. + income_tax_if_claiming_claim_of_right_deduction: 5_162 + # $1,160 + $4,266 + 22% x ($49,950 - $47,150) = $6,042 (Tax Table + # $6,048), less $568 = $5,474 (Publication 525: $5,480). + income_tax_if_claiming_claim_of_right_credit: 5_474 + claim_of_right_credit_applies: false + claim_of_right_deduction: 5_000 + taxable_income: 44_950 + income_tax: 5_162 + +- name: Case 3, Publication 525 (2018) Example 40, the deduction gives less tax. + # https://www.irs.gov/pub/irs-prior/p525--2018.pdf#page=34 + period: 2018 + input: + age: 40 + employment_income: 61_950 + charitable_cash_donations: 12_000 + claim_of_right_repayment: 5_000 + claim_of_right_prior_year_tax_decrease: 750 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + # $952.50 + $3,501 + 22% x ($44,950 - $38,700) = $5,828.50 (Tax Table + # $5,834). + income_tax_if_claiming_claim_of_right_deduction: 5_828.5 + # $4,453.50 + 22% x ($49,950 - $38,700) = $6,928.50 (Tax Table $6,934), + # less $750 = $6,178.50 (Publication 525: $6,184). + income_tax_if_claiming_claim_of_right_credit: 6_178.5 + claim_of_right_credit_applies: false + claim_of_right_deduction: 5_000 + taxable_income: 44_950 + income_tax: 5_828.5 + +- name: Case 4, a filer who takes the standard deduction gains only from the credit. + # Publication 525 (2025) method 2 steps: tax without the deduction, less + # the decrease in the earlier year's tax. + period: 2025 + input: + age: 40 + employment_income: 40_000 + claim_of_right_repayment: 10_000 + claim_of_right_prior_year_tax_decrease: 2_200 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + # Itemizing $10,000 loses to the $15,750 standard deduction, so the + # deduction leaves tax on $24,250 at $1,192.50 + 12% x $12,325. + income_tax_if_claiming_claim_of_right_deduction: 2_671.5 + income_tax_if_claiming_claim_of_right_credit: 471.5 + claim_of_right_credit_applies: true + claim_of_right_deduction: 0 + claim_of_right_credit: 2_200 + taxable_income: 24_250 + income_tax: 471.5 + +- name: Case 5, a prior-year decrease above the year's tax is refunded. + # 26 U.S.C. 1341(b)(1): the excess is considered a payment of tax. + period: 2025 + input: + age: 40 + employment_income: 40_000 + claim_of_right_repayment: 10_000 + claim_of_right_prior_year_tax_decrease: 3_000 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_credit_applies: true + claim_of_right_credit: 3_000 + income_tax_before_refundable_credits: 2_671.5 + income_tax: -328.5 + +- name: Case 6, a joint return counts the spouses' repayments, not a dependent's. + period: 2025 + input: + people: + person1: + age: 45 + employment_income: 90_000 + claim_of_right_repayment: 2_000 + person2: + age: 44 + claim_of_right_repayment: 2_000 + person3: + age: 30 + is_tax_unit_dependent: true + claim_of_right_repayment: 9_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + # The decrease in the couple's earlier joint tax from excluding the + # $4,000 they repaid. + claim_of_right_prior_year_tax_decrease: 500 + state_sales_tax: 0 + local_sales_tax: 0 + households: + household: + members: [person1, person2, person3] + state_code: TX + output: + tax_unit_claim_of_right_repayment: 4_000 + claim_of_right_section_1341_eligible: true + # Taxable income $90,000 - $31,500 = $58,500; tax $2,385 + 12% x + # $34,650 = $6,543, less the $500 credit for other dependents. The + # $4,000 deduction loses to the standard deduction. + income_tax_if_claiming_claim_of_right_deduction: 6_043 + income_tax_if_claiming_claim_of_right_credit: 5_543 + claim_of_right_credit_applies: true + claim_of_right_credit: 500 + income_tax: 5_543 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/tax_unit_claim_of_right_repayment.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/tax_unit_claim_of_right_repayment.yaml new file mode 100644 index 00000000000..20e1e01985c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/claim_of_right/tax_unit_claim_of_right_repayment.yaml @@ -0,0 +1,40 @@ +- name: Case 1, the head's and spouse's repayments add up. + period: 2025 + input: + people: + person1: + age: 45 + claim_of_right_repayment: 2_000 + person2: + age: 44 + claim_of_right_repayment: 1_500 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + tax_unit_claim_of_right_repayment: 3_500 + +- name: Case 2, a dependent's repayment belongs on the dependent's own return. + period: 2025 + input: + people: + person1: + age: 45 + claim_of_right_repayment: 1_000 + person2: + age: 20 + is_tax_unit_dependent: true + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + tax_unit_claim_of_right_repayment: 1_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/claim_of_right_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/claim_of_right_deduction.yaml new file mode 100644 index 00000000000..8afcaf3dcbd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/claim_of_right_deduction.yaml @@ -0,0 +1,69 @@ +- name: Case 1, the whole repayment is deductible under section 1341(a)(4). + period: 2025 + input: + tax_unit_claim_of_right_repayment: 8_000 + claim_of_right_section_1341_eligible: true + claim_of_right_credit_applies: false + output: + claim_of_right_deduction: 8_000 + +- name: Case 2, no deduction under section 1341(a)(5). + # 26 U.S.C. 1341(b)(3). + period: 2025 + input: + tax_unit_claim_of_right_repayment: 8_000 + claim_of_right_section_1341_eligible: true + claim_of_right_credit_applies: true + output: + claim_of_right_deduction: 0 + +- name: Case 3, a repayment of $3,000 or less is not a section 1341 deduction. + period: 2025 + input: + tax_unit_claim_of_right_repayment: 3_000 + claim_of_right_section_1341_eligible: false + claim_of_right_credit_applies: false + output: + claim_of_right_deduction: 0 + +- name: Case 4, the deduction is subject to the overall limitation on itemized deductions before 2018. + # 2017 Schedule A instructions, Itemized Deductions Worksheet, PDF page 14: + # line 2 removes only gambling and casualty or theft losses on line 28, so + # the line 28 claim of right deduction stays in the limited base. + period: 2017 + input: + age: 40 + employment_income: 400_000 + claim_of_right_repayment: 50_000 + # Federal income tax is modeled from 2018, so fix the method here + # instead of deriving it from 2017 tax. + claim_of_right_credit_applies: false + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_deduction: 50_000 + total_itemized_taxable_income_deductions: 50_000 + # min(3% x ($400,000 - $261,500), 80% x $50,000) = $4,155. + itemized_taxable_income_deductions_reduction: 4_155 + itemized_taxable_income_deductions: 45_845 + +- name: Case 5, the deduction is subject to the limitation on itemized deductions from 2026. + # 26 U.S.C. 68 as amended by P.L. 119-21, sec. 70111: 2/37 of the lesser + # of itemized deductions and taxable income plus itemized deductions above + # the $640,600 start of the 37% bracket for single filers in 2026. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 1_000_000 + claim_of_right_repayment: 100_000 + state_sales_tax: 0 + local_sales_tax: 0 + state_code: TX + output: + claim_of_right_deduction: 100_000 + total_itemized_taxable_income_deductions: 100_000 + # 2/37 x min($100,000, $1,000,000 - $640,600) = $5,405.41. + itemized_taxable_income_deductions_reduction: 5_405.41 + itemized_taxable_income_deductions: 94_594.59 diff --git a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.yaml index 3cfeaae9e08..5f1570845b9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.yaml @@ -127,6 +127,22 @@ # min($8,000, $10,000) = $8,000 az_itemized_deductions: 8_000 +- name: The federal claim of right repayment deduction gives no Arizona deduction + # A.R.S. 43-1021(9) adds the section 1341(a)(4) deduction back to Arizona + # gross income; A.R.S. 43-1029 computes Arizona tax without it. + absolute_error_margin: 0.01 + period: 2025 + input: + state_code: AZ + interest_deduction: 6_000 + claim_of_right_deduction: 5_000 + salt_deduction: 0 + charitable_deduction: 0 + az_charitable_contributions_credit_potential: 0 + itemized_medical_expenses: 0 + output: + az_itemized_deductions: 6_000 + - name: Arizona excludes self-employed premiums from its medical deduction without an AGI floor period: 2025 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.yaml index 01b7b1b685f..d490ca69c44 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.yaml @@ -413,3 +413,15 @@ # The federal deduction, now positive, is replaced by California's 4,900, # not added to it. ca_itemized_deductions_pre_limitation: 4_900 + +- name: The federal claim of right repayment deduction does not carry over. + # 2025 Schedule CA (540) instructions, Part II line 16: California allows + # its own deduction or credit, and neither for income it did not tax. + period: 2025 + input: + state_code: CA + interest_deduction: 6_000 + claim_of_right_deduction: 5_000 + output: + itemized_deductions_less_salt: 11_000 + ca_itemized_deductions_pre_limitation: 6_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_itemized_deductions.yaml index 63a6603f62b..bdcf4857084 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ks/tax/income/ks_itemized_deductions.yaml @@ -19,3 +19,16 @@ state_code: KS output: ks_itemized_deductions: 12 + 9 + 2 + +- name: KS itemized deductions leave out the federal claim of right repayment deduction + # K.S.A. 79-32,120(a) lists charitable contributions, medical care, + # qualified residence interest and property taxes only. + period: 2025 + input: + real_estate_taxes: 3_000 + interest_deduction: 9_000 + claim_of_right_deduction: 5_000 + state_code: KS + output: + itemized_deductions_less_salt: 14_000 + ks_itemized_deductions: 12_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_federal_income_tax_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_federal_income_tax_deduction.yaml index b1a7ebea29c..e1e12da80db 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_federal_income_tax_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_federal_income_tax_deduction.yaml @@ -166,3 +166,20 @@ state_code: MO output: mo_federal_income_tax_deduction: 0 + +- name: The section 1341 credit does not reduce the tax on the federal return + # The MO-1040 Line 9 chart starts from Form 1040 Line 22 and does not + # subtract Schedule 3 Line 13b. Income tax of 340 is 1,840 on Line 22 less + # the 1,500 credit; 25% of 1,840 is 460. + period: 2025 + absolute_error_margin: 0.01 + input: + mo_adjusted_gross_income: 30_000 + income_tax: 340 + claim_of_right_credit: 1_500 + recovery_rebate_credit: 0 + refundable_ctc: 0 + filing_status: SINGLE + state_code: MO + output: + mo_federal_income_tax_deduction: 460 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_claim_of_right_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_claim_of_right_deduction.yaml new file mode 100644 index 00000000000..b1051767977 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_claim_of_right_deduction.yaml @@ -0,0 +1,82 @@ +# 2023 Form 2 instructions, itemized deductions line 18: "These deductions +# are the same deductions that are allowed on your federal Form 1040, +# Schedule A, line 16." +- name: Case 1, Montana follows the federal deduction through 2023. + period: 2023 + input: + people: + person1: + age: 40 + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [person1] + claim_of_right_deduction: 5_000 + households: + household: + members: [person1] + state_code: MT + output: + mt_claim_of_right_deduction: 5_000 + mt_misc_deductions: 5_000 + +- name: Case 2, no Montana deduction when the federal deduction is zero. + period: 2023 + input: + people: + person1: + age: 40 + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [person1] + claim_of_right_deduction: 0 + households: + household: + members: [person1] + state_code: MT + output: + mt_claim_of_right_deduction: 0 + +- name: Case 3, each spouse deducts their own repayment and a dependent's is left out. + period: 2023 + input: + people: + person1: + age: 45 + claim_of_right_repayment: 3_000 + person2: + age: 44 + claim_of_right_repayment: 1_500 + person3: + age: 20 + is_tax_unit_dependent: true + claim_of_right_repayment: 9_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + claim_of_right_deduction: 4_500 + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + mt_claim_of_right_deduction: [3_000, 1_500, 0] + +- name: Case 4, from 2024 the miscellaneous deductions list is empty. + period: 2024 + input: + people: + person1: + age: 40 + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [person1] + claim_of_right_deduction: 5_000 + households: + household: + members: [person1] + state_code: MT + output: + mt_misc_deductions: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_payment.yaml new file mode 100644 index 00000000000..a2181be2bc1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_payment.yaml @@ -0,0 +1,32 @@ +# N.C. Gen. Stat. 105-266.2: a filer subject to section 1341(a)(5) is +# considered to have paid North Carolina tax equal to the increase in the +# earlier year's North Carolina tax from including the restored item. +- name: Case 1, the payment is the prior-year North Carolina tax increase under section 1341(a)(5). + period: 2025 + input: + claim_of_right_credit_applies: true + nc_claim_of_right_prior_year_tax_increase: 300 + state_code: NC + output: + nc_claim_of_right_payment: 300 + +- name: Case 2, no payment under section 1341(a)(4). + period: 2025 + input: + claim_of_right_credit_applies: false + nc_claim_of_right_prior_year_tax_increase: 300 + state_code: NC + output: + nc_claim_of_right_payment: 0 + +- name: Case 3, the payment is modeled for current North Carolina residents only. + # A coverage limit of the model, not a rule of G.S. 105-266.2: a filer who + # paid the earlier North Carolina tax and has since moved away is not + # covered. + period: 2025 + input: + claim_of_right_credit_applies: true + nc_claim_of_right_prior_year_tax_increase: 300 + state_code: SC + output: + nc_claim_of_right_payment: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.yaml index b0eafe19c2f..3341d040242 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.yaml @@ -369,3 +369,122 @@ nc_taxable_income: 63_000 # 63,000 * 4.25% = 2,677.50 (2,858.13 without the repayment). nc_income_tax: 2_677.50 + +- name: No deduction when federal tax is computed under section 1341(a)(5). + period: 2025 + input: + people: + head: + age: 40 + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [head] + adjusted_gross_income: 80_000 + claim_of_right_credit_applies: true + households: + household: + members: [head] + state_code: NC + output: + nc_claim_of_right_deduction: 0 + +- name: The deduction is allowed when federal tax is computed under section 1341(a)(4). + period: 2025 + input: + people: + head: + age: 40 + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [head] + adjusted_gross_income: 80_000 + claim_of_right_credit_applies: false + households: + household: + members: [head] + state_code: NC + output: + nc_claim_of_right_deduction: 5_000 + +- name: A filer the federal credit method suits loses the North Carolina deduction and is treated as paying the prior-year North Carolina tax on the item. + # The household above, now with a $1,100 decrease in 2024 federal tax and + # a $212.50 increase in 2024 North Carolina tax from the repaid $5,000. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 80_000 + claim_of_right_repayment: 5_000 + real_estate_taxes: 4_000 + tax_units: + tax_unit: + members: [head] + charitable_deduction: 8_000 + claim_of_right_prior_year_tax_decrease: 1_100 + nc_claim_of_right_prior_year_tax_increase: 212.5 + households: + household: + members: [head] + state_code: NC + output: + claim_of_right_credit_applies: true + claim_of_right_credit: 1_100 + nc_claim_of_right_deduction: 0 + # 4,000 + 8,000 = 12,000 loses to the 12,750 standard deduction. + nc_standard_or_itemized_deductions: 12_750 + nc_taxable_income: 67_250 + # 67,250 * 4.25% = 2,858.13, less the 212.50 payment. + nc_income_tax_before_refundable_credits: 2_858.13 + nc_claim_of_right_payment: 212.5 + nc_income_tax: 2_645.63 + +- name: A filer the federal deduction method suits keeps the North Carolina deduction and gets no payment. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 80_000 + claim_of_right_repayment: 5_000 + real_estate_taxes: 4_000 + tax_units: + tax_unit: + members: [head] + charitable_deduction: 8_000 + claim_of_right_prior_year_tax_decrease: 568 + nc_claim_of_right_prior_year_tax_increase: 212.5 + households: + household: + members: [head] + state_code: NC + output: + claim_of_right_credit_applies: false + nc_claim_of_right_deduction: 5_000 + nc_claim_of_right_payment: 0 + nc_income_tax: 2_677.50 + +- name: Before 2018 a repayment above $3,000 with no prior-year decrease keeps the deduction. + # No method is supplied: with no prior-year tax decrease the credit method + # cannot give less tax, so tax is computed with the deduction. + period: 2016 + input: + people: + head: + age: 40 + claim_of_right_repayment: 5_000 + tax_units: + tax_unit: + members: [head] + adjusted_gross_income: 50_000 + households: + household: + members: [head] + state_code: NC + output: + claim_of_right_credit_applies: false + nc_claim_of_right_deduction: 5_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_income_tax.yaml index 6957ae44fac..02a5fb29344 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/nc_income_tax.yaml @@ -27,3 +27,15 @@ state_code: NC output: nc_income_tax: 0 + +- name: The refundable claim of right payment can make North Carolina income tax negative + period: 2025 + input: + nc_income_tax_before_credits: 100 + nc_non_refundable_credits: 30 + nc_claim_of_right_payment: 500 + state_code: NC + output: + nc_income_tax_before_refundable_credits: 70 + nc_refundable_credits: 500 + nc_income_tax: -430 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..380a592755c 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 @@ -112,3 +112,18 @@ output: # 50% x 8,750 or_federal_tax_liability_subtraction: 4_375 + +- name: The section 1341 credit does not reduce the federal tax liability subtraction + # 2025 Publication OR-17, federal tax worksheet: Form 1040 line 22 less the + # American Opportunity, refundable adoption and premium tax credits only. + period: 2025 + input: + state_code: OR + filing_status: SINGLE + adjusted_gross_income: 60_000 + # 4,000 on Form 1040 line 22 less a 1,500 section 1341 credit. + income_tax: 2_500 + eitc: 0 + claim_of_right_credit: 1_500 + output: + or_federal_tax_liability_subtraction: 4_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml index 80257f26b5c..2e42bfbd9c8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml @@ -40,3 +40,20 @@ state_code: VA output: va_reduced_itemized_deductions: 35_282 + +- name: The federal claim of right repayment deduction stays in the limited itemized deduction total + # 2025 Schedule A line 16b is in line 1 of the Limited Itemized Deduction + # Worksheet and not in line 2. + period: 2025 + absolute_error_margin: 0.01 + input: + filing_status: SINGLE + adjusted_gross_income: 500_000 # > $332,700 + charitable_deduction: 20_000 + claim_of_right_deduction: 10_000 + state_and_local_sales_or_income_tax: 0 + state_code: VA + output: + # Line 1: 30,000. Line 9: min(80% x 30,000, 3% x (500,000 - 332,700)) + # = 5,019. Line 12a: 30,000 - 5,019. + va_reduced_itemized_deductions: 24_981 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_itemized_deduction_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_itemized_deduction_credit.yaml index 38850b7f5cd..72cc1f65438 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_itemized_deduction_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_itemized_deduction_credit.yaml @@ -25,3 +25,17 @@ state_code: WI output: wi_itemized_deduction_credit_potential: 0 + +- name: WI itemized deduction credit leaves out the federal claim of right repayment deduction + # Wis. Stat. 71.07(5)(a)7; Schedule 1 has lines for medical expenses, + # interest, gifts to charity and casualty losses only. + absolute_error_margin: 0.01 + period: 2025 + input: + wi_standard_deduction: 10_000 + charitable_deduction: 3_000 + interest_deduction: 12_000 + claim_of_right_deduction: 20_000 + state_code: WI + output: + wi_itemized_deduction_credit_potential: 250 # = (3K + 12K - 10K) * 5% diff --git a/policyengine_us/tests/policy/contrib/states/nc/cdcc/nc_cdcc.yaml b/policyengine_us/tests/policy/contrib/states/nc/cdcc/nc_cdcc.yaml index 9dab37908b0..8a0ab67fdcb 100644 --- a/policyengine_us/tests/policy/contrib/states/nc/cdcc/nc_cdcc.yaml +++ b/policyengine_us/tests/policy/contrib/states/nc/cdcc/nc_cdcc.yaml @@ -81,3 +81,63 @@ cdcc_potential: 1_000 output: nc_cdcc: 0 + +- name: NC CDCC reform keeps the baseline claim of right payment + # Regression test for #8775: the reform adds its CDCC to North Carolina's + # baseline refundable credits instead of replacing them. + period: 2026 + reforms: policyengine_us.reforms.states.nc.cdcc.nc_cdcc + input: + gov.contrib.states.nc.cdcc.in_effect: true + state_code: NC + cdcc_potential: 1_000 + nc_claim_of_right_payment: 250 + nc_income_tax_before_credits: 600 + nc_use_tax: 0 + nc_non_refundable_credits: 0 + output: + nc_cdcc: 300 + nc_refundable_credits: 550 + nc_income_tax: 50 + +- name: NC CDCC reform does not add use tax to income tax + # Use tax is counted at the household level through state_use_tax (#8775). + period: 2026 + reforms: policyengine_us.reforms.states.nc.cdcc.nc_cdcc + input: + gov.contrib.states.nc.cdcc.in_effect: true + state_code: NC + cdcc_potential: 1_000 + nc_income_tax_before_credits: 600 + nc_use_tax: 100 + nc_non_refundable_credits: 0 + output: + nc_cdcc: 300 + # 600 - 300 = 300; it would be 400 if use tax were added. + nc_income_tax: 300 + +- name: Both NC contrib credits stack on the claim of right payment, and use tax stays out of income tax + # Review r2 of PolicyEngine/policyengine-us#10038. Refundable credits: + # 250 payment + 100 EITC (10% of 1,000) + 300 CDCC (30% of 1,000) = 650. + period: 2026 + reforms: + - policyengine_us.reforms.states.nc.eitc.nc_eitc + - policyengine_us.reforms.states.nc.cdcc.nc_cdcc + input: + gov.contrib.states.nc.eitc.match: 0.1 + gov.contrib.states.nc.cdcc.in_effect: true + state_code: NC + eitc: 1_000 + cdcc_potential: 1_000 + nc_claim_of_right_payment: 250 + nc_income_tax_before_credits: 600 + nc_use_tax: 100 + nc_non_refundable_credits: 0 + output: + nc_refundable_credits: 650 + # 600 - 650; the 100 of use tax is counted once, through state_use_tax. + nc_income_tax: -50 + state_income_tax: -50 + state_use_tax: 100 + household_state_tax_before_refundable_credits: 700 + household_refundable_state_tax_credits: 650 diff --git a/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc.yaml b/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc.yaml index ed383cb202b..09145e7375a 100644 --- a/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc.yaml +++ b/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc.yaml @@ -49,3 +49,21 @@ nc_eitc: 3_000 nc_refundable_credits: 3_000 nc_income_tax: -2_500 + +- name: NC EITC reform keeps the baseline claim of right payment + # Regression test for #8775: the reform adds its EITC to North Carolina's + # baseline refundable credits instead of replacing them. + period: 2026 + reforms: policyengine_us.reforms.states.nc.eitc.nc_eitc + input: + gov.contrib.states.nc.eitc.match: 0.1 + state_code: NC + eitc: 1_000 + nc_claim_of_right_payment: 250 + nc_income_tax_before_credits: 600 + nc_use_tax: 0 + nc_non_refundable_credits: 0 + output: + nc_eitc: 100 + nc_refundable_credits: 350 + nc_income_tax: 250 diff --git a/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc_excludes_use_tax.yaml b/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc_excludes_use_tax.yaml index bf877dda502..4abc4d48e43 100644 --- a/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc_excludes_use_tax.yaml +++ b/policyengine_us/tests/policy/contrib/states/nc/eitc/nc_eitc_excludes_use_tax.yaml @@ -1,5 +1,5 @@ -# Regression test for #8775: the NC EITC reform's nc_income_tax override must -# mirror baseline (nc_income_tax_before_credits minus credits) and must NOT add +# Regression test for #8775: under the NC EITC reform, nc_income_tax must +# match baseline (nc_income_tax_before_credits minus credits) and must NOT add # nc_use_tax. Use tax is already counted at the household level via # state_use_tax; adding it here would raise tax for every NC filer (even with # no EITC) and double-count it. diff --git a/policyengine_us/tests/unit/test_claim_of_right_section_1341_properties.py b/policyengine_us/tests/unit/test_claim_of_right_section_1341_properties.py new file mode 100644 index 00000000000..574705f8640 --- /dev/null +++ b/policyengine_us/tests/unit/test_claim_of_right_section_1341_properties.py @@ -0,0 +1,457 @@ +"""Branch isolation and cross-case properties of the section 1341 computation. + +26 U.S.C. 1341(a): when a filer repays more than $3,000 of income included in +an earlier year under a claim of right, federal tax is the lesser of (4) the +tax with a deduction for the repayment and (5) the tax without it minus the +decrease in the earlier year's tax from excluding the income. Ties use (4) +(26 CFR 1.1341-1(b)(3)); any excess of the decrease over the tax is refunded +(1341(b)(1)). North Carolina allows no claim of right deduction under +1341(a)(5) (G.S. 105-153.5(a)(2)d) and instead treats the increase in the +earlier year's North Carolina tax as a payment (G.S. 105-266.2). + +The policy cases are in YAML (``gov/irs/credits/claim_of_right``). These tests +cover what a YAML case cannot. The model picks the method by calculating +federal tax in two override branches, so the tests build separate simulations +and compare them: + +- each branch equals a simulation that fixes the method as an input before + anything is calculated, and income tax equals the smaller of the two + (ties to the deduction), where the credit side comes from a simulation with + no repayment at all less the prior-year decrease; +- results do not depend on which variable is calculated first, including + when a tax liability under one itemizing choice is calculated before the + method is known, under both settings of + ``gov.simulation.branch_to_determine_itemization``; the itemizing branches + keep the simulation's method; +- a later year calculated on the same simulation equals a simulation of that + year alone, and an itemizing branch of a simulation whose itemizing input + changed after its tax was calculated does not reuse that tax; +- relations between cases over a grid: tax never rises with the repayment + (also across the $3,000 threshold) or with the prior-year decrease, and a + repayment of $3,000 or less changes nothing (section 67(h), formerly 67(g)). + +They also check, at every grid point, that the deduction is the whole +repayment or nothing, the credit is the whole decrease or nothing, and never +both; that North Carolina's deduction is zero whenever the credit method +applies and otherwise is what it is with the deduction method fixed; and that +its payment is the prior-year increase when the credit method applies and zero +otherwise. + +Last, one itemizing household per state: with the credit method fixed, the +repayment leaves every state's income tax unchanged in the model (no state +input is supplied), and with the deduction method fixed it leaves Arizona's, +California's, Kansas's and Wisconsin's unchanged. +""" + +import gc +import itertools + +import numpy as np +import pytest +from policyengine_core.periods import instant +from policyengine_core.reforms import Reform + +from policyengine_us import CountryTaxBenefitSystem, Simulation +from policyengine_us.variables.household.demographic.geographic.state_code import ( + StateCode, +) + +REPAYMENTS = [0, 2_999, 3_000, 3_001, 5_000, 20_000, 150_000] +DECREASES = [0, 400, 2_500, 12_000, 60_000] +WAGES = [0, 30_000, 80_000, 400_000, 1_500_000] +CHARITY = [0, 20_000] +AXES = [REPAYMENTS, DECREASES, WAGES, CHARITY] +GRID = list(itertools.product(*AXES)) +SHAPE = tuple(len(axis) for axis in AXES) +REPAYMENT, DECREASE, WAGE, CHARITABLE = ( + np.array([point[i] for point in GRID], dtype=float) for i in range(len(AXES)) +) +# An illustrative North Carolina prior-year tax increase on the repaid income. +NC_INCREASE = 0.05 * REPAYMENT +ZEROS = np.zeros_like(REPAYMENT) +THRESHOLD = 3_000 +YEARS = [2025, 2026] +TOLERANCE = 0.01 +# The 50 states and the District of Columbia. +STATES = [ + state.name + for state in StateCode + if state.name not in {"AA", "AE", "AP", "AS", "GU", "MP", "PR", "PW", "VI"} +] +# States whose own rules leave out the federal claim of right repayment +# deduction: A.R.S. 43-1021(9), the California Schedule CA (540) instructions +# for Part II line 16, K.S.A. 79-32,120(a) and Wisconsin Form 1 Schedule 1. +STATES_WITHOUT_THE_DEDUCTION = ["AZ", "CA", "KS", "WI"] +BRANCHES = [ + "income_tax_if_claiming_claim_of_right_deduction", + "income_tax_if_claiming_claim_of_right_credit", +] +VARIABLES = [ + "income_tax", + "tax_unit_itemizes", + "tax_liability_if_itemizing", + "tax_liability_if_not_itemizing", + "claim_of_right_section_1341_eligible", + "claim_of_right_credit_applies", + "claim_of_right_deduction", + "claim_of_right_credit", + "nc_claim_of_right_deduction", + "nc_claim_of_right_payment", + "nc_income_tax_before_refundable_credits", + "nc_income_tax", +] + BRANCHES + + +def grid_simulation( + years, repayment=REPAYMENT, decrease=DECREASE, nc_increase=NC_INCREASE, method=None +): + """One single-person North Carolina tax unit per grid point.""" + + def yearly(value): + return {year: value for year in years} + + situation = {"people": {}, "tax_units": {}, "households": {}} + for i in range(len(GRID)): + person = f"p{i}" + situation["people"][person] = { + "age": yearly(40), + "employment_income": yearly(WAGE[i]), + "charitable_cash_donations": yearly(CHARITABLE[i]), + "claim_of_right_repayment": yearly(float(repayment[i])), + } + tax_unit = { + "members": [person], + "claim_of_right_prior_year_tax_decrease": yearly(float(decrease[i])), + "nc_claim_of_right_prior_year_tax_increase": yearly(float(nc_increase[i])), + } + if method is not None: + tax_unit["claim_of_right_credit_applies"] = yearly(method) + situation["tax_units"][f"t{i}"] = tax_unit + situation["households"][f"h{i}"] = { + "members": [person], + "state_code": yearly("NC"), + } + return Simulation(situation=situation) + + +def calculate(simulation, year, variables): + return {v: np.asarray(simulation.calculate(v, year)) for v in variables} + + +@pytest.fixture(scope="module") +def results(): + out = {} + for year in YEARS: + out[year] = calculate(grid_simulation([year]), year, VARIABLES) + # Independent references: no repayment at all, and each method fixed + # as an input rather than chosen in a branch. + none = grid_simulation([year], ZEROS, ZEROS, ZEROS) + out[year]["tax_without_repayment"] = np.asarray( + none.calculate("income_tax", year) + ) + deduction = grid_simulation([year], method=False) + out[year]["tax_with_deduction"] = np.asarray( + deduction.calculate("income_tax", year) + ) + out[year]["nc_deduction_if_deduction"] = np.asarray( + deduction.calculate("nc_claim_of_right_deduction", year) + ) + credit = grid_simulation([year], method=True) + out[year]["nc_deduction_if_credit"] = np.asarray( + credit.calculate("nc_claim_of_right_deduction", year) + ) + return out + + +def eligible(): + return REPAYMENT > THRESHOLD + + +@pytest.mark.parametrize("year", YEARS) +def test_tax_is_the_lesser_of_the_two_methods(results, year): + r = results[year] + tax_with_deduction = r["tax_with_deduction"] + tax_with_credit = r["tax_without_repayment"] - DECREASE + # Compared to the cent; equal amounts keep the deduction. + credit = eligible() & ( + np.round(tax_with_credit, 2) < np.round(tax_with_deduction, 2) + ) + assert credit.any() and (eligible() & ~credit).any(), "both methods occur" + np.testing.assert_array_equal(r["claim_of_right_credit_applies"], credit) + expected = np.where( + eligible(), + np.where(credit, tax_with_credit, tax_with_deduction), + r["tax_without_repayment"], + ) + np.testing.assert_allclose(r["income_tax"], expected, atol=TOLERANCE) + + +@pytest.mark.parametrize("year", YEARS) +def test_branches_match_independent_simulations(results, year): + r = results[year] + np.testing.assert_allclose( + r["income_tax_if_claiming_claim_of_right_deduction"], + r["tax_with_deduction"], + atol=TOLERANCE, + ) + tax_with_credit = r["tax_without_repayment"] - DECREASE + np.testing.assert_allclose( + r["income_tax_if_claiming_claim_of_right_credit"][eligible()], + tax_with_credit[eligible()], + atol=TOLERANCE, + ) + + +@pytest.mark.parametrize( + "first", ["tax_liability_if_itemizing", "tax_liability_if_not_itemizing"] + BRANCHES +) +def test_results_do_not_depend_on_calculation_order(results, first): + # A tax liability under one itemizing choice is calculated in a branch. + # Asked for before the method is known, it must not leave a value behind + # that a later method branch reuses. + year = YEARS[0] + simulation = grid_simulation([year]) + simulation.calculate(first, year) + for variable in VARIABLES: + np.testing.assert_allclose( + np.asarray(simulation.calculate(variable, year)), + results[year][variable], + atol=TOLERANCE, + err_msg=f"{variable} changes when {first} is calculated first", + ) + + +def test_a_later_year_matches_a_simulation_of_that_year_alone(results): + simulation = grid_simulation(YEARS) + simulation.calculate("income_tax", YEARS[0]) + for variable in VARIABLES: + np.testing.assert_allclose( + np.asarray(simulation.calculate(variable, YEARS[1])), + results[YEARS[1]][variable], + atol=TOLERANCE, + err_msg=f"{variable} differs after an earlier year was calculated", + ) + + +@pytest.mark.parametrize("year", YEARS) +def test_deduction_and_credit_are_all_or_nothing_and_exclusive(results, year): + r = results[year] + credit_applies = r["claim_of_right_credit_applies"] + deduction = r["claim_of_right_deduction"] + credit = r["claim_of_right_credit"] + np.testing.assert_array_equal(r["claim_of_right_section_1341_eligible"], eligible()) + np.testing.assert_allclose( + deduction, np.where(eligible() & ~credit_applies, REPAYMENT, 0) + ) + np.testing.assert_allclose(credit, np.where(credit_applies, DECREASE, 0)) + assert not np.any((deduction > 0) & (credit > 0)) + + +@pytest.mark.parametrize("year", YEARS) +def test_a_repayment_of_3000_or_less_changes_nothing(results, year): + r = results[year] + small = ~eligible() + assert not r["claim_of_right_credit_applies"][small].any() + np.testing.assert_allclose( + r["income_tax"][small], r["tax_without_repayment"][small], atol=TOLERANCE + ) + + +@pytest.mark.parametrize("year", YEARS) +def test_tax_never_rises(results, year): + r = results[year] + assert np.all(r["income_tax"] <= r["tax_without_repayment"] + TOLERANCE) + table = r["income_tax"].reshape(SHAPE) + assert np.all(np.diff(table, axis=0) <= TOLERANCE), "rises with the repayment" + assert np.all(np.diff(table, axis=1) <= TOLERANCE), "rises with the decrease" + + +@pytest.mark.parametrize("year", YEARS) +def test_a_decrease_above_the_tax_is_refunded(results, year): + r = results[year] + credit_applies = r["claim_of_right_credit_applies"] + refunded = credit_applies & (DECREASE > r["tax_without_repayment"]) + assert refunded.any(), "the grid should include refunds" + assert np.all(r["income_tax"][refunded] < 0) + + +@pytest.mark.parametrize("year", YEARS) +def test_north_carolina_follows_the_federal_method(results, year): + r = results[year] + credit_applies = r["claim_of_right_credit_applies"] + np.testing.assert_allclose( + r["nc_claim_of_right_deduction"], + np.where(credit_applies, 0, r["nc_deduction_if_deduction"]), + ) + assert np.all(r["nc_deduction_if_credit"] == 0) + np.testing.assert_allclose( + r["nc_claim_of_right_payment"], np.where(credit_applies, NC_INCREASE, 0) + ) + np.testing.assert_allclose( + r["nc_income_tax"], + r["nc_income_tax_before_refundable_credits"] - r["nc_claim_of_right_payment"], + atol=TOLERANCE, + ) + + +def state_income_tax(year, repayment, method=None): + """State income tax of an itemizing single filer in each state.""" + situation = {"people": {}, "tax_units": {}, "households": {}} + for i, state in enumerate(STATES): + situation["people"][f"p{i}"] = { + "age": {year: 40}, + "employment_income": {year: 60_000}, + "charitable_cash_donations": {year: 25_000}, + "claim_of_right_repayment": {year: repayment}, + } + situation["tax_units"][f"t{i}"] = { + "members": [f"p{i}"], + "claim_of_right_prior_year_tax_decrease": {year: 2_500}, + } + if method is not None: + situation["tax_units"][f"t{i}"]["claim_of_right_credit_applies"] = { + year: method + } + situation["households"][f"h{i}"] = { + "members": [f"p{i}"], + "state_code": {year: state}, + } + simulation = Simulation(situation=situation) + return np.asarray(simulation.calculate("state_income_tax", year)) + + +def test_states_under_each_federal_method(): + # One year: each of these three simulations computes every state's tax. + year = YEARS[0] + without_repayment = state_income_tax(year, 0) + with_credit = state_income_tax(year, 10_000, method=True) + np.testing.assert_allclose(with_credit, without_repayment, atol=TOLERANCE) + with_deduction = state_income_tax(year, 10_000, method=False) + assert np.any(np.abs(with_deduction - without_repayment) > 1), ( + "the deduction should reach the states that follow federal itemized deductions" + ) + excluded = np.isin(STATES, STATES_WITHOUT_THE_DEDUCTION) + np.testing.assert_allclose( + with_deduction[excluded], without_repayment[excluded], atol=TOLERANCE + ) + + +# The household from reviews r1 and r2 of PolicyEngine/policyengine-us#10038. +# With the deduction, $20,000 of itemized deductions leave $40,000 of taxable +# income: $1,192.50 + 12% x $28,075 = $4,561.50. Without it, the $15,750 +# standard deduction leaves $44,250: $5,071.50, less the $100 decrease is +# $4,971.50. So the deduction applies, and tax without itemizing under that +# method is $5,071.50. +TEXAS_YEAR = 2025 +TEXAS_EXPECTED = { + "income_tax": 4_561.5, + "claim_of_right_credit_applies": False, + "tax_liability_if_itemizing": 4_561.5, + "tax_liability_if_not_itemizing": 5_071.5, + "income_tax_if_claiming_claim_of_right_deduction": 4_561.5, + "income_tax_if_claiming_claim_of_right_credit": 4_971.5, +} + + +class no_itemization_branching(Reform): + def apply(self): + def modify(parameters): + parameters.gov.simulation.branch_to_determine_itemization.update( + start=instant(f"{TEXAS_YEAR}-01-01"), + stop=instant(f"{TEXAS_YEAR}-12-31"), + value=False, + ) + return parameters + + self.modify_parameters(modify) + + +@pytest.fixture(scope="module") +def systems(): + systems = { + True: None, + False: CountryTaxBenefitSystem(reform=(no_itemization_branching,)), + } + yield systems + # Release the reformed system before the rest of the test process runs. + systems.clear() + gc.collect() + + +def texas_simulation(system): + year = TEXAS_YEAR + situation = { + "people": { + "p": { + "age": {year: 40}, + "employment_income": {year: 60_000}, + "charitable_cash_donations": {year: 10_000}, + "claim_of_right_repayment": {year: 10_000}, + } + }, + "tax_units": { + "t": { + "members": ["p"], + "claim_of_right_prior_year_tax_decrease": {year: 100}, + "state_sales_tax": {year: 0}, + "local_sales_tax": {year: 0}, + } + }, + "households": {"h": {"members": ["p"], "state_code": {year: "TX"}}}, + } + if system is None: + return Simulation(situation=situation) + return Simulation(tax_benefit_system=system, situation=situation) + + +@pytest.mark.parametrize("branching", [True, False]) +@pytest.mark.parametrize("first", list(TEXAS_EXPECTED)) +def test_texas_household_does_not_depend_on_calculation_order( + systems, branching, first +): + simulation = texas_simulation(systems[branching]) + simulation.calculate(first, TEXAS_YEAR) + for variable, expected in TEXAS_EXPECTED.items(): + np.testing.assert_allclose( + np.asarray(simulation.calculate(variable, TEXAS_YEAR)), + [expected], + atol=TOLERANCE, + err_msg=f"{variable} when {first} is calculated first", + ) + + +def test_a_branch_does_not_reuse_tax_calculated_before_an_input_changed(): + # Review r3 of PolicyEngine/policyengine-us#10038. No repayment, $20,000 of + # charitable gifts: $5,071.50 with the $15,750 standard deduction (taxable + # income $44,250), $4,561.50 itemizing (taxable income $40,000). + year = TEXAS_YEAR + simulation = Simulation( + situation={ + "people": { + "p": { + "age": {year: 40}, + "employment_income": {year: 60_000}, + "charitable_cash_donations": {year: 20_000}, + } + }, + "tax_units": { + "t": { + "members": ["p"], + "tax_unit_itemizes": {year: False}, + "state_sales_tax": {year: 0}, + "local_sales_tax": {year: 0}, + } + }, + "households": {"h": {"members": ["p"], "state_code": {year: "TX"}}}, + } + ) + np.testing.assert_allclose( + simulation.calculate("income_tax", year), [5_071.5], atol=TOLERANCE + ) + # A plain branch whose itemizing input now equals the itemizing branch's + # override still holds the tax calculated without itemizing. + branch = simulation.get_branch("itemizes_after_the_fact") + branch.set_input("tax_unit_itemizes", year, np.array([True])) + np.testing.assert_allclose( + branch.calculate("tax_liability_if_itemizing", year), [4_561.5], atol=TOLERANCE + ) diff --git a/policyengine_us/tests/unit/test_nc_claim_of_right_properties.py b/policyengine_us/tests/unit/test_nc_claim_of_right_properties.py index 7ff5a694e8d..d86fc79c5d5 100644 --- a/policyengine_us/tests/unit/test_nc_claim_of_right_properties.py +++ b/policyengine_us/tests/unit/test_nc_claim_of_right_properties.py @@ -65,6 +65,12 @@ def grid_simulation(year, people, tax_units): } situation["tax_units"][f"t{i}"] = { "members": [person], + # These tests check the deduction's arithmetic, which applies + # when federal tax is computed with the deduction (section + # 1341(a)(4)). Fixing that method here skips the two extra + # federal tax computations that choose it; + # test_claim_of_right_section_1341_properties.py covers the choice. + "claim_of_right_credit_applies": {year: False}, **{name: {year: float(values[i])} for name, values in tax_units.items()}, } situation["households"][f"h{i}"] = { diff --git a/policyengine_us/tools/period_branch.py b/policyengine_us/tools/period_branch.py index 5e47797cf20..2f606c1b7c3 100644 --- a/policyengine_us/tools/period_branch.py +++ b/policyengine_us/tools/period_branch.py @@ -23,8 +23,15 @@ formula branches while its parent is still calculating the variable the branch overrides. Otherwise the branch drops every array it copied except inputs, each for the periods it was set for, and calculates the rest - itself. + itself. A parent value equal to the override is not enough: the parent's + cache can predate a later change to another input. - A branch reused within its period with different inputs is created again. +- A caller can ask the branch never to keep the parent's calculated values + (``inherit_calculated=False``). The parent can hold a value that depends on + the overridden variable although it has no value for that variable yet: + one it calculated in another branch, which chose the overridden variable + for itself. The branches that fix the claim of right method (26 U.S.C. + 1341) start from inputs for that reason. ``get_branch_for_period`` is the same with no inputs, for branches that change the tax-benefit system rather than inputs: the caller swaps the system @@ -61,9 +68,9 @@ def _input_keys(branch: Simulation) -> Set[Tuple[str, str, Period]]: """The (variable, branch name, period) keys ``branch`` reads as inputs. policyengine-core records each key that ``set_input`` stores, whether - from the dataset, the situation or a branch, in one set shared by a - simulation and all its branches. ``branch`` reads its own keys and its - ancestors'. + from the dataset, the situation or a branch. A branch starts with a copy + of its parent's keys (``Simulation.clone``) and adds its own. ``branch`` + reads its own keys and its ancestors'. """ visible_branches = set(branch._get_visible_branch_names()) return { @@ -110,11 +117,14 @@ def get_override_branch( name: str, period: Period, inputs: Dict[str, Override], + inherit_calculated: bool = True, ) -> Simulation: """Return ``simulation``'s branch ``name`` for ``period``, with ``inputs`` set. ``inputs`` maps each overridden variable to its value for ``period``, or - to a ``(period, value)`` pair for another period (e.g. a month). + to a ``(period, value)`` pair for another period (e.g. a month). With + ``inherit_calculated=False`` a new branch keeps only inputs, whether or + not the parent has calculated an overridden variable. """ period = to_period(period) overrides = list(_overrides(period, inputs)) @@ -138,7 +148,7 @@ def get_override_branch( ) branch = simulation.get_branch(name) branch.branch_period = period - if parent_knows_override: + if parent_knows_override or not inherit_calculated: drop_inherited_values(branch) for variable, input_period, value in overrides: branch.set_input(variable, input_period, value) diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_credit.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_credit.py new file mode 100644 index 00000000000..494a6cb24e7 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_credit.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class claim_of_right_credit(Variable): + value_type = float + entity = TaxUnit + label = "Section 1341 credit for repayment of amounts included in income from earlier years" + unit = USD + documentation = ( + "When tax is computed under section 1341(a)(5), the decrease in " + "prior-year tax from excluding the repaid income. It is reported as " + "a payment (Schedule 3, line 13b), so any excess over the year's tax " + "is refunded (section 1341(b)(1))." + ) + definition_period = YEAR + reference = ( + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm", + "https://www.irs.gov/pub/irs-prior/f1040s3--2025.pdf", + # 2025 Form 1040 instructions, Schedule 3 line 13b + "https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf#page=117", + # Publication 525 (2025), Repayments, method 2 + "https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36", + ) + + def formula(tax_unit, period, parameters): + eligible = tax_unit("claim_of_right_section_1341_eligible", period) + credit_applies = tax_unit("claim_of_right_credit_applies", period) + decrease = tax_unit("claim_of_right_prior_year_tax_decrease", period) + return where(eligible & credit_applies, decrease, 0) diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_credit_applies.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_credit_applies.py new file mode 100644 index 00000000000..7c46719ad72 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_credit_applies.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * + + +class claim_of_right_credit_applies(Variable): + value_type = bool + entity = TaxUnit + label = "Tax is computed under section 1341(a)(5)" + documentation = ( + "Whether federal income tax for the year of repayment is computed " + "under 26 U.S.C. 1341(a)(5): the tax without a deduction for the " + "repayment, minus the decrease in prior-year tax from excluding the " + "repaid income (the credit method). Section 1341 makes the tax the " + "lesser of this and the tax computed with the deduction under " + "1341(a)(4), so the method is computed, not elected. When the two " + "are equal, the tax is computed under 1341(a)(4). The comparison is " + "of federal income tax after credits; the parts of it outside " + "chapter 1 do not depend on the repayment." + ) + definition_period = YEAR + reference = ( + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm", + # 26 CFR 1.1341-1(b)(1) and (b)(3) + "https://www.ecfr.gov/current/title-26/chapter-I/subchapter-A/part-1/subject-group-ECFR4c74bc8cf7c11fb/section-1.1341-1", + # Publication 525 (2025), Repayments, methods 1 and 2 + "https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36", + ) + + def formula(tax_unit, period, parameters): + eligible = tax_unit("claim_of_right_section_1341_eligible", period) + if not eligible.any(): + # Comparing the two methods takes two more tax computations; + # skip them when no tax unit repays more than the threshold. + return np.zeros(tax_unit.count, dtype=bool) + tax_if_credit = tax_unit("income_tax_if_claiming_claim_of_right_credit", period) + tax_if_deduction = tax_unit( + "income_tax_if_claiming_claim_of_right_deduction", period + ) + # Ties go to the deduction (26 CFR 1.1341-1(b)(3)). Comparing to the + # cent keeps floating-point noise from deciding a tie. + return eligible & (np.round(tax_if_credit, 2) < np.round(tax_if_deduction, 2)) diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_prior_year_tax_decrease.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_prior_year_tax_decrease.py new file mode 100644 index 00000000000..6ed12f6a3d1 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_prior_year_tax_decrease.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class claim_of_right_prior_year_tax_decrease(Variable): + value_type = float + entity = TaxUnit + label = "Prior-year tax decrease from excluding repaid claim of right income" + unit = USD + documentation = ( + "Decrease in the filers' federal income tax under chapter 1 of the " + "Internal Revenue Code for the earlier year or years that would " + "result solely from excluding from gross income all the income they " + "repaid this year (claim_of_right_repayment): the chapter 1 tax " + "previously determined, after credits and including the alternative " + "minimum tax, less that tax refigured without the repaid income. It " + "leaves out the net investment income tax, self-employment tax and " + "payroll taxes." + ) + definition_period = YEAR + reference = ( + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm", + # 26 CFR 1.1341-1(b)(1)(ii) and (d)(4) + "https://www.ecfr.gov/current/title-26/chapter-I/subchapter-A/part-1/subject-group-ECFR4c74bc8cf7c11fb/section-1.1341-1", + # Publication 525 (2025), Repayments, method 2, steps 2 and 3 + "https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36", + ) diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_section_1341_eligible.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_section_1341_eligible.py new file mode 100644 index 00000000000..14de55d9256 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/claim_of_right_section_1341_eligible.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class claim_of_right_section_1341_eligible(Variable): + value_type = bool + entity = TaxUnit + label = "Claim of right repayment qualifies for section 1341" + documentation = ( + "Whether the repayment of claim of right income on the return " + "exceeds the section 1341(a)(3) threshold, so that tax is the " + "lesser of the tax with a deduction for the repayment and the tax " + "without it less the prior-year tax decrease. The threshold applies " + "to the total repaid on the return, not to each repayment." + ) + definition_period = YEAR + reference = ( + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm", + "https://www.ecfr.gov/current/title-26/chapter-I/subchapter-A/part-1/subject-group-ECFR4c74bc8cf7c11fb/section-1.1341-1", + "https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36", + ) + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.deductions.itemized.claim_of_right + repayment = tax_unit("tax_unit_claim_of_right_repayment", period) + return repayment > p.threshold diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/income_tax_if_claiming_claim_of_right_credit.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/income_tax_if_claiming_claim_of_right_credit.py new file mode 100644 index 00000000000..91095a583d8 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/income_tax_if_claiming_claim_of_right_credit.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class income_tax_if_claiming_claim_of_right_credit(Variable): + value_type = float + entity = TaxUnit + label = "Federal income tax under section 1341(a)(5)" + unit = USD + documentation = ( + "Federal income tax computed without a deduction for the repayment of " + "claim of right income, less the decrease in prior-year tax from " + "excluding the repaid income (section 1341(a)(5))." + ) + definition_period = YEAR + reference = "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm" + + def formula(tax_unit, period, parameters): + branch = get_override_branch( + tax_unit.simulation, + "claim_of_right_credit", + period, + {"claim_of_right_credit_applies": np.ones(tax_unit.count, dtype=bool)}, + # A value the parent calculated in another branch reflects the + # method that branch chose, so start from inputs only. + inherit_calculated=False, + ) + return branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/income_tax_if_claiming_claim_of_right_deduction.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/income_tax_if_claiming_claim_of_right_deduction.py new file mode 100644 index 00000000000..1e5372ca046 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/income_tax_if_claiming_claim_of_right_deduction.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class income_tax_if_claiming_claim_of_right_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Federal income tax under section 1341(a)(4)" + unit = USD + documentation = ( + "Federal income tax computed with a deduction for the repayment of " + "claim of right income (section 1341(a)(4))." + ) + definition_period = YEAR + reference = "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm" + + def formula(tax_unit, period, parameters): + branch = get_override_branch( + tax_unit.simulation, + "claim_of_right_deduction", + period, + {"claim_of_right_credit_applies": np.zeros(tax_unit.count, dtype=bool)}, + # A value the parent calculated in another branch reflects the + # method that branch chose, so start from inputs only. + inherit_calculated=False, + ) + return branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/irs/credits/claim_of_right/tax_unit_claim_of_right_repayment.py b/policyengine_us/variables/gov/irs/credits/claim_of_right/tax_unit_claim_of_right_repayment.py new file mode 100644 index 00000000000..be284128fc5 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/claim_of_right/tax_unit_claim_of_right_repayment.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class tax_unit_claim_of_right_repayment(Variable): + value_type = float + entity = TaxUnit + label = "Repayment of claim of right income on the return" + unit = USD + documentation = ( + "Total repaid this year by the head and spouse of income included in " + "an earlier year under a claim of right, excluding repayments " + "deducted in arriving at adjusted gross income. Dependents' " + "repayments belong on their own returns." + ) + definition_period = YEAR + reference = ( + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm", + # Publication 525 (2025), Repayments: consider the total amount + # repaid on the return + "https://www.irs.gov/pub/irs-prior/p525--2025.pdf#page=36", + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + repayment = person("claim_of_right_repayment", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return tax_unit.sum(repayment * head_or_spouse) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/claim_of_right_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/claim_of_right_deduction.py new file mode 100644 index 00000000000..bb8b52793c1 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/claim_of_right_deduction.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class claim_of_right_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Claim of right repayment deduction" + unit = USD + documentation = ( + "Itemized deduction for repaying more than the section 1341 threshold " + "of income included in an earlier year under a claim of right " + "(Schedule A, line 16), when tax is computed with the deduction " + "under section 1341(a)(4). It is not a miscellaneous itemized " + "deduction (section 67(b)(9)), so neither the two percent floor nor " + "the suspension of those deductions from 2018 applies. Under " + "section 1341(a)(5) the repayment is not deducted (section " + "1341(b)(3)). Not modeled: before 2018 a repayment of the threshold " + "amount or less was a miscellaneous itemized deduction subject to " + "the two percent floor; from 2018 it is disallowed (section 67(g), " + "redesignated 67(h) by P.L. 119-21)." + ) + definition_period = YEAR + reference = ( + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapQ-partV-sec1341.htm", + "https://www.law.cornell.edu/uscode/text/26/67#b_9", + # 2025 Schedule A instructions, line 16, PDF pages 11-12 + "https://www.irs.gov/pub/irs-prior/i1040sca--2025.pdf#page=11", + # 2017 Schedule A instructions, line 28 + "https://www.irs.gov/pub/irs-prior/i1040sca--2017.pdf#page=13", + ) + + def formula(tax_unit, period, parameters): + eligible = tax_unit("claim_of_right_section_1341_eligible", period) + credit_applies = tax_unit("claim_of_right_credit_applies", period) + repayment = tax_unit("tax_unit_claim_of_right_repayment", period) + return where(eligible & ~credit_applies, repayment, 0) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py index bbfb5c097ba..984e9f60aa8 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_itemizing.py @@ -10,10 +10,19 @@ class tax_liability_if_itemizing(Variable): def formula(tax_unit, period, parameters): simulation = tax_unit.simulation + overrides = {"tax_unit_itemizes": np.ones((tax_unit.count,), dtype=bool)} + if tax_unit("claim_of_right_section_1341_eligible", period).any(): + # Keep this simulation's claim of right method (26 U.S.C. 1341) + # rather than letting the branch choose its own. With no + # qualifying repayment the method is false everywhere, and + # leaving it out keeps the branch sharing this simulation's cache. + overrides["claim_of_right_credit_applies"] = tax_unit( + "claim_of_right_credit_applies", period + ) itemized_branch = get_override_branch( simulation, "itemizing", period, - {"tax_unit_itemizes": np.ones((tax_unit.count,), dtype=bool)}, + overrides, ) return itemized_branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py index 07279c5639b..a6c74288e0e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_liability_if_not_itemizing.py @@ -11,10 +11,19 @@ class tax_liability_if_not_itemizing(Variable): def formula(tax_unit, period, parameters): simulation = tax_unit.simulation + overrides = {"tax_unit_itemizes": np.zeros((tax_unit.count,), dtype=bool)} + if tax_unit("claim_of_right_section_1341_eligible", period).any(): + # Keep this simulation's claim of right method (26 U.S.C. 1341) + # rather than letting the branch choose its own. With no + # qualifying repayment the method is false everywhere, and + # leaving it out keeps the branch sharing this simulation's cache. + overrides["claim_of_right_credit_applies"] = tax_unit( + "claim_of_right_credit_applies", period + ) non_itemized_branch = get_override_branch( simulation, "not_itemizing", period, - {"tax_unit_itemizes": np.zeros((tax_unit.count,), dtype=bool)}, + overrides, ) return non_itemized_branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.py b/policyengine_us/variables/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.py index 30916228f82..f5d3a3e073f 100644 --- a/policyengine_us/variables/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.py +++ b/policyengine_us/variables/gov/states/az/tax/income/deductions/itemized/az_itemized_deductions.py @@ -12,6 +12,8 @@ class az_itemized_deductions(Variable): reference = ( "https://law.justia.com/codes/arizona/2022/title-43/section-43-1042/", "https://www.azleg.gov/ars/43/01042.htm", + "https://www.azleg.gov/ars/43/01021.htm", + "https://www.azleg.gov/ars/43/01029.htm", "https://azdor.gov/forms/individual/itemized-deduction-adjustments-form", "https://azdor.gov/forms/individual/form-140-resident-personal-income-tax-form-calculating", ) @@ -23,6 +25,12 @@ def formula(tax_unit, period, parameters): az_itemized = parameters(period).gov.states.az.tax.income.deductions.itemized # Medical and charitable are handled separately below; the state and # local tax deduction is also handled separately so Arizona can cap it. + # Arizona adds the federal claim of right repayment deduction back to + # Arizona gross income (A.R.S. 43-1021(9)) and computes its tax + # without it (A.R.S. 43-1029), so it gives no net Arizona deduction. + # Leaving it out here gives the same taxable income for an Arizona + # itemizer, but Arizona AGI is not raised by the addition, and the + # Arizona claim of right credit is not modeled. deductions = [ deduction for deduction in p.itemized_deductions @@ -31,6 +39,7 @@ def formula(tax_unit, period, parameters): "medical_expense_deduction", "charitable_deduction", "salt_deduction", + "claim_of_right_deduction", ] ] federal_deductions = add(tax_unit, period, deductions) diff --git a/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py b/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py index 091b5150824..2b9edc6ef0e 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py @@ -10,6 +10,7 @@ class ca_itemized_deductions_pre_limitation(Variable): reference = ( "https://www.ftb.ca.gov/forms/2021/2021-540-ca-instructions.html", "https://www.ftb.ca.gov/forms/2022/2022-540-ca-instructions.html", + # Schedule CA (540) Part II line 16, claim of right "https://www.ftb.ca.gov/forms/2025/2025-540-ca-instructions.html", "https://www.ftb.ca.gov/about-ftb/data-reports-plans/summary-of-federal-income-tax-changes/index.html", # 2024 Schedule CA (540) instructions, line 15 @@ -34,6 +35,11 @@ def formula(tax_unit, period, parameters): "charitable_deduction_for_non_itemizers", "misc_deduction", "casualty_loss_deduction", + # California chooses between its own claim of right deduction + # and credit whichever method federal tax used, and allows + # neither for income it did not tax, so the federal deduction + # does not carry over. California's own rule is not modeled. + "claim_of_right_deduction", ) ] deductions += [ diff --git a/policyengine_us/variables/gov/states/ks/tax/income/deductions/ks_itemized_deductions.py b/policyengine_us/variables/gov/states/ks/tax/income/deductions/ks_itemized_deductions.py index fd0b8dee13f..fa987b8ec7c 100644 --- a/policyengine_us/variables/gov/states/ks/tax/income/deductions/ks_itemized_deductions.py +++ b/policyengine_us/variables/gov/states/ks/tax/income/deductions/ks_itemized_deductions.py @@ -10,6 +10,7 @@ class ks_itemized_deductions(Variable): reference = ( "https://www.ksrevenue.gov/pdf/ip21.pdf", "https://www.ksrevenue.gov/pdf/ip22.pdf", + "https://www.ksrevisor.gov/statutes/chapters/ch79/079_032_0120.html", ) defined_for = StateCode.KS @@ -25,4 +26,9 @@ def formula(tax_unit, period, parameters): # compute itemized deduction maximum itm_deds_less_salt = tax_unit("itemized_deductions_less_salt", period) uncapped_property_taxes = add(tax_unit, period, ["real_estate_taxes"]) - return itm_deds_less_salt + uncapped_property_taxes + # K.S.A. 79-32,120(a) limits Kansas itemized deductions to charitable + # contributions, medical care, qualified residence interest and + # property taxes, so the federal claim of right repayment deduction + # is not one of them. + claim_of_right_deduction = tax_unit("claim_of_right_deduction", period) + return itm_deds_less_salt - claim_of_right_deduction + uncapped_property_taxes diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_claim_of_right_deduction.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_claim_of_right_deduction.py new file mode 100644 index 00000000000..201192fc27d --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_claim_of_right_deduction.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class mt_claim_of_right_deduction(Variable): + value_type = float + entity = Person + label = "Montana claim of right repayment deduction" + unit = USD + documentation = ( + "The person's part of the federal claim of right repayment deduction " + "(Schedule A, line 16), which Montana allowed as an itemized " + "deduction through 2023 (Form 2 itemized deductions, line 18)." + ) + definition_period = YEAR + reference = "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=39" + defined_for = StateCode.MT + + def formula(person, period, parameters): + # The federal deduction is the head's and spouse's repayments when + # federal tax is computed with the deduction, and zero otherwise. + deducted = person.tax_unit("claim_of_right_deduction", period) > 0 + repayment = person("claim_of_right_repayment", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return deducted * head_or_spouse * repayment diff --git a/policyengine_us/variables/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_payment.py b/policyengine_us/variables/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_payment.py new file mode 100644 index 00000000000..abcfca2ca20 --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_payment.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class nc_claim_of_right_payment(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina tax payment for restored claim of right income" + unit = USD + documentation = ( + "A filer whose federal tax is computed under 26 U.S.C. 1341(a)(5) is " + "considered to have paid North Carolina tax equal to the increase in " + "the earlier year's North Carolina tax from including the restored " + "income, minus the decrease in this year's North Carolina tax because " + "the item was deductible. An overpayment it creates is refunded. " + "Modeled for current North Carolina residents only: a filer who " + "paid the earlier North Carolina tax and has since moved away is " + "not covered, because the model computes a state's tax for its " + "residents." + ) + definition_period = YEAR + reference = ( + "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-266.2.html", + "https://www.ncleg.gov/EnactedLegislation/SessionLaws/HTML/1997-1998/SL1997-213.html", + ) + defined_for = StateCode.NC + + def formula(tax_unit, period, parameters): + credit_applies = tax_unit("claim_of_right_credit_applies", period) + prior_year_tax_increase = tax_unit( + "nc_claim_of_right_prior_year_tax_increase", period + ) + # Term (ii), the decrease in this year's North Carolina tax because + # the item was deductible, is zero: under section 1341(a)(5) the + # repayment is not deducted federally (section 1341(b)(3)), and + # G.S. 105-153.5(a)(2)d allows no North Carolina deduction for it. + return where(credit_applies, prior_year_tax_increase, 0) diff --git a/policyengine_us/variables/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_prior_year_tax_increase.py b/policyengine_us/variables/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_prior_year_tax_increase.py new file mode 100644 index 00000000000..814b4df7615 --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/credits/claim_of_right/nc_claim_of_right_prior_year_tax_increase.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class nc_claim_of_right_prior_year_tax_increase(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina prior-year tax increase from restored claim of right income" + unit = USD + documentation = ( + "Amount by which the filers' North Carolina individual income tax " + "(Article 4 of Chapter 105) for the earlier year or years was " + "increased because the income they repaid this year " + "(claim_of_right_repayment) was included in gross income for that " + "year." + ) + definition_period = YEAR + reference = "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-266.2.html" + defined_for = StateCode.NC diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.py index dbcbe3397ee..c978d27021d 100644 --- a/policyengine_us/variables/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.py +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/itemized/nc_claim_of_right_deduction.py @@ -12,10 +12,11 @@ class nc_claim_of_right_deduction(Variable): "2026, only income North Carolina taxed that year. Repayments above " "the threshold are deductible in full. Smaller repayments are reduced " "by two percent of federal adjusted gross income less the filers' " - "other miscellaneous itemized deductions. Not modeled: the bar on the " - "deduction when federal tax for the year of repayment is computed " - "under 26 U.S.C. 1341(a)(5), and from 2026, repayments of amounts North " - "Carolina added to adjusted gross income that federal law excluded." + "other miscellaneous itemized deductions. No deduction is allowed " + "when federal tax for the year of repayment is computed under 26 " + "U.S.C. 1341(a)(5). Not modeled: from 2026, repayments of amounts " + "North Carolina added to adjusted gross income that federal law " + "excluded." ) definition_period = YEAR reference = ( @@ -59,8 +60,13 @@ def formula(tax_unit, period, parameters): other_misc_deductions = 0 remaining_floor = max_(floor - other_misc_deductions, 0) small_repayment_deduction = max_(repayment - remaining_floor, 0) - return where( + deduction = where( repayment > p.claim_of_right.threshold, repayment, small_repayment_deduction, ) + # No deduction when federal tax for the year of repayment is + # computed under section 1341(a)(5); G.S. 105-266.2 instead treats + # the prior-year North Carolina tax on the item as a payment. + credit_applies = tax_unit("claim_of_right_credit_applies", period) + return where(credit_applies, 0, deduction) diff --git a/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax.py b/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax.py index 83a4c333d99..f42b325f956 100644 --- a/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax.py +++ b/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax.py @@ -8,9 +8,5 @@ class nc_income_tax(Variable): unit = USD definition_period = YEAR defined_for = StateCode.NC - - def formula(tax_unit, period, parameters): - tax_before_credits = tax_unit("nc_income_tax_before_credits", period) - # North Carolina provides no refundable income tax credits - credits = tax_unit("nc_non_refundable_credits", period) - return max_(0, tax_before_credits - credits) + adds = ["nc_income_tax_before_refundable_credits"] + subtracts = ["nc_refundable_credits"] diff --git a/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax_before_refundable_credits.py b/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax_before_refundable_credits.py new file mode 100644 index 00000000000..92cabeac3b5 --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/nc_income_tax_before_refundable_credits.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class nc_income_tax_before_refundable_credits(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina income tax before refundable credits" + unit = USD + definition_period = YEAR + defined_for = StateCode.NC + + def formula(tax_unit, period, parameters): + tax_before_credits = tax_unit("nc_income_tax_before_credits", period) + credits = tax_unit("nc_non_refundable_credits", period) + return max_(0, tax_before_credits - credits) diff --git a/policyengine_us/variables/gov/states/nc/tax/income/nc_refundable_credits.py b/policyengine_us/variables/gov/states/nc/tax/income/nc_refundable_credits.py new file mode 100644 index 00000000000..8f6815920db --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/nc_refundable_credits.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class nc_refundable_credits(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina refundable credits" + unit = USD + definition_period = YEAR + defined_for = StateCode.NC + adds = "gov.states.nc.tax.income.credits.refundable" 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..5f16cde2e48 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 @@ -10,6 +10,8 @@ class or_federal_tax_liability_subtraction(Variable): 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 + # 2025 Publication OR-17, federal tax worksheet, PDF pages 71-72 + "https://www.oregon.gov/dor/forms/FormsPubs/publication-or-17_101-431_2025.pdf#page=71", ) defined_for = StateCode.OR @@ -17,7 +19,12 @@ 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) + # The federal tax worksheet starts from Form 1040 line 22 and does + # not subtract the section 1341 credit (Schedule 3, line 13b). + claim_of_right_credit = tax_unit("claim_of_right_credit", period) + or_federal_income_tax = max_( + 0, federal_itax + federal_eitc + claim_of_right_credit + ) # limit subtraction based on caps scaled to federal AGI filing_status = tax_unit("filing_status", period) status = filing_status.possible_values diff --git a/policyengine_us/variables/gov/states/wi/tax/income/credits/itemized_deduction/wi_itemized_deduction_credit_potential.py b/policyengine_us/variables/gov/states/wi/tax/income/credits/itemized_deduction/wi_itemized_deduction_credit_potential.py index da76fe63638..5228fd84ff3 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/credits/itemized_deduction/wi_itemized_deduction_credit_potential.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/credits/itemized_deduction/wi_itemized_deduction_credit_potential.py @@ -11,11 +11,20 @@ class wi_itemized_deduction_credit_potential(Variable): "https://www.revenue.wi.gov/TaxForms2021/2021-Form1f.pdf#page=4", "https://www.revenue.wi.gov/TaxForms2022/2022-Form1f.pdf#page=4", "https://docs.legis.wisconsin.gov/misc/lfb/informational_papers/january_2023/0002_individual_income_tax_informational_paper_2.pdf#page=19", + # Wis. Stat. 71.07(5)(a) + "https://docs.legis.wisconsin.gov/statutes/statutes/71.pdf#page=39", + "https://www.revenue.wi.gov/TaxForms2025/2025-Form1.pdf#page=4", ) defined_for = StateCode.WI def formula(tax_unit, period, parameters): - wi_itmded = tax_unit("itemized_deductions_less_salt", period) + # Wis. Stat. 71.07(5)(a)7 leaves miscellaneous itemized deductions + # out of the credit whether or not the two percent floor applies to + # them, and Schedule 1 has no line for other itemized deductions, so + # the federal claim of right repayment deduction does not count. + wi_itmded = tax_unit("itemized_deductions_less_salt", period) - tax_unit( + "claim_of_right_deduction", period + ) wi_stdded = tax_unit("wi_standard_deduction", period) excess_itmded = max_(0, wi_itmded - wi_stdded) p = parameters(period).gov.states.wi.tax.income