Skip to content
4 changes: 4 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- bump: minor
changes:
added:
- Add NY tax calculation for filters with AGI above $25 million integration test.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
- name: 36828-NY.yaml
absolute_error_margin: 2
period: 2024
input:
people:
person1:
age: 40
employment_income: 31_920_562
ssi: 0
wic: 0
head_start: 0
early_head_start: 0
commodity_supplemental_food_program: 0
taxable_interest_income: 1_519_734
is_tax_unit_head: true
tax_units:
tax_unit:
members: [person1]
premium_tax_credit: 0
local_income_tax: 0
state_sales_tax: 0
spm_units:
spm_unit:
members: [person1]
snap: 0
tanf: 0
free_school_meals: 0
reduced_price_school_meals: 0
households:
household:
members: [person1]
state_fips: 36
output:
ny_income_tax: 3_644_120
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
# 2021 NY Form IT-201 Instructions Tax computation worksheet 5

- name: Unit test 6 for 2021 New York supplemental tax married filing jointly and qualifying surviving spouse
absolute_error_margin: 0.1
absolute_error_margin: 2
period: 2021
input:
state_code: NY
Expand Down Expand Up @@ -129,13 +129,13 @@
# 2021 NY Form IT-201 Instructions Tax computation worksheet 10

- name: Unit test 5 for 2021 New York supplemental tax single and married filling separately filer
absolute_error_margin: 0.1
absolute_error_margin: 2
period: 2021
input:
state_code: NY
filing_status: SINGLE
ny_agi: 30_000_000 # AGI > $25,000,000
ny_taxable_income: 21_000_000
ny_taxable_income: 21_000_000
ny_main_income_tax: 2_098_682.5
output:
ny_supplemental_tax: 190_317.5 # 21,000,000 * 10.9% - 2,098,682.5
Expand Down Expand Up @@ -194,13 +194,13 @@
# 2021 NY Form IT-201 Instructions Tax computation worksheet 15

- name: Unit test 5 for 2021 New York supplemental tax head of household filer
absolute_error_margin: 0.1
absolute_error_margin: 2
period: 2021
input:
input:
state_code: NY
filing_status: HEAD_OF_HOUSEHOLD
filing_status: HEAD_OF_HOUSEHOLD
ny_agi: 30_000_000 # AGI > $25,000,000
ny_taxable_income: 28_000_000
ny_taxable_income: 28_000_000
ny_main_income_tax: 2_822_096.8
output:
ny_supplemental_tax: 229_903.2 # 28,000,000 * 10.9% - 2,822,096.8
Expand Down Expand Up @@ -295,9 +295,23 @@
period: 2028
input:
state_code: NY
filing_status: SEPARATE
filing_status: SEPARATE
ny_agi: 220_000
ny_taxable_income: 200_000
ny_main_income_tax: 11_963.755
output:
ny_supplemental_tax: 568

- name: Unit test for 2024 single filer with AGI above $25M (flat 10.9% rate)
absolute_error_margin: 2
period: 2024
input:
state_code: NY
filing_status: SINGLE
ny_agi: 33_440_296
ny_taxable_income: 33_432_296
ny_main_income_tax: 3_429_049.71
output:
# For AGI > $25M, supplemental tax = (taxable_income * 10.9%) - main_tax
# = 33_432_296 * 0.109 - 3_429_049.71 = 215_070.55
ny_supplemental_tax: 215_070.55
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
from policyengine_us.model_api import *
from policyengine_core.taxscales import MarginalRateTaxScale
import numpy as np


def get_last_finite_threshold(scale):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a separate function for this? Can we not just do it in the formula? I assume we wont reuse this?

"""Get the last non-infinity threshold from a tax scale.

For NY tax scales, the last threshold may be infinity for certain years.
This function returns the last finite threshold value instead.
"""
thresholds = scale.thresholds
if np.isinf(thresholds[-1]):
return thresholds[-2]
return thresholds[-1]


class ny_supplemental_tax(Variable):
Expand Down Expand Up @@ -53,20 +66,18 @@ def formula(tax_unit, period, parameters):
applicable_amount / p.phase_in_length,
)

# edge case for high agi
agi_limit = select(
# For AGI above the high threshold, apply flat top rate to all income
# Get the last finite threshold from each scale (handles infinity in 2022+)
high_agi_threshold = select(
in_each_status,
[
single.thresholds[-1],
joint.thresholds[-1],
hoh.thresholds[-1],
surviving_spouse.thresholds[-1],
separate.thresholds[-1],
],
[get_last_finite_threshold(scale) for scale in scales],
)
# Create array for marginal_rates lookup
# Use +100 for float32 precision at $25M threshold
high_agi_lookup = ny_agi * 0 + high_agi_threshold + 100
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not +1 to account for the threshold. Not sure why we need a $100 buffer

high_agi_rate = select(
in_each_status,
[scale.marginal_rates(agi_limit + 1) for scale in scales],
[scale.marginal_rates(high_agi_lookup) for scale in scales],
)

supplemental_tax_high_agi = (
Expand Down Expand Up @@ -105,13 +116,13 @@ def formula(tax_unit, period, parameters):
)

return where(
ny_agi > agi_limit,
ny_agi > high_agi_threshold,
supplemental_tax_high_agi,
supplemental_tax_general,
)

return where(
ny_agi > agi_limit,
ny_agi > high_agi_threshold,
supplemental_tax_high_agi,
0,
)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.