Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/claimable-filer-nj-pa.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply New Jersey, North Dakota, Ohio, Oklahoma and Pennsylvania rules for a filer who can be claimed as a dependent: no NJ dependent, college or child tax credit dependents, NJ childless EITC age fallback, North Dakota stillborn subtraction, Ohio tuition credit dependents, Oklahoma regular or dependent exemptions, sales tax credit exemptions or frozen childless EITC, or Pennsylvania dependent child allowance, and Pennsylvania Tax Forgiveness only on the tax of a filer who is not claimable or whose claiming parent qualifies.
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,29 @@ def formula(tax_unit, period, parameters):
fagi = tax_unit("adjusted_gross_income", period) - roth_conversions
filing_status = tax_unit("filing_status", period)
agi_eligible = fagi <= p.special_agi_limit[filing_status]
# Regular exemptions as in the baseline: none for a filer who can
# be claimed as a dependent on another return.
claimable = person("claimable_as_dependent_on_another_return", period)
head_claimable = tax_unit.any(
person("is_tax_unit_head", period) & claimable
)
spouse_claimable = tax_unit.any(
person("is_tax_unit_spouse", period) & claimable
)
# head exemptions
age_eligible = tax_unit("age_head", period) >= p.special_age_minimum
head_exemptions = where(tax_unit("blind_head", period), 2, 1) + where(
agi_eligible & age_eligible, 1, 0
head_exemptions = (
where(head_claimable, 0, 1)
+ where(tax_unit("blind_head", period), 1, 0)
+ where(agi_eligible & age_eligible, 1, 0)
)
# spouse exemptions
age_eligible = tax_unit("age_spouse", period) >= p.special_age_minimum
spouse_exemptions = where(
filing_status == filing_status.possible_values.JOINT,
(
where(tax_unit("blind_spouse", period), 2, 1)
where(spouse_claimable, 0, 1)
+ where(tax_unit("blind_spouse", period), 1, 0)
+ where(agi_eligible & age_eligible, 1, 0)
),
0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ def formula(tax_unit, period, parameters):
period
).gov.states.nj.tax.income.exemptions.dependents.amount
person = tax_unit.members
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)).
# Publication 501 excepts a return whose would-be claimant files only
# for a refund.
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
# Age gate: when in effect, only dependents under the chosen age
# are re-priced to the reform amount; older dependents keep the
# baseline exemption. Otherwise every dependent is re-priced,
# matching the baseline tax_unit_dependents count at the default.
# matching the baseline dependent count at the default.
if p.age_limit.in_effect:
age = person("age", period)
young = is_dependent & (age < p.age_limit.threshold)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ def formula(tax_unit, period, parameters):

person = tax_unit.members
age = person("age", period)
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)).
# Publication 501 excepts a return whose would-be claimant files only
# for a refund.
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)

# Apply age limit if in effect
if p.age_limit.in_effect:
Expand Down Expand Up @@ -49,7 +55,11 @@ class ok_older_dependents_count(Variable):

