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/nj-claim-of-right.added.md b/changelog.d/nj-claim-of-right.added.md new file mode 100644 index 00000000000..cdd3bc78cca --- /dev/null +++ b/changelog.d/nj-claim-of-right.added.md @@ -0,0 +1 @@ +Offset New Jersey claim of right repayments against income of the same category in the year of repayment (N.J.S.A. 54A:5-2, TAM 2011-7), including in the pension exclusion's base and Worksheet D's earnings test. 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..012bffd43b0 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,13 +6,22 @@ description: Montana counts these sources as miscellaneous deductions. values: 2021-01-01: - casualty_loss_deduction - 2024-01-01: [] - + # Form 2 itemized deductions line 18 follows federal Schedule A line 16. + - mt_claim_of_right_deduction + # From 2024 Form 2 line 2 takes the federal itemized deductions (Worksheet A + # line 1: Form 1040 line 12), which include Schedule A line 16. + 2024-01-01: + - mt_claim_of_right_deduction + metadata: unit: list - label: Montana miscellaneous deductions + 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: 2024 Montana Form 2 instructions, line 2 and Worksheet A + href: https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=15 - 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/nj/tax/income/gross_income/claim_of_right_category_exempt_sources/interest.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_exempt_sources/interest.yaml new file mode 100644 index 00000000000..bd7d0645103 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_exempt_sources/interest.yaml @@ -0,0 +1,15 @@ +description: New Jersey leaves these amounts out of the interest a claim of right repayment of interest can offset, because New Jersey does not tax them; it subtracts them from gross income separately. +values: + 2021-01-01: + - us_govt_interest_person # Interest on U.S. obligations, exempt in New Jersey +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (e) exempt interest + reference: + - title: N.J.S.A. 54A:5-1(e) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: 2025 NJ-1040 instructions, income New Jersey does not tax (direct federal obligations) + href: https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=10 diff --git a/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/alimony.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/alimony.yaml new file mode 100644 index 00000000000..2807706acce --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/alimony.yaml @@ -0,0 +1,17 @@ +description: New Jersey offsets a claim of right repayment of category (n) income against these sources of the same category in the year of repayment. +values: + 2021-01-01: + - alimony_income # Line 25 +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (n) sources + reference: + - title: N.J.S.A. 54A:5-1(n) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: New Jersey Division of Taxation, TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + href: https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf + - title: 2025 NJ-1040, Line 25 + href: https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf diff --git a/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/dividends.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/dividends.yaml new file mode 100644 index 00000000000..7aaa532849f --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/dividends.yaml @@ -0,0 +1,17 @@ +description: New Jersey offsets a claim of right repayment of category (f) income against these sources of the same category in the year of repayment. +values: + 2021-01-01: + - dividend_income # Line 17 +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (f) sources + reference: + - title: N.J.S.A. 54A:5-1(f) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: New Jersey Division of Taxation, TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + href: https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf + - title: 2025 NJ-1040, Line 17 + href: https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf diff --git a/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/gambling_winnings.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/gambling_winnings.yaml new file mode 100644 index 00000000000..304fe62e703 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/gambling_winnings.yaml @@ -0,0 +1,17 @@ +description: New Jersey offsets a claim of right repayment of category (g) income against these sources of the same category in the year of repayment. +values: + 2021-01-01: + - gambling_winnings # Line 24 +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (g) sources + reference: + - title: N.J.S.A. 54A:5-1(g) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: New Jersey Division of Taxation, TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + href: https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf + - title: 2025 NJ-1040, Line 24 + href: https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf diff --git a/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/interest.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/interest.yaml new file mode 100644 index 00000000000..b0c0973ed2d --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/interest.yaml @@ -0,0 +1,17 @@ +description: New Jersey offsets a claim of right repayment of category (e) income against these sources of the same category in the year of repayment. +values: + 2021-01-01: + - taxable_interest_income # Line 16a +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (e) sources + reference: + - title: N.J.S.A. 54A:5-1(e) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: New Jersey Division of Taxation, TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + href: https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf + - title: 2025 NJ-1040, Line 16a + href: https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf diff --git a/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/pensions_and_annuities.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/pensions_and_annuities.yaml new file mode 100644 index 00000000000..63d3662e62f --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/pensions_and_annuities.yaml @@ -0,0 +1,23 @@ +description: New Jersey offsets a claim of right repayment of category (j) income against these sources of the same category in the year of repayment. +values: + 2021-01-01: + - taxable_pension_income # Line 20a + - taxable_ira_distributions + - taxable_roth_conversions + - taxable_401k_distributions + - taxable_403b_distributions + - taxable_sep_distributions + - keogh_distributions +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (j) sources + reference: + - title: N.J.S.A. 54A:5-1(j) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: New Jersey Division of Taxation, TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + href: https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf + - title: 2025 NJ-1040, Line 20a + href: https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf diff --git a/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/wages.yaml b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/wages.yaml new file mode 100644 index 00000000000..28953ccb468 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nj/tax/income/gross_income/claim_of_right_category_sources/wages.yaml @@ -0,0 +1,17 @@ +description: New Jersey offsets a claim of right repayment of category (a) income against these sources of the same category in the year of repayment. +values: + 2021-01-01: + - employment_income # Line 15 +metadata: + unit: list + period: year + label: New Jersey claim of right repayment offset, category (a) sources + reference: + - title: N.J.S.A. 54A:5-1(a) + href: https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/ + - title: N.J.S.A. 54A:5-2 + href: https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634 + - title: New Jersey Division of Taxation, TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + href: https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf + - title: 2025 NJ-1040, Line 15 + href: https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf 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..cd13b6b920b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_claim_of_right_deduction.yaml @@ -0,0 +1,112 @@ +# 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] + +# 2024 Form 2 instructions, line 2, Worksheet A line 1: "Enter your total +# federal itemized deductions from Form 1040, line 12", which includes +# Schedule A line 16. +- name: Case 4, from 2024 Montana keeps the deduction within federal itemized deductions. + 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_claim_of_right_deduction: 5_000 + mt_misc_deductions: 5_000 + +- name: Case 5, the 2025 deduction reaches Montana itemized deductions. + period: 2025 + input: + people: + person1: + age: 40 + claim_of_right_repayment: 5_000 + investment_interest_expense: 0 + deductible_mortgage_interest: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [person1] + claim_of_right_deduction: 5_000 + charitable_deduction: 0 + mt_child_dependent_care_expense_deduction_joint: 0 + mt_federal_income_tax_deduction_unit: 0 + households: + household: + members: [person1] + state_code: MT + output: + # Interest 0 + charity 0 + miscellaneous 5,000 + medical 0 + SALT 0. + mt_itemized_deductions_joint: 5_000 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/nj/tax/income/adjusted_gross_income/nj_claim_of_right_offset.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/adjusted_gross_income/nj_claim_of_right_offset.yaml new file mode 100644 index 00000000000..f55e1667afb --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/adjusted_gross_income/nj_claim_of_right_offset.yaml @@ -0,0 +1,342 @@ +# N.J.S.A. 54A:5-2 and TAM 2011-7: "any repayment or deduction may be applied +# against other income earned in the same category and in the same tax year as +# the repayment ... If the offset results in a net loss or negative amount, the +# taxpayer must report zero income in that category." + +- name: Case 1, a wage repayment like TAM 2011-7's example reduces this year's wages. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 50_000 + claim_of_right_repayment: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 8_000 + nj_gross_income: 42_000 + nj_agi: 42_000 + +- name: Case 2, wages are reported as zero, and other categories are untouched, when the repayment exceeds them. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 5_000 + taxable_interest_income: 1_000 + claim_of_right_repayment: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 5_000 + nj_gross_income: 1_000 + +- name: Case 3, a repaid pension offsets pensions, not wages. + period: 2025 + input: + people: + head: + age: 70 + employment_income: 10_000 + taxable_pension_income: 2_000 + claim_of_right_repayment: 3_000 + nj_claim_of_right_repayment_category: PENSIONS_AND_ANNUITIES + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 2_000 + nj_claim_of_right_pension_offset: 2_000 + nj_gross_income: 10_000 + # Line 20a is zero after the offset, so the pension exclusion is zero. + nj_eligible_pension_income: 0 + nj_agi: 10_000 + +- name: Case 4, repaid income New Jersey never taxed, such as unemployment compensation, offsets nothing. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 30_000 + claim_of_right_repayment: 4_000 + nj_claim_of_right_repayment_category: NOT_TAXED + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 0 + nj_gross_income: 30_000 + +# Joint return: line 15 combines both spouses' wages ($2,000 + $40,000 = +# $42,000), so the head's $6,000 wage repayment is offset in full. It offsets +# the head's own $2,000 of wages first and the other $4,000 against the +# spouse's. The dependent's repayment belongs on the dependent's own return. +- name: Case 5, a joint return offsets the repayer's wages first and the excess against the spouse's. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 2_000 + claim_of_right_repayment: 6_000 + spouse: + age: 40 + employment_income: 40_000 + child: + age: 17 + employment_income: 3_000 + claim_of_right_repayment: 3_000 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: NJ + output: + nj_claim_of_right_offset: [2_000, 4_000, 0] + nj_gross_income: [0, 36_000, 3_000] + +- name: Case 6, the federal credit method does not change the offset. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 50_000 + claim_of_right_repayment: 8_000 + tax_units: + tax_unit: + members: [head] + claim_of_right_credit_applies: true + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 8_000 + +- name: Case 7, outside New Jersey. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 50_000 + claim_of_right_repayment: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NY + output: + nj_claim_of_right_offset: 0 + +- name: Case 8, a repayment of another category is not offset. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 30_000 + claim_of_right_repayment: 5_000 + nj_claim_of_right_repayment_category: OTHER + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 0 + nj_gross_income: 30_000 + +# Age 70, wages $30,000, pensions $50,000, repays $8,000 of pension income. +# Line 20a = $42,000 and line 27 = $72,000. Line 28a = the lesser of $42,000 +# and the maximum exclusion ($75,000 single at total income of $100,000 or +# less), so $42,000. Worksheet D does not apply: line 15 is over $3,000. +# Line 29 = $72,000 - $42,000 = $30,000. +- name: Case 9, a pension repayment also reduces the pension exclusion's base, line 20a. + period: 2025 + input: + people: + head: + age: 70 + employment_income: 30_000 + taxable_pension_income: 50_000 + claim_of_right_repayment: 8_000 + nj_claim_of_right_repayment_category: PENSIONS_AND_ANNUITIES + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_total_income: 72_000 + nj_eligible_pension_income: 42_000 + nj_pension_retirement_exclusion: 42_000 + nj_agi: 30_000 + +# Age 65, wages $10,000 of which $8,000 is repaid, interest $20,000. Worksheet +# D line 4 is line 15, $2,000, so lines 4-7 total $3,000 or less and the +# unused exclusion applies: total income $22,000, all of it excludable +# ($22,000 is under both the income limit and the $75,000 maximum), less the +# $0 pension exclusion = $22,000. Line 29 = $22,000 - $22,000 = $0. +- name: Case 10, Worksheet D's $3,000 earnings test uses wages after a wage repayment. + period: 2025 + input: + people: + head: + age: 65 + employment_income: 10_000 + taxable_interest_income: 20_000 + claim_of_right_repayment: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_wage_offset: 8_000 + nj_total_income: 22_000 + nj_other_retirement_income_exclusion: 22_000 + nj_agi: 0 + +# Wages $30,000, $5,000 of interest, all of it on U.S. Treasury obligations, +# which New Jersey does not tax and subtracts from gross income separately. A +# repaid $8,000 of interest has no New Jersey-taxable interest to offset, so +# total income stays $30,000. +- name: Case 11, a repayment of interest does not offset interest New Jersey exempts. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 30_000 + taxable_interest_income: 5_000 + us_govt_interest_person: 5_000 + claim_of_right_repayment: 8_000 + nj_claim_of_right_repayment_category: INTEREST + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 0 + nj_total_income: 30_000 + +# Joint return: head aged 70 with wages $30,000 and a $20,000 pension; spouse +# aged 40 with a $20,000 pension. Line 27 is $70,000 - $8,000 = $62,000 either +# way. Only the head is old enough for the pension exclusion, so line 28a is +# the head's own pension (Line A, joint filers): $20,000 when the spouse repaid +# $8,000 of the spouse's pension (line 29 $42,000), $12,000 when the head +# repaid it (line 29 $50,000). Wages exceed $3,000, so no Worksheet D. +- name: Case 12, a pension repayment by the younger spouse leaves the older spouse's exclusion whole. + period: 2025 + input: + people: + head: + age: 70 + employment_income: 30_000 + taxable_pension_income: 20_000 + spouse: + age: 40 + taxable_pension_income: 20_000 + claim_of_right_repayment: 8_000 + nj_claim_of_right_repayment_category: PENSIONS_AND_ANNUITIES + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: NJ + output: + nj_claim_of_right_pension_offset: [0, 8_000] + nj_eligible_pension_income: [20_000, 0] + nj_agi: 42_000 + +- name: Case 13, a pension repayment by the older spouse reduces that spouse's exclusion. + period: 2025 + input: + people: + head: + age: 70 + employment_income: 30_000 + taxable_pension_income: 20_000 + claim_of_right_repayment: 8_000 + nj_claim_of_right_repayment_category: PENSIONS_AND_ANNUITIES + spouse: + age: 40 + taxable_pension_income: 20_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: NJ + output: + nj_claim_of_right_pension_offset: [8_000, 0] + nj_eligible_pension_income: [12_000, 0] + nj_agi: 50_000 + +# Wages $30,000 and $10,000 of interest, $5,000 of it on U.S. Treasury +# obligations. A repaid $8,000 of interest offsets only the $5,000 of interest +# New Jersey taxes: gross income $30,000 + $10,000 - $5,000 = $35,000, and the +# separate subtraction of the $5,000 of Treasury interest leaves total income of +# $30,000. +- name: Case 14, a repayment of interest offsets the interest New Jersey taxes. + period: 2025 + input: + people: + head: + age: 40 + employment_income: 30_000 + taxable_interest_income: 10_000 + us_govt_interest_person: 5_000 + claim_of_right_repayment: 8_000 + nj_claim_of_right_repayment_category: INTEREST + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: NJ + output: + nj_claim_of_right_offset: 5_000 + nj_gross_income: 35_000 + nj_total_income: 30_000 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..2846b3a7504 --- /dev/null +++ b/policyengine_us/tests/unit/test_claim_of_right_section_1341_properties.py @@ -0,0 +1,547 @@ +"""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, including the 2026 branch that + ``tax_unit_itemizes`` opens for the section 68 limitation; +- 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 (section 1341(b)(3)), +except that it lowers the tax of each state listed in +STATES_WITH_RELIEF_UNDER_THE_CREDIT whose rule is in the model; 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"] +# States whose own claim of right relief applies when federal tax is computed +# with the section 1341 credit, and the variable that carries it. Under the +# credit method these lower the household's state tax; every other state's +# tax must not change. A listed state whose variable is not in the model yet +# must not change either. +STATES_WITH_RELIEF_UNDER_THE_CREDIT = { + # Ark. Code 26-51-457: a deduction whichever federal method applies. + "AR": "ar_claim_of_right_deduction", + # Act 56, SLH 2023: section 1341 computed on Hawaii tax, independently. + "HI": "hi_claim_of_right_credit", + # 35 ILCS 5/203(a)(2)(P): the deduction used to compute the federal credit. + "IL": "il_claim_of_right_subtraction", + # IC 6-3-1-3.5(a)(22). + "IN": "in_claim_of_right_deduction", + # M.G.L. c. 62, s. 3(B)(a)(14). + "MA": "ma_claim_of_right_deduction", + # N.J.S.A. 54A:5-2 and TAM 2011-7: an offset within the income category. + "NJ": "nj_claim_of_right_offset", + # R.C. 5747.01(A)(12). + "OH": "oh_claim_of_right_deduction", + # IT-140 Schedule M, IRC 1341 Repayments. + "WV": "wv_claim_of_right_subtraction", +} +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_simulation(year, repayment, method=None): + """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}, + } + return Simulation(situation=situation) + + +def state_income_tax(simulation, year): + 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(state_simulation(year, 0), year) + credit = state_simulation(year, 10_000, method=True) + with_credit = state_income_tax(credit, year) + variables = credit.tax_benefit_system.variables + relief = np.isin( + STATES, + [ + state + for state, variable in STATES_WITH_RELIEF_UNDER_THE_CREDIT.items() + if variable in variables + ], + ) + np.testing.assert_allclose( + with_credit[~relief], without_repayment[~relief], atol=TOLERANCE + ) + assert np.all(with_credit[relief] < without_repayment[relief] - 1), ( + "each listed state's own rule should lower its tax under the credit method" + ) + with_deduction = state_income_tax( + state_simulation(year, 10_000, method=False), year + ) + 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 +# From 2026 tax_unit_itemizes takes the itemized deductions from the branch +# that itemizes (the P.L. 119-21 section 68 limitation). A household for which +# that branch, choosing a method for itself, would pick the deduction while +# the simulation picks the credit. Standard deduction $16,100 plus the $1,000 +# section 170(p) charitable deduction: taxable income $42,900, tax $1,240 + +# 12% x $30,500 = $4,900, less the $800 decrease is $4,100. With the deduction: +# gifts of $10,000 less the 0.5% floor ($300) plus the $10,000 repayment are +# $19,700 of itemized deductions, taxable income $40,300, tax $4,588. Itemizing +# under the credit method: $9,700 of deductions, taxable income $50,300, tax +# $5,788 less $800 is $4,988. +ITEMIZING_BRANCH_YEAR = 2026 +ITEMIZING_BRANCH_EXPECTED = { + "income_tax": 4_100, + "claim_of_right_credit_applies": True, + "tax_unit_itemizes": False, + "tax_liability_if_itemizing": 4_988, + "tax_liability_if_not_itemizing": 4_100, + "income_tax_if_claiming_claim_of_right_deduction": 4_588, + "income_tax_if_claiming_claim_of_right_credit": 4_100, +} +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"{ITEMIZING_BRANCH_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, decrease=100, **tax_unit_inputs): + 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: decrease}, + "state_sales_tax": {year: 0}, + "local_sales_tax": {year: 0}, + **{name: {year: value} for name, value in tax_unit_inputs.items()}, + } + }, + "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", + ) + + +@pytest.mark.parametrize("branching", [True, False]) +@pytest.mark.parametrize("first", list(ITEMIZING_BRANCH_EXPECTED)) +def test_the_itemizing_branch_keeps_the_simulation_method(systems, branching, first): + # From policyengine-core 3.33, set_input on a branch drops the values + # stored after the overridden variable was first stored anywhere in the + # simulation. A branch fixing only itemization then lost the inherited + # method and chose its own, and with itemization branching off its + # itemized deductions alone decided whether to itemize: $4,988 here. + year = ITEMIZING_BRANCH_YEAR + forced = texas_simulation(systems[branching], year, 800, tax_unit_itemizes=True) + assert not forced.calculate("claim_of_right_credit_applies", year)[0], ( + "a filer who must itemize would choose the deduction" + ) + simulation = texas_simulation(systems[branching], year, 800) + simulation.calculate(first, year) + for variable, expected in ITEMIZING_BRANCH_EXPECTED.items(): + np.testing.assert_allclose( + np.asarray(simulation.calculate(variable, 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/tests/unit/test_nj_claim_of_right_properties.py b/policyengine_us/tests/unit/test_nj_claim_of_right_properties.py new file mode 100644 index 00000000000..ce8ceb820b5 --- /dev/null +++ b/policyengine_us/tests/unit/test_nj_claim_of_right_properties.py @@ -0,0 +1,217 @@ +"""Properties of New Jersey's claim of right repayment offset. + +N.J.S.A. 54A:5-2 and TAM 2011-7: a repayment is applied against income of the +same category in the year of repayment, and a category left negative is +reported as zero. New Jersey has no counterpart to the section 1341 credit. +The policy cases are in YAML. These tests check what YAML cannot, over a grid +of single returns and joint returns with a spouse of the same age (with wages) +or the other age (with a pension): + +- a repayment acts on New Jersey AGI and taxable income exactly as a + simulation with no repayment and that much less income in the repaid + category, taken from the repayer first and then the spouse; this reaches the + pension exclusion (line 20a) and Worksheet D's earnings test (line 15); +- no filer's offset exceeds their own income in the category; +- New Jersey tax does not depend on the federal method, never rises with a + repayment, and never rises as it grows; +- the category source lists cover the gross income sources and match the + pension exclusion's sources. +""" + +import gc +import itertools + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system + +YEAR = 2025 +CATEGORIES = ["WAGES", "PENSIONS_AND_ANNUITIES", "NOT_TAXED", "OTHER"] +REPAYMENTS = [0, 1_000, 8_000, 60_000] +HEAD_WAGES = [0, 2_000, 50_000] +HEAD_PENSIONS = [0, 20_000] +AGES = [40, 70] +# A single return, or a joint return with a spouse of the head's age who has +# wages, or of the other age who has a pension. +SPOUSES = ["none", "same_age_wages", "other_age_pension"] +AXES = [CATEGORIES, REPAYMENTS, HEAD_WAGES, HEAD_PENSIONS, AGES, SPOUSES] +GRID = list(itertools.product(*AXES)) +SHAPE = tuple(len(axis) for axis in AXES) +CATEGORY = np.array([point[0] for point in GRID]) +REPAYMENT, WAGE, PENSION, AGE = ( + np.array([point[i] for point in GRID], dtype=float) for i in range(1, 5) +) +SPOUSE = np.array([point[5] for point in GRID]) +JOINT = SPOUSE != "none" +SPOUSE_WAGE = np.where(SPOUSE == "same_age_wages", 30_000.0, 0.0) +SPOUSE_PENSION = np.where(SPOUSE == "other_age_pension", 20_000.0, 0.0) +# The other age: 70 for a 40-year-old head and 40 for a 70-year-old one. +SPOUSE_AGE = np.where(SPOUSE == "other_age_pension", 110 - AGE, AGE) +ZEROS = np.zeros_like(REPAYMENT) +TOLERANCE = 0.01 + + +def category_cuts(): + """The income each filer gives up in the reference: the return's offset, + min(repayment, the filers' income in the repaid category), taken from the + repayer (the head) first and the rest from the spouse.""" + wages = CATEGORY == "WAGES" + pensions = CATEGORY == "PENSIONS_AND_ANNUITIES" + head_income = np.where(wages, WAGE, np.where(pensions, PENSION, 0)) + spouse_income = np.where(wages, SPOUSE_WAGE, np.where(pensions, SPOUSE_PENSION, 0)) + offset = np.minimum(REPAYMENT, head_income + spouse_income) + head_cut = np.minimum(offset, head_income) + return offset, head_cut, offset - head_cut + + +OFFSET, HEAD_CUT, SPOUSE_CUT = category_cuts() + + +def cut_of(kind, i, cut): + """How much of a filer's wages or pension the reference removes.""" + return cut[i] if CATEGORY[i] == kind else 0 + + +def grid_simulation(repayment, cut=False, tax_unit_inputs=None): + """One New Jersey return per grid point; the head repays. With cut=True, + no repayment and each filer's repaid-category income lowered instead.""" + head_cut = HEAD_CUT if cut else ZEROS + spouse_cut = SPOUSE_CUT if cut else ZEROS + situation = {"people": {}, "tax_units": {}, "marital_units": {}, "households": {}} + for i in range(len(GRID)): + head = f"h{i}" + members = [head] + situation["people"][head] = { + "age": {YEAR: AGE[i]}, + "employment_income": {YEAR: WAGE[i] - cut_of("WAGES", i, head_cut)}, + "taxable_pension_income": { + YEAR: PENSION[i] - cut_of("PENSIONS_AND_ANNUITIES", i, head_cut) + }, + "taxable_interest_income": {YEAR: 1_000}, + "claim_of_right_repayment": {YEAR: 0 if cut else float(repayment[i])}, + "nj_claim_of_right_repayment_category": {YEAR: CATEGORY[i]}, + } + if JOINT[i]: + spouse = f"s{i}" + members.append(spouse) + situation["people"][spouse] = { + "age": {YEAR: SPOUSE_AGE[i]}, + "employment_income": { + YEAR: SPOUSE_WAGE[i] - cut_of("WAGES", i, spouse_cut) + }, + "taxable_pension_income": { + YEAR: SPOUSE_PENSION[i] + - cut_of("PENSIONS_AND_ANNUITIES", i, spouse_cut) + }, + } + # Lowering wages in the reference can qualify a couple over 65 for a + # Medicare Savings Program, which pays their Part B premiums and so + # removes New Jersey's medical expense deduction; the repayment itself + # does not. Pin the deduction so the comparison isolates the offset. + tax_unit = {"members": members, "nj_medical_expense_deduction": {YEAR: 0}} + for name, value in (tax_unit_inputs or {}).items(): + tax_unit[name] = {YEAR: value} + situation["tax_units"][f"t{i}"] = tax_unit + situation["marital_units"][f"m{i}"] = {"members": members} + situation["households"][f"hh{i}"] = { + "members": members, + "state_code": {YEAR: "NJ"}, + } + return Simulation(situation=situation) + + +def by_tax_unit(simulation, variable): + return np.asarray(simulation.calculate(variable, YEAR, map_to="tax_unit")) + + +@pytest.fixture(scope="module") +def results(): + model = grid_simulation(REPAYMENT) + out = { + "offset": by_tax_unit(model, "nj_claim_of_right_offset"), + "nj_agi": by_tax_unit(model, "nj_agi"), + "nj_taxable_income": by_tax_unit(model, "nj_taxable_income"), + "tax": by_tax_unit(model, "nj_income_tax"), + "person_offset": np.asarray(model.calculate("nj_claim_of_right_offset", YEAR)), + } + reference = grid_simulation(ZEROS, cut=True) + out["reference_nj_agi"] = by_tax_unit(reference, "nj_agi") + out["reference_nj_taxable_income"] = by_tax_unit(reference, "nj_taxable_income") + out["tax_none"] = by_tax_unit(grid_simulation(ZEROS), "nj_income_tax") + out["tax_federal_credit"] = by_tax_unit( + grid_simulation( + REPAYMENT, tax_unit_inputs={"claim_of_right_credit_applies": True} + ), + "nj_income_tax", + ) + yield out + gc.collect() + + +def test_a_repayment_acts_as_less_income_in_its_category(results): + np.testing.assert_allclose(results["offset"], OFFSET, atol=TOLERANCE) + np.testing.assert_allclose( + results["nj_agi"], results["reference_nj_agi"], atol=TOLERANCE + ) + np.testing.assert_allclose( + results["nj_taxable_income"], + results["reference_nj_taxable_income"], + atol=TOLERANCE, + ) + assert np.any((OFFSET > 0) & (OFFSET < REPAYMENT)), "the cap should bind" + assert np.any(SPOUSE_CUT > 0), "some repayment should reach the spouse" + + +def test_no_filer_offsets_more_than_their_own_category_income(results): + # In the simulation's person order: each head, then its spouse. + bound = [] + for i in range(len(GRID)): + head = {"WAGES": WAGE[i], "PENSIONS_AND_ANNUITIES": PENSION[i]} + bound.append(head.get(CATEGORY[i], 0)) + if JOINT[i]: + spouse = { + "WAGES": SPOUSE_WAGE[i], + "PENSIONS_AND_ANNUITIES": SPOUSE_PENSION[i], + } + bound.append(spouse.get(CATEGORY[i], 0)) + offset = results["person_offset"] + assert np.all(offset >= -TOLERANCE) + assert np.all(offset <= np.array(bound) + TOLERANCE) + + +def test_tax_ignores_the_federal_method_and_never_rises(results): + np.testing.assert_allclose( + results["tax_federal_credit"], results["tax"], atol=TOLERANCE + ) + assert np.all(results["tax"] <= results["tax_none"] + TOLERANCE) + assert np.any(results["tax"] < results["tax_none"] - 1) + table = results["tax"].reshape(SHAPE) + assert np.all(np.diff(table, axis=1) <= TOLERANCE), "rises with the repayment" + + +def test_category_sources_match_the_gross_income_lists(): + """The offset's category lists cover every non-negative gross income + source but line 26's other income, without overlap, and the pension + category is exactly the pension exclusion's sources, in every year block.""" + income = system.parameters.gov.states.nj.tax.income + names = [ + "wages", + "interest", + "dividends", + "gambling_winnings", + "pensions_and_annuities", + "alimony", + ] + for instant in ["2021-01-01", "2025-01-01", "2026-01-01"]: + categories = income.gross_income.claim_of_right_category_sources + lists = [list(getattr(categories, name)(instant)) for name in names] + flat = [source for sources in lists for source in sources] + assert len(flat) == len(set(flat)), "a source is in two categories" + expected = set(income.gross_income.non_negative_sources(instant)) - { + "miscellaneous_income" + } + assert set(flat) == expected, (instant, set(flat) ^ expected) + pensions = set(categories.pensions_and_annuities(instant)) + assert pensions == set(income.exclusions.retirement.sources(instant)) diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index d2bd4f5fd5f..bc8446b01b2 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -185,6 +185,32 @@ def filer_share(person, period, amounts): return where(total > 0, proportional, equal) +def get_itemization_branch(tax_unit, period, itemizes: bool): + """ + The branch of ``tax_unit``'s simulation that fixes ``tax_unit_itemizes``. + + It is named ``itemizing`` or ``not_itemizing``. When some tax unit repays + more than $3,000 of claim of right income (26 U.S.C. 1341), the branch + also takes this simulation's method, ``claim_of_right_credit_applies``, as + an input, so it never chooses one for itself: with itemizing fixed, a + branch would choose the method best for that choice, not the method this + simulation uses. Every caller asks for the same inputs, so they share one + branch. With no qualifying repayment the method is false everywhere and is + left out, which keeps the branch sharing this simulation's cache. + """ + overrides = {"tax_unit_itemizes": np.full(tax_unit.count, itemizes, dtype=bool)} + if tax_unit("claim_of_right_section_1341_eligible", period).any(): + overrides["claim_of_right_credit_applies"] = tax_unit( + "claim_of_right_credit_applies", period + ) + return get_override_branch( + tax_unit.simulation, + "itemizing" if itemizes else "not_itemizing", + period, + overrides, + ) + + def sum_contained_tax_units(var, population, period): tax_unit = population.members.tax_unit.reference_entity values = tax_unit(var, period) diff --git a/policyengine_us/tools/period_branch.py b/policyengine_us/tools/period_branch.py index 1270a9490ce..469b239ec1a 100644 --- a/policyengine_us/tools/period_branch.py +++ b/policyengine_us/tools/period_branch.py @@ -1,11 +1,15 @@ """Formula branches that calculate one period, under overridden inputs. A policyengine-core branch starts from every array its parent has cached when -the branch is created and never sees the parent's later calculations, and -``set_input`` on the branch stores the new value without clearing anything -that was calculated from the old one. A branch therefore answers any variable -its parent had already calculated with the parent's value, whatever the -branch's inputs say. A branch kept from another period also answers this +the branch is created and never sees the parent's later calculations. Before +policyengine-core 3.33, ``set_input`` on the branch stored the new value +without clearing anything that was calculated from the old one, so a branch +answered any variable its parent had already calculated with the parent's +value, whatever the branch's inputs said. From 3.33 it drops the values stored +after the overridden variable was first stored anywhere in the simulation, +which can include a choice the caller wants the branch to keep; pass such a +choice as an input (as ``get_itemization_branch`` does for the claim of right +method). A branch kept from another period also answers this period from a copy taken before any of this period was calculated, unlike the branch a simulation calculating only this period would create, so a later year came out differently when an earlier year had been calculated first. @@ -23,8 +27,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 @@ -139,11 +150,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)) @@ -167,7 +181,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..c12237b7abc 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 @@ -9,11 +9,5 @@ class tax_liability_if_itemizing(Variable): definition_period = YEAR def formula(tax_unit, period, parameters): - simulation = tax_unit.simulation - itemized_branch = get_override_branch( - simulation, - "itemizing", - period, - {"tax_unit_itemizes": np.ones((tax_unit.count,), dtype=bool)}, - ) + itemized_branch = get_itemization_branch(tax_unit, period, itemizes=True) 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..5b85efef0c3 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 @@ -10,11 +10,5 @@ class tax_liability_if_not_itemizing(Variable): definition_period = YEAR def formula(tax_unit, period, parameters): - simulation = tax_unit.simulation - non_itemized_branch = get_override_branch( - simulation, - "not_itemizing", - period, - {"tax_unit_itemizes": np.zeros((tax_unit.count,), dtype=bool)}, - ) + non_itemized_branch = get_itemization_branch(tax_unit, period, itemizes=False) return non_itemized_branch.calculate("income_tax", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py index ab7736c1835..1625937db1e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tax_unit_itemizes.py @@ -18,14 +18,10 @@ def formula(tax_unit, period, parameters): # qualified business income deduction, which depends on the # itemization choice, so they come from the branch that itemizes # (the one that also gives tax_liability_if_itemizing). - # Calculated before branching, so the branch inherits it. + # Calculated before branching, so the branch inherits it when no + # one repays claim of right income above $3,000. tax_unit("total_itemized_taxable_income_deductions", period) - itemizing_branch = get_override_branch( - tax_unit.simulation, - "itemizing", - period, - {"tax_unit_itemizes": np.ones((tax_unit.count,), dtype=bool)}, - ) + itemizing_branch = get_itemization_branch(tax_unit, period, itemizes=True) itemized_deductions = itemizing_branch.calculate( "itemized_taxable_income_deductions", 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..e4a00c0745e --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_claim_of_right_deduction.py @@ -0,0 +1,30 @@ +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 allows as an itemized " + "deduction: through 2023 on Form 2 itemized deductions line 18, and " + "from 2024 within the federal itemized deductions on Form 2 line 2." + ) + 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", + # 2024 Form 2 instructions, line 2, Worksheet A line 1: "Enter your + # total federal itemized deductions from Form 1040, line 12". + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=15", + ) + 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/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_offset.py b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_offset.py new file mode 100644 index 00000000000..c498cdb6cbf --- /dev/null +++ b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_offset.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nj.tax.income.adjusted_gross_income.claim_of_right.nj_claim_of_right_repayment_category import ( + CATEGORY_SOURCES, + nj_claim_of_right_category_offset, +) + + +class nj_claim_of_right_offset(Variable): + value_type = float + entity = Person + label = "New Jersey claim of right repayment offset" + unit = USD + documentation = ( + "Reduction in a person's New Jersey gross income for a claim of " + "right repayment. New Jersey applies a repayment against income of " + "the same category in the year of repayment, and a category with a " + "net negative amount is reported as zero. A repayment offsets the " + "repayer's own taxable income in the category first; on a joint " + "return any excess offsets the other spouse's. New Jersey has no " + "counterpart to the section 1341 credit, so the federal method does " + "not matter." + ) + definition_period = YEAR + reference = ( + # N.J.S.A. 54A:5-2 + "https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634", + # TAM 2011-7, Claim of Right Repayment Refunds (IRC 1341) + "https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf", + ) + defined_for = StateCode.NJ + + def formula(person, period, parameters): + offset = 0 + for category in CATEGORY_SOURCES: + offset = offset + nj_claim_of_right_category_offset( + person, period, parameters, category + ) + return offset diff --git a/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_pension_offset.py b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_pension_offset.py new file mode 100644 index 00000000000..ef6b4f13539 --- /dev/null +++ b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_pension_offset.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nj.tax.income.adjusted_gross_income.claim_of_right.nj_claim_of_right_repayment_category import ( + NJClaimOfRightRepaymentCategory, + nj_claim_of_right_category_offset, +) + + +class nj_claim_of_right_pension_offset(Variable): + value_type = float + entity = Person + label = "New Jersey claim of right offset against pensions and annuities" + unit = USD + documentation = ( + "The part of nj_claim_of_right_offset that reduces the person's " + "pensions, annuities and IRA withdrawals (NJ-1040 line 20a), the " + "base of the pension exclusion." + ) + definition_period = YEAR + reference = ( + # N.J.S.A. 54A:5-2 + "https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634", + "https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf", + # 2025 NJ-1040 instructions, line 28a, Line A + "https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=22", + ) + defined_for = StateCode.NJ + + def formula(person, period, parameters): + return nj_claim_of_right_category_offset( + person, + period, + parameters, + NJClaimOfRightRepaymentCategory.PENSIONS_AND_ANNUITIES, + ) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_repayment_category.py b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_repayment_category.py new file mode 100644 index 00000000000..078539e8ef0 --- /dev/null +++ b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_repayment_category.py @@ -0,0 +1,102 @@ +from policyengine_us.model_api import * + + +class NJClaimOfRightRepaymentCategory(Enum): + WAGES = "Salaries, wages and other remuneration (category a)" + INTEREST = "Interest (category e)" + DIVIDENDS = "Dividends (category f)" + GAMBLING_WINNINGS = "Gambling winnings (category g)" + PENSIONS_AND_ANNUITIES = "Pensions, annuities and IRA withdrawals (category j)" + ALIMONY = "Alimony and separate maintenance (category n)" + OTHER = "Another category of New Jersey gross income (not offset in the model)" + NOT_TAXED = "Income New Jersey did not tax when received" + + +# The parameter list of income sources for each category the model offsets. +CATEGORY_SOURCES = { + NJClaimOfRightRepaymentCategory.WAGES: "wages", + NJClaimOfRightRepaymentCategory.INTEREST: "interest", + NJClaimOfRightRepaymentCategory.DIVIDENDS: "dividends", + NJClaimOfRightRepaymentCategory.GAMBLING_WINNINGS: "gambling_winnings", + NJClaimOfRightRepaymentCategory.PENSIONS_AND_ANNUITIES: "pensions_and_annuities", + NJClaimOfRightRepaymentCategory.ALIMONY: "alimony", +} + + +# Amounts in a category's sources that New Jersey does not tax and subtracts +# from gross income separately, so a repayment cannot offset them. +CATEGORY_EXEMPT_SOURCES = { + NJClaimOfRightRepaymentCategory.INTEREST: "interest", +} + + +def nj_claim_of_right_category_offset(person, period, parameters, category): + """A person's share of the return's claim of right offset in one category. + + New Jersey applies a repayment against income of the same category in the + year of repayment, and reports a category left negative as zero (N.J.S.A. + 54A:5-2; TAM 2011-7). A repayment first offsets the repayer's own taxable + income in the category; on a joint return, whose category line combines + both spouses, any excess offsets the other spouse's income in it. So the + return's offset is the smaller of the filers' repayments of the category + and their income in it, and no one's income in the category falls below + zero. Dependents' repayments and income belong on their own returns. + """ + gross_income = parameters(period).gov.states.nj.tax.income.gross_income + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + repaid_category = person("nj_claim_of_right_repayment_category", period) + repayment = ( + person("claim_of_right_repayment", period) + * head_or_spouse + * (repaid_category == category) + ) + sources = getattr( + gross_income.claim_of_right_category_sources, CATEGORY_SOURCES[category] + ) + income = add(person, period, sources) + if category in CATEGORY_EXEMPT_SOURCES: + exempt = getattr( + gross_income.claim_of_right_category_exempt_sources, + CATEGORY_EXEMPT_SOURCES[category], + ) + income = income - add(person, period, exempt) + income = max_(income, 0) * head_or_spouse + own_offset = min_(repayment, income) + # The tax unit sums below are repeated for each member. + excess = person.tax_unit.sum(repayment - own_offset) + remaining_income = income - own_offset + return_remaining_income = person.tax_unit.sum(remaining_income) + excess_offset = min_(excess, return_remaining_income) + share = np.divide( + remaining_income, + return_remaining_income, + out=np.zeros_like(remaining_income), + where=return_remaining_income > 0, + ) + return own_offset + share * excess_offset + + +class nj_claim_of_right_repayment_category(Variable): + value_type = Enum + possible_values = NJClaimOfRightRepaymentCategory + default_value = NJClaimOfRightRepaymentCategory.WAGES + entity = Person + label = "New Jersey gross income category of a claim of right repayment" + documentation = ( + "The New Jersey gross income category (N.J.S.A. 54A:5-1) of the " + "income repaid in claim_of_right_repayment. New Jersey offsets the " + "repayment only against income of the same category in the year of " + "repayment. NOT_TAXED is income New Jersey did not tax when " + "received, such as unemployment compensation or Social Security " + "benefits. OTHER is a category the model does not offset, such as " + "prizes and awards or income from estates and trusts. Defaults to " + "wages, the category of TAM 2011-7's example." + ) + definition_period = YEAR + reference = ( + "https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/", + # N.J.S.A. 54A:5-2 + "https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634", + "https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf", + ) + defined_for = StateCode.NJ diff --git a/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_wage_offset.py b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_wage_offset.py new file mode 100644 index 00000000000..6b8920461d0 --- /dev/null +++ b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/claim_of_right/nj_claim_of_right_wage_offset.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nj.tax.income.adjusted_gross_income.claim_of_right.nj_claim_of_right_repayment_category import ( + NJClaimOfRightRepaymentCategory, + nj_claim_of_right_category_offset, +) + + +class nj_claim_of_right_wage_offset(Variable): + value_type = float + entity = Person + label = "New Jersey claim of right offset against wages" + unit = USD + documentation = ( + "The part of nj_claim_of_right_offset that reduces the person's " + "wages (NJ-1040 line 15), which Worksheet D's earned income test " + "reads." + ) + definition_period = YEAR + reference = ( + # N.J.S.A. 54A:5-2 + "https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634", + "https://www.nj.gov/treasury/taxation/pdf/pubs/tams/tam7.pdf", + # 2025 NJ-1040 instructions, Worksheet D line 4 + "https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=23", + ) + defined_for = StateCode.NJ + + def formula(person, period, parameters): + return nj_claim_of_right_category_offset( + person, period, parameters, NJClaimOfRightRepaymentCategory.WAGES + ) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/nj_gross_income.py b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/nj_gross_income.py index bad7256143f..2432cdb5979 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/nj_gross_income.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/adjusted_gross_income/nj_gross_income.py @@ -10,6 +10,8 @@ class nj_gross_income(Variable): reference = ( "https://law.justia.com/codes/new-jersey/title-54a/section-54a-5-1/", "https://www.nj.gov/treasury/taxation/pdf/current/1040.pdf", + # N.J.S.A. 54A:5-2 + "https://lis.njleg.state.nj.us/nxt/gateway.dll/statutes/1/53569/53634", ) defined_for = StateCode.NJ @@ -24,4 +26,6 @@ def formula(person, period, parameters): total = total + max_(add(person, period, cats.category_d), 0) total = total + max_(add(person, period, cats.category_h), 0) total = total + max_(add(person, period, cats.category_k_p), 0) - return total + # A claim of right repayment offsets income of its own category, + # not below zero (N.J.S.A. 54A:5-2). + return total - person("nj_claim_of_right_offset", period) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_eligible_pension_income.py b/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_eligible_pension_income.py index d3b1701e2c3..5e4d2583000 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_eligible_pension_income.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_eligible_pension_income.py @@ -26,4 +26,11 @@ def formula(person, period, parameters): role_eligible = is_head | is_spouse eligible = demographic_eligible & role_eligible - return eligible * add(person, period, p.sources) + # Line A is line 20a, which a claim of right repayment of pension + # income reduces (N.J.S.A. 54A:5-2; TAM 2011-7). + pension_income = max_( + add(person, period, p.sources) + - person("nj_claim_of_right_pension_offset", period), + 0, + ) + return eligible * pension_income diff --git a/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_other_retirement_income_exclusion.py b/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_other_retirement_income_exclusion.py index f6dc7e5ff8e..d02aa8a60b4 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_other_retirement_income_exclusion.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/exclusions/nj_other_retirement_income_exclusion.py @@ -42,7 +42,11 @@ def formula(tax_unit, period, parameters): unused_exclusion = max_(0, maximum_exclusion - used) # calculate earnings eligibility - earnings = add(tax_unit, period, ["earned_income"]) + # Line 4 is NJ-1040 line 15, wages net of any claim of right + # repayment of wages (N.J.S.A. 54A:5-2; TAM 2011-7). + earnings = add(tax_unit, period, ["earned_income"]) - add( + tax_unit, period, ["nj_claim_of_right_wage_offset"] + ) limit = p.other_retirement_income.earned_income_threshold earnings_eligible = earnings <= limit 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