Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1f0cd1b
Make head_is_dependent_elsewhere consumers independent of the head label
MaxGhenis Oct 8, 2026
3ecc675
Keep tax units without a filer eligible where only claimed filers are…
MaxGhenis Oct 9, 2026
8da8312
Address review: Hawaii worksheet earnings, NM same-filer age, childle…
MaxGhenis Oct 9, 2026
e838623
Allow a cent of rounding in the Hawaii self-employed joint pair
MaxGhenis Oct 9, 2026
41fb4a5
Address review round 2: worksheet earnings, minors' own status, input…
MaxGhenis Oct 9, 2026
527d47f
Merge remote-tracking branch 'upstream/main' into head-dependent-else…
MaxGhenis Oct 9, 2026
436805e
Merge the Hawaii deductions references after syncing main
MaxGhenis Oct 9, 2026
23c6229
Apply the federal rules for a filer who can be claimed as a dependent
MaxGhenis Oct 9, 2026
ab8304d
Apply NJ, ND, OH, OK and PA rules for a filer who can be claimed as a…
MaxGhenis Oct 10, 2026
4597c55
Address the federal claimable-filer review
MaxGhenis Oct 10, 2026
e4b8f36
Separate dependency facts and fix Hawaii qualified-person counts
MaxGhenis Oct 10, 2026
c7f969f
Clarify claimability outside the current tax unit
MaxGhenis Oct 10, 2026
4c8ed81
Merge commit 'c7f969f872' into claimable-filer-federal
MaxGhenis Oct 10, 2026
3f88702
Read the split dependency inputs from #10024
MaxGhenis Oct 10, 2026
b2771d2
Follow the corrected exemption count in three Hawaii credits
MaxGhenis Oct 10, 2026
163fcd4
Address the second federal claimable-filer review
MaxGhenis Oct 10, 2026
3d0864f
Address the third federal claimable-filer review
MaxGhenis Oct 10, 2026
35a2cbc
Give the spouse without an SSN a card type in the recovery rebate test
MaxGhenis Oct 10, 2026
54e3029
Check the Oklahoma child care / child tax credit for swap invariance …
MaxGhenis Oct 11, 2026
a8686e1
Merge remote-tracking branch 'upstream/claimable-filer-federal' into …
MaxGhenis Oct 11, 2026
41f1c19
Read claimability, the claimant filing exception and the actual claim…
MaxGhenis Oct 11, 2026
5bc3936
Add cases for a filer who can be claimed but is not, and for a claima…
MaxGhenis Oct 11, 2026
f4b4ba8
Cite North Carolina's child deduction as G.S. 105-153.5(a1)
MaxGhenis Oct 11, 2026
1660875
Merge main into claimable-filer-nj-pa after #10087
MaxGhenis Oct 16, 2026
6f241ac
Quote claimable-filer YAML test names containing colons
MaxGhenis Oct 16, 2026
578bc49
Merge main into claimable-filer-nj-pa
MaxGhenis Nov 5, 2026
6ee4524
Follow statutory dependent-filer rules and resolve review findings
MaxGhenis Nov 5, 2026
547ab47
Clarify dependent-filer coverage and tighten invariants
MaxGhenis Nov 5, 2026
27ead42
Give the NJ EITC property test the dependent-filer inputs
MaxGhenis Nov 6, 2026
5f363a0
Limit Ohio medical deductions for claimable filers
MaxGhenis Nov 6, 2026
aa18655
Merge main
MaxGhenis Nov 6, 2026
611ebd6
Merge main into claimable-filer-nj-pa
MaxGhenis Nov 7, 2026
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 the federal dependent test and refund-only filing exception to New Jersey, Ohio, Oklahoma and Pennsylvania dependent counts. Exclude dependents' Ohio medical expenses when another taxpayer can claim a filer, retaining the head's and spouse's expenses. Remove Oklahoma's regular exemption for a claimable filer and retain its special and blind exemptions. Deny Pennsylvania tax forgiveness to claimable filers without a parental exception, and preserve the other spouse's forgiveness on their own tax. Allow North Dakota's stillborn subtraction without a dependency condition. Limit Ohio tuition credits to dependents and New Jersey and Oklahoma childless EITC age tests to filers, regardless of claimability.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
description: North Dakota provides this subtraction from federal taxable income for each child who was stillborn during the tax year, provided a Fetal Death Record was filed with the North Dakota Division of Vital Records and the child would have been a dependent. The amount is adjusted annually for inflation.
description: North Dakota provides this subtraction from federal taxable income for each birth resulting in stillbirth during the tax year with a filed fetal death certificate. The statute imposes no dependency condition. North Dakota adjusts the amount annually for inflation.
values:
2021-01-01: 4_310
2022-01-01: 4_530
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,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
2 changes: 2 additions & 0 deletions policyengine_us/tests/code_health/test_source_conflicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
"policyengine_us/variables/gov/states/mn/dcyf/mfip/income/earned/mn_mfip_dependent_care_deduction.py": 1,
"policyengine_us/variables/gov/states/mn/tax/income/deductions/mn_itemized_deductions.py": 1,
"policyengine_us/variables/gov/states/mo/dese/ccs/eligibility/mo_ccs_income_eligible.py": 1,
"policyengine_us/variables/gov/states/nd/tax/income/subtractions/nd_stillborn_subtraction.py": 1,
"policyengine_us/variables/gov/states/pa/tax/income/forgiveness/pa_tax_forgiveness_eligible.py": 1,
"policyengine_us/variables/gov/states/sc/tax/income/subtractions/retirement/sc_retirement_deduction_survivors.py": 1,
"policyengine_us/variables/gov/states/vt/tax/income/credits/cdcc/vt_low_income_cdcc_eligible.py": 1,
"policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_categorical_eligible.py": 1,
Expand Down
138 changes: 138 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,138 @@
"""Check claimable-filer invariants in New Jersey, North Carolina, North Dakota,
Ohio, Oklahoma and Pennsylvania.

Hypothesis draws couples with each adult's claimability, actual claim and
refund-only claimant exception. These tests exchange head and spouse labels
and check that each output stays unchanged. They also mark another filer as
claimed and check that the listed exemptions, credits, deductions and
forgiveness never rise, using generated and seeded populations.

For Pennsylvania, they bound the eligible share between zero and one and
require a full share when neither filer can be claimed. They also check that
forgiveness stays between zero and tax before forgiveness. YAML cases cover
North Dakota's stillborn subtraction; these populations supply no stillborn
children.
"""

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

from policyengine_us.system import system
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,
_calc,
_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 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(adults()), draw(adults())],
"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
]


def _pa_forgiveness_bounds(sim, year):
share = _calc(sim, "pa_tax_forgiveness_eligible_share", year)
assert ((share >= 0) & (share <= 1)).all()
in_pa = np.asarray(sim.calculate("PA", year, map_to="tax_unit"), dtype=bool)
claimable_filer = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0
np.testing.assert_allclose(share[in_pa & ~claimable_filer], 1, atol=0, rtol=0)
forgiveness = _calc(sim, "pa_tax_forgiveness_amount", year)
tax = _calc(sim, "pa_income_tax_before_forgiveness", year)
assert (forgiveness >= 0).all()
assert (forgiveness <= tax).all()


def test_pa_tax_forgiveness_has_no_parent_qualification_input():
assert "pa_tax_forgiveness_parent_qualifies" not in system.variables


@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, _pa_forgiveness_bounds)
check_monotone(_claim_only(units), year, MONOTONE, _pa_forgiveness_bounds)


@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, _pa_forgiveness_bounds)
check_monotone(units, year, MONOTONE, _pa_forgiveness_bounds)
Loading
Loading