def formula(tax_unit, period, parameters):
person = tax_unit.members
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)).
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
total_dependents = tax_unit.sum(is_dependent)
eligible_dependent_exemptions = tax_unit(
"ok_eligible_dependents_count", period
Expand All @@ -67,9 +77,16 @@ class ok_exemptions(Variable):
def formula(tax_unit, period, parameters):
p_base = parameters(period).gov.states.ok.tax.income.exemptions

# Personal exemptions exclude the dependent portion.
# Personal exemptions exclude the dependent portion, which
# ok_count_exemptions counts only when no filer can be claimed as
# a dependent (IRC 152(b)(1)).
total_exemptions_count = tax_unit("ok_count_exemptions", period)
dependents = tax_unit("tax_unit_dependents", period)
filer_is_dependent = tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
dependents = where(
filer_is_dependent, 0, tax_unit("tax_unit_dependents", period)
)
older_dependents = tax_unit("ok_older_dependents_count", period)
personal_exemptions_count = (
total_exemptions_count - dependents + older_dependents
Expand Down
11 changes: 10 additions & 1 deletion policyengine_us/reforms/taxsim.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ def formula(tax_unit, period, parameters):
is_child_dependent = person("is_child_of_tax_head", period) & person(
"is_tax_unit_dependent", period
)
child_dependents = tax_unit.sum(is_child_dependent)
# As in the baseline, a return on which the filer (or, if joint,
# either spouse) can be claimed as a dependent has no dependents.
# Publication 501 excepts a return whose would-be claimant files only
# for a refund.
filer_is_dependent = tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
child_dependents = where(
filer_is_dependent, 0, tax_unit.sum(is_child_dependent)
)
# filing status affects the base, where it doubles for married claimants
filing_status = tax_unit("filing_status", period)
filing_statuses = filing_status.possible_values
Expand Down
132 changes: 132 additions & 0 deletions policyengine_us/tests/core/test_claimable_filer_nj_pa_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""New Jersey, North Carolina, North Dakota, Ohio, Oklahoma and Pennsylvania
rules for a filer who can be claimed as a dependent.

A return on which the filer (or, if joint, either spouse) can be claimed as a
dependent has no dependents (IRC 152(b)(1)), so the NJ dependent, college
and child tax credit counts, Ohio's tuition credit, Oklahoma's dependent
exemptions and sales tax credit dependents, North Dakota's stillborn
subtraction and Pennsylvania's dependent child allowance count none. A
claimable filer has no Oklahoma regular exemption, no childless NJ or
Oklahoma EITC, and claims Pennsylvania Tax Forgiveness only on their own tax
and only when the parent who claims them qualifies.

For couples drawn by Hypothesis (with claimability, the actual claim and the
refund-only claimant exception drawn per adult) and a seeded population in
these states, each output is the same under either head/spouse labelling, and
marking another filer as claimed never raises the exemptions, credits,
deductions and forgiveness changed here.
"""

import pytest
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st

from policyengine_us.tests.core.test_claimable_filer_federal_invariants import (
check_monotone,
check_swap,
)
from policyengine_us.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import (
CLAIM_PATTERNS,
_crafted_cases,
_seeded_couples,
adults,
dependents,
money,
)

STATES = ["NJ", "NC", "ND", "OH", "OK", "PA"]
YEARS = [2022, 2025, 2026]
# Amounts that marking another filer as claimed must never raise.
MONOTONE = [
"nj_dependents_exemption",
"nj_dependents_attending_college_exemption",
"nj_ctc",
"nj_eitc",
"nc_child_deduction",
"oh_non_public_school_credits_potential",
"ok_count_exemptions",
"ok_stc",
"ok_eitc",
"pa_tax_forgiveness_eligible_share",
"pa_tax_forgiveness_amount",
]
# The Oklahoma child care / child tax credit takes the larger of a care and a
# child tax credit branch; the care branch follows the federal credit, whose
# tax-liability limit can rise when a filer is claimable, so it is checked for
# swap invariance only. Taxes can rise too.
OUTPUTS = MONOTONE + [
"ok_child_care_child_tax_credit",
"nj_income_tax",
"nc_income_tax",
"nd_income_tax",
"oh_income_tax",
"ok_income_tax",
"pa_income_tax",
]


@st.composite
def filers(draw):
# Whether the parent who claims this adult qualifies for Pennsylvania
# Tax Forgiveness (read only when the adult can be claimed).
return {
**draw(adults()),
"pa_tax_forgiveness_parent_qualifies": draw(st.booleans()),
}


@st.composite
def children(draw):
child = draw(dependents())
return {
**child,
"is_full_time_college_student": child["is_full_time_student"],
"non_public_school_tuition": draw(money(3_000)),
}


@st.composite
def couples(draw):
# As in the swap-invariance test: who can be claimed, who of them is
# actually claimed, and, for the unclaimed, whether the would-be claimant
# files only for a refund (Publication 501's exception).
claimable = draw(st.sampled_from(CLAIM_PATTERNS))
claimed = tuple(can_claim and draw(st.booleans()) for can_claim in claimable)
return {
"state": draw(st.sampled_from(STATES)),
"adults": [draw(filers()), draw(filers())],
"dependents": draw(st.lists(children(), max_size=2)),
"claimed": claimed,
"claimable": claimable,
"exception": tuple(
not is_claimed and draw(st.booleans()) for is_claimed in claimed
),
}


def _claim_only(units):
# Monotonicity marks filers as claimed; without separate claimability or
# exception inputs, claimability follows the claim.
return [
{k: v for k, v in unit.items() if k not in ("claimable", "exception")}
for unit in units
]


@settings(
max_examples=2,
deadline=None,
derandomize=True,
suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
)
@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=12))
def test_nj_pa_claimable_filer_rules_are_label_free(year, units):
check_swap(units, year, OUTPUTS)
check_monotone(_claim_only(units), year, MONOTONE)


@pytest.mark.parametrize("year", YEARS)
def test_nj_pa_claimable_filer_rules_seeded_population(year):
units = _seeded_couples(STATES, per_state=2) + _crafted_cases(STATES)
check_swap(units, year, OUTPUTS)
check_monotone(units, year, MONOTONE)
Loading