From 5415450663a2a8f4564bb3ceb516b0a6b54e07fc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 21:19:35 -0400 Subject: [PATCH 1/4] Count only tips and overtime received by a head or spouse with a valid SSN 26 U.S.C. 224(e)(1) and 225(d)(1) require "such individual's social security number", meaning the recipient's. The 2025 Schedule 1-A cautions and Form 1040 instructions say the spouse who received the tips or overtime must have a valid SSN, and proposed Treas. Reg. 1.224-1(e)(2) needs both SSNs only when both spouses claim tips. The model instead denied both deductions to any joint return with one spouse lacking a valid SSN. The deductions also summed every tax-unit member's tips and overtime, so a dependent's amounts raised the filer's deduction although the dependent's wages are outside the filer's gross income (irs_gross_income; 26 U.S.C. 73(a) for a child's services). New person-level tip_income_deduction_eligible_person and overtime_income_deduction_eligible_person mark a head or spouse with a valid SSN; only their amounts count. The tax-unit SSN variables now hold when any head or spouse has a valid SSN, and stay the deductions' gates. Co-Authored-By: Claude Opus 5.5 --- .../fix-tips-overtime-dependents-ssn.fixed.md | 1 + .../eligible_ssn_card_type.yaml | 8 +- .../tip_income/eligible_ssn_card_type.yaml | 8 +- ...t_tip_and_overtime_deduction_properties.py | 231 ++++++++++++++---- .../overtime_income_deduction.yaml | 154 ++++++++++++ ...time_income_deduction_eligible_person.yaml | 89 +++++++ ..._income_deduction_ssn_requirement_met.yaml | 56 ++++- .../tip_income/tip_income_deduction.yaml | 138 ++++++++++- .../tip_income_deduction_eligible_person.yaml | 89 +++++++ ...duction_ineligible_cardholder_present.yaml | 53 ---- ..._income_deduction_ssn_requirement_met.yaml | 107 ++++++++ .../overtime_income_deduction.py | 5 +- ...ertime_income_deduction_eligible_person.py | 27 ++ ...me_income_deduction_ssn_requirement_met.py | 16 +- .../tip_income/tip_income_deduction.py | 5 +- .../tip_income_deduction_eligible_person.py | 26 ++ ...ip_income_deduction_ssn_requirement_met.py | 16 +- 17 files changed, 903 insertions(+), 126 deletions(-) create mode 100644 changelog.d/fix-tips-overtime-dependents-ssn.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml delete mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ineligible_cardholder_present.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py diff --git a/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md b/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md new file mode 100644 index 00000000000..53d12bc7132 --- /dev/null +++ b/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md @@ -0,0 +1 @@ +Count only tips and overtime received by a head or spouse with a valid SSN toward the qualified tips and overtime deductions (26 U.S.C. 224(e)(1) and 225(d)(1)). A dependent's tips and overtime no longer raise the filer's deductions, and a joint return on which one spouse lacks a valid SSN now deducts the other spouse's tips and overtime instead of nothing. diff --git a/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml b/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml index 90377ed5557..5e72d80e994 100644 --- a/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml @@ -1,4 +1,4 @@ -description: The IRS limits the overtime income deduction to filers with one of these SSN Card types. +description: The IRS counts overtime toward the overtime income deduction only when the head or spouse who received it has one of these SSN card types. values: 2018-01-01: - CITIZEN @@ -9,5 +9,11 @@ metadata: period: year unit: list reference: + - title: 26 U.S.C. 225(d) + href: https://www.law.cornell.edu/uscode/text/26/225#d + - title: 26 U.S.C. 24(h)(7) + href: https://www.law.cornell.edu/uscode/text/26/24#h_7 + - title: 2025 Form 1040 instructions, Schedule 1-A, Part III + href: https://www.irs.gov/pub/irs-pdf/i1040gi.pdf#page=106 - title: H.R.1 - One Big Beautiful Bill Act href: https://www.congress.gov/bill/119th-congress/house-bill/1/text diff --git a/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml b/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml index e5c4510e96f..fdf1946f5ba 100644 --- a/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml @@ -1,4 +1,4 @@ -description: The IRS limits the tip income deduction to filers with one of these SSN Card types. +description: The IRS counts tips toward the tip income deduction only when the head or spouse who received them has one of these SSN card types. values: 2018-01-01: - CITIZEN @@ -9,5 +9,11 @@ metadata: period: year unit: list reference: + - title: 26 U.S.C. 224(e) + href: https://www.law.cornell.edu/uscode/text/26/224#e + - title: 26 U.S.C. 24(h)(7) + href: https://www.law.cornell.edu/uscode/text/26/24#h_7 + - title: 2025 Form 1040 instructions, Schedule 1-A, Part II + href: https://www.irs.gov/pub/irs-pdf/i1040gi.pdf#page=101 - title: H.R.1 - One Big Beautiful Bill Act href: https://www.congress.gov/bill/119th-congress/house-bill/1/text diff --git a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py index 14ca653d774..579c3965c99 100644 --- a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py +++ b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py @@ -9,13 +9,23 @@ either deduction only on a joint return, and the Part II and Part III cautions say "If married, you must file jointly to claim this deduction." +Sections 224(e)(1) and 225(d)(1) allow no deduction unless the return includes +"such individual's social security number", and the same cautions say "You +and/or your spouse who received qualified tips must have a valid social +security number" (and the same for overtime). So only amounts received by a +head or spouse with a valid SSN count, and a spouse with nothing to claim needs +none. A dependent's tips and overtime are the dependent's own income (section +73(a) for a child's services) and belong on the dependent's own return. + Each test call, and each Hypothesis example, builds one simulation of many tax -units. Joint returns have two spouses, so the combined caps and the -both-spouses SSN rule apply as on the return. The tests check: +units. Joint returns have two spouses, so the combined caps apply as on the +return. Seeded draws also vary each head's and spouse's SSN card type and add +dependents with tips and overtime of their own. The tests check: 1. Every seeded draw, evaluated under all five filing statuses, matches an independent restatement of Schedule 1-A lines 7-13 (tips) and 15-21 - (overtime), with zero for married filing separately. + (overtime) on the amounts that count, with zero for married filing + separately. 2. Each deduction lies between 0 and its capped qualifying amount, and a positive deduction is the capped amount less a whole multiple of $100. These follow from 1, and are checked on the model's output directly so @@ -28,6 +38,17 @@ 4. Raising modified AGI never raises either deduction, and leaves it unchanged while the count of whole $1,000 increments over the threshold is unchanged (Hypothesis, with incomes drawn around both thresholds and their steps). +5. Removing every dependent, with their tips and overtime, leaves both + deductions unchanged. +6. Zeroing the tips and overtime of each head or spouse without a valid SSN + leaves both deductions unchanged. +7. A spouse with no tips or overtime needs no valid SSN: switching that + spouse's card type between valid and invalid leaves both unchanged. +8. Giving every head and spouse a valid SSN never lowers either deduction. + +5-8 compare the model with itself on paired returns, so they do not depend on +the restatement in 1, and each checks that its draw contains returns where the +property could fail. Modified AGI here is adjusted gross income with no section 911, 931 or 933 exclusions; the YAML tests cover the section 911 and 931 add-backs. AGIs are @@ -35,6 +56,8 @@ magnitudes. """ +from collections import namedtuple + import numpy as np import pytest from hypothesis import Phase, given, settings @@ -60,6 +83,21 @@ INCREMENT = 1_000 REDUCTION = 100 OUTPUTS = ("tip_income_deduction", "overtime_income_deduction") +# Sections 224(e)(2) and 225(d)(2) adopt section 24(h)(7)(B): an SSN the +# Social Security Administration issued to a citizen, or to a noncitizen +# lawfully admitted to work, before the return's due date. The model's card +# types restate that as citizens and noncitizens with valid work authorization. +VALID_SSN = ("CITIZEN", "NON_CITIZEN_VALID_EAD") +SSN_CARD_TYPES = (*VALID_SSN, "OTHER_NON_CITIZEN", "NONE") + +# One tax unit: filing status, modified AGI, the head's and (joint returns +# only) the spouse's tips and overtime, the head's and spouse's SSN card +# types, and dependents as (age, SSN card type, tips, overtime). +Unit = namedtuple( + "Unit", + "status magi head_tips spouse_tips head_overtime spouse_overtime ssns dependents", + defaults=(("CITIZEN", "CITIZEN"), ()), +) @pytest.fixture(scope="module") @@ -77,6 +115,17 @@ def _caps(status): return TIP_CAP, OVERTIME_CAP.get(status, DEFAULT_OVERTIME_CAP) +def _counted(unit): + """Tips and overtime that count: the head's, and on a joint return the + spouse's, when that person has a valid SSN; never a dependent's.""" + filers = [(unit.ssns[0], unit.head_tips, unit.head_overtime)] + if unit.status == "JOINT": + filers.append((unit.ssns[1], unit.spouse_tips, unit.spouse_overtime)) + tips = sum(tips for ssn, tips, _ in filers if ssn in VALID_SSN) + overtime = sum(overtime for ssn, _, overtime in filers if ssn in VALID_SSN) + return tips, overtime + + def _schedule_1a(qualifying, cap, magi, status): """Lines 7-13 (tips) or 15-21 (overtime) of Schedule 1-A, restated.""" if status == "SEPARATE": @@ -90,32 +139,48 @@ def _schedule_1a(qualifying, cap, magi, status): def _simulate(reference_system, units): - """One tax unit per entry of (status, magi, head tips, spouse tips, head - overtime, spouse overtime); joint units get a spouse, others one person.""" + """One tax unit per Unit (or a tuple of its leading fields); joint units + get a spouse, others one filer, and each dependent is added as a member.""" period = str(YEAR) + units = [Unit(*unit) for unit in units] people, tax_units, marital_units, households = {}, {}, {}, {} - for i, (status, magi, tips, spouse_tips, overtime, spouse_overtime) in enumerate( - units - ): - members = [f"head{i}"] - incomes = [(tips, overtime)] - if status == "JOINT": - members.append(f"spouse{i}") - incomes.append((spouse_tips, spouse_overtime)) - for name, (person_tips, person_overtime) in zip(members, incomes): + for i, unit in enumerate(units): + # (name, age, SSN card type, tips, overtime, input as a dependent) + filers = [(f"head{i}", 40, unit.ssns[0], unit.head_tips, unit.head_overtime)] + if unit.status == "JOINT": + filers.append( + ( + f"spouse{i}", + 40, + unit.ssns[1], + unit.spouse_tips, + unit.spouse_overtime, + ) + ) + dependents = [ + (f"dependent{i}_{j}", *dependent) + for j, dependent in enumerate(unit.dependents) + ] + members = [person[0] for person in filers + dependents] + for name, age, ssn, tips, overtime, dependent in [ + (*filer, False) for filer in filers + ] + [(*dependent, True) for dependent in dependents]: people[name] = { - "age": {period: 40}, - "ssn_card_type": {period: "CITIZEN"}, + "age": {period: age}, + "is_tax_unit_dependent": {period: dependent}, + "ssn_card_type": {period: ssn}, "treasury_tipped_occupation_code": {period: 102}, - "tip_income": {period: float(person_tips)}, - "fsla_overtime_premium": {period: float(person_overtime)}, + "tip_income": {period: float(tips)}, + "fsla_overtime_premium": {period: float(overtime)}, } tax_units[f"t{i}"] = { "members": members, - "filing_status": {period: status}, - "adjusted_gross_income": {period: float(magi)}, + "filing_status": {period: unit.status}, + "adjusted_gross_income": {period: float(unit.magi)}, } - marital_units[f"m{i}"] = {"members": members} + marital_units[f"m{i}"] = {"members": [filer[0] for filer in filers]} + for dependent in dependents: + marital_units[f"m_{dependent[0]}"] = {"members": [dependent[0]]} households[f"h{i}"] = {"members": members, "state_code": {period: "TX"}} simulation = Simulation( tax_benefit_system=reference_system, @@ -127,24 +192,34 @@ def _simulate(reference_system, units): }, ) magi = simulation.calculate("agi_plus_section_911_931_933_exclusions", period) - np.testing.assert_array_equal(magi, [unit[1] for unit in units]) + np.testing.assert_array_equal(magi, [unit.magi for unit in units]) + filer = simulation.calculate("is_tax_unit_head_or_spouse", period) + np.testing.assert_array_equal( + filer, + [not person["is_tax_unit_dependent"][period] for person in people.values()], + ) return {name: simulation.calculate(name, period) for name in OUTPUTS} def _expected(units): tips, overtime = [], [] - for status, magi, head_tips, spouse_tips, head_ot, spouse_ot in units: - tip_cap, overtime_cap = _caps(status) - joint = status == "JOINT" - total_tips = head_tips + (spouse_tips if joint else 0) - total_ot = head_ot + (spouse_ot if joint else 0) - tips.append(_schedule_1a(total_tips, tip_cap, magi, status)) - overtime.append(_schedule_1a(total_ot, overtime_cap, magi, status)) + for unit in (Unit(*unit) for unit in units): + tip_cap, overtime_cap = _caps(unit.status) + counted_tips, counted_overtime = _counted(unit) + tips.append(_schedule_1a(counted_tips, tip_cap, unit.magi, unit.status)) + overtime.append( + _schedule_1a(counted_overtime, overtime_cap, unit.magi, unit.status) + ) return {"tip_income_deduction": tips, "overtime_income_deduction": overtime} +def _ssn(rng): + return str(rng.choice(SSN_CARD_TYPES, p=[0.55, 0.15, 0.15, 0.15])) + + def _draw(seed, n=150): - """Random returns concentrated around the $1,000 steps of each threshold.""" + """Random returns concentrated around the $1,000 steps of each threshold, + with random SSN card types and up to two dependents.""" rng = np.random.default_rng(seed) draws = [] for _ in range(n): @@ -162,7 +237,17 @@ def _draw(seed, n=150): int(rng.choice([0, 12_500, 25_000, int(rng.integers(0, 40_001))])) for _ in range(4) ] - draws.append((magi, *amounts)) + ssns = (_ssn(rng), _ssn(rng)) + dependents = tuple( + ( + int(rng.choice([10, 17, 22, 70])), + _ssn(rng), + int(rng.choice([0, int(rng.integers(0, 30_001))])), + int(rng.choice([0, int(rng.integers(0, 20_001))])), + ) + for _ in range(int(rng.integers(0, 3))) + ) + draws.append((magi, *amounts, ssns, dependents)) return draws @@ -184,17 +269,11 @@ def test_deductions_match_schedule_1a_under_every_filing_status(reference_system ("tip_income_deduction", 0), ("overtime_income_deduction", 1), ): - for unit, value in zip(units, results[name]): - status, _, head_tips, spouse_tips, head_ot, spouse_ot = unit - joint = status == "JOINT" - qualifying = ( - head_tips + (spouse_tips if joint else 0) - if cap_index == 0 - else head_ot + (spouse_ot if joint else 0) - ) - capped = min(qualifying, _caps(status)[cap_index]) + for unit, value in zip((Unit(*unit) for unit in units), results[name]): + qualifying = _counted(unit)[cap_index] + capped = min(qualifying, _caps(unit.status)[cap_index]) assert 0 <= value <= capped, (name, unit, value) - if status != "SEPARATE" and value > 0: + if unit.status != "SEPARATE" and value > 0: assert (capped - value) % REDUCTION == 0, (name, unit, value) @@ -298,3 +377,73 @@ def test_more_income_never_raises_either_deduction(reference_system, cases): same_step = np.array([before == after for before, after in steps]) # Piecewise constant: equal while the whole increments are equal. np.testing.assert_array_equal(higher[same_step], lower[same_step], name) + + +def _without_invalid_filers(unit): + head_valid, spouse_valid = (ssn in VALID_SSN for ssn in unit.ssns) + return unit._replace( + head_tips=unit.head_tips * head_valid, + head_overtime=unit.head_overtime * head_valid, + spouse_tips=unit.spouse_tips * spouse_valid, + spouse_overtime=unit.spouse_overtime * spouse_valid, + ) + + +def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): + rng = np.random.default_rng(2) + base = [Unit(str(rng.choice(CLAIMING)), *draw) for draw in _draw(seed=3, n=120)] + idle_spouse = [unit._replace(spouse_tips=0, spouse_overtime=0) for unit in base] + variants = { + "base": base, + "no dependents": [unit._replace(dependents=()) for unit in base], + "no invalid filers' amounts": list(map(_without_invalid_filers, base)), + "idle spouse, valid SSN": [ + unit._replace(ssns=(unit.ssns[0], "CITIZEN")) for unit in idle_spouse + ], + "idle spouse, no valid SSN": [ + unit._replace(ssns=(unit.ssns[0], "OTHER_NON_CITIZEN")) + for unit in idle_spouse + ], + "all valid SSNs": [unit._replace(ssns=("CITIZEN", "CITIZEN")) for unit in base], + } + results = _simulate( + reference_system, [unit for units in variants.values() for unit in units] + ) + n = len(base) + joint = np.array([unit.status == "JOINT" for unit in base]) + for name in OUTPUTS: + value = { + label: results[name][k * n : (k + 1) * n] + for k, label in enumerate(variants) + } + # 5. Dependents' tips and overtime add nothing. + np.testing.assert_array_equal(value["base"], value["no dependents"], name) + # 6. Nor do those of a head or spouse without a valid SSN. + np.testing.assert_array_equal( + value["base"], value["no invalid filers' amounts"], name + ) + # 7. A spouse with nothing to claim needs no valid SSN. + np.testing.assert_array_equal( + value["idle spouse, valid SSN"], value["idle spouse, no valid SSN"], name + ) + # 8. A valid SSN never lowers either deduction. + assert np.all(value["all valid SSNs"] >= value["base"]), name + + # Each property has returns in the draw where it could fail: a + # dependent or an invalid filer with an amount the deduction would + # count, and a joint return whose spouse lacks a valid SSN and whose + # head has something to claim. + output = 0 if name == "tip_income_deduction" else 1 + dependent_amounts = [ + any(dependent[2 + output] > 0 for dependent in unit.dependents) + for unit in base + ] + invalid_amounts = [ + _counted(unit._replace(ssns=("CITIZEN", "CITIZEN")))[output] + > _counted(unit)[output] + for unit in base + ] + assert sum(dependent_amounts) >= 20, name + assert sum(invalid_amounts) >= 20, name + assert (joint & (value["idle spouse, valid SSN"] > 0)).sum() >= 10, name + assert (value["all valid SSNs"] > value["base"]).sum() >= 10, name diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml index 221c1e40f97..0777cad7155 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml @@ -318,3 +318,157 @@ # 26 U.S.C. 7703(b): considered unmarried, so 225(e) does not apply. filing_status: [HEAD_OF_HOUSEHOLD, SEPARATE] overtime_income_deduction: [12_500, 0] + +- name: A joint return claims the overtime of a spouse with a valid SSN when the other spouse has none + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + fsla_overtime_premium: 5_000 + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + households: + household: + members: [head, spouse] + state_code: TX + output: + # 26 U.S.C. 225(d)(1): only the spouse who received the overtime needs an SSN. + filing_status: JOINT + overtime_income_deduction_ssn_requirement_met: true + overtime_income_deduction: 5_000 + +- name: Overtime received by a spouse without a valid SSN does not count + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + fsla_overtime_premium: 10_000 + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + households: + household: + members: [head, spouse] + state_code: TX + output: + # 26 U.S.C. 225(d)(1): the spouse who received the overtime needs the SSN. + filing_status: JOINT + overtime_income_deduction_ssn_requirement_met: true + overtime_income_deduction: 0 + +- name: On a joint return with overtime on both sides, only the spouse with a valid SSN counts + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + fsla_overtime_premium: 15_000 + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + fsla_overtime_premium: 20_000 + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + households: + household: + members: [head, spouse] + state_code: TX + output: + # The $25,000 joint cap applies to the $15,000 that counts, not to $35,000. + filing_status: JOINT + overtime_income_deduction_ssn_requirement_met: true + overtime_income_deduction: 15_000 + +- name: A dependent's overtime does not count toward the filer's deduction + period: 2025 + input: + people: + parent: + age: 45 + ssn_card_type: CITIZEN + employment_income: 60_000 + teen: + age: 17 + ssn_card_type: CITIZEN + employment_income: 8_000 + tip_income: 6_000 + treasury_tipped_occupation_code: 102 + fsla_overtime_premium: 1_000 + marital_units: + parent_marital_unit: + members: [parent] + teen_marital_unit: + members: [teen] + tax_units: + tax_unit: + members: [parent, teen] + households: + household: + members: [parent, teen] + state_code: TX + output: + # The teen's overtime and wages are the teen's own income (26 U.S.C. 73(a)), outside the parent's AGI. + filing_status: HEAD_OF_HOUSEHOLD + is_tax_unit_dependent: [false, true] + adjusted_gross_income: 60_000 + overtime_income_deduction: 0 + +- name: A filer's own overtime counts while a dependent's does not + period: 2025 + input: + people: + parent: + age: 45 + ssn_card_type: CITIZEN + employment_income: 60_000 + fsla_overtime_premium: 3_000 + teen: + age: 17 + ssn_card_type: CITIZEN + employment_income: 8_000 + tip_income: 6_000 + treasury_tipped_occupation_code: 102 + fsla_overtime_premium: 1_000 + marital_units: + parent_marital_unit: + members: [parent] + teen_marital_unit: + members: [teen] + tax_units: + tax_unit: + members: [parent, teen] + households: + household: + members: [parent, teen] + state_code: TX + output: + # Only the parent's $3,000 counts; the teen's $1,000 does not. + filing_status: HEAD_OF_HOUSEHOLD + is_tax_unit_dependent: [false, true] + adjusted_gross_income: 60_000 + overtime_income_deduction: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml new file mode 100644 index 00000000000..8a3dafc37ec --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml @@ -0,0 +1,89 @@ +- name: Head and spouse with valid SSNs count; a dependent and an ITIN spouse do not + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + spouse: + age: 38 + ssn_card_type: NON_CITIZEN_VALID_EAD + child: + age: 17 + ssn_card_type: CITIZEN + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + tax_units: + tax_unit: + members: [head, spouse, child] + output: + is_tax_unit_head_or_spouse: [true, true, false] + # A dependent's overtime is the dependent's own income (26 U.S.C. 73(a)). + overtime_income_deduction_eligible_person: [true, true, false] + +- name: A spouse without a valid SSN does not count, whatever the card type + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + output: + overtime_income_deduction_eligible_person: [true, false] + +- name: A head with no SSN card does not count + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: NONE + child: + age: 10 + ssn_card_type: CITIZEN + marital_units: + marital_unit: + members: [head] + child_marital_unit: + members: [child] + tax_units: + tax_unit: + members: [head, child] + output: + overtime_income_deduction_eligible_person: [false, false] + +- name: An adult input as a dependent does not count + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + is_tax_unit_dependent: false + parent: + age: 70 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + marital_units: + marital_unit: + members: [head] + parent_marital_unit: + members: [parent] + tax_units: + tax_unit: + members: [head, parent] + output: + is_tax_unit_head: [true, false] + overtime_income_deduction_eligible_person: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.yaml index 1304890ea87..2c4d178fcaa 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.yaml @@ -1,4 +1,4 @@ -- name: Ineligible cardholder present +- name: Joint return where only the spouse lacks a valid SSN meets the requirement period: 2025 input: people: @@ -13,6 +13,60 @@ tax_units: tax_unit: members: [person1, person2] + output: + # 26 U.S.C. 225(d)(1) needs the SSN of the spouse who received the overtime; + # see overtime_income_deduction_eligible_person for whose amounts count. + tax_unit_is_joint: true + overtime_income_deduction_ssn_requirement_met: true + +- name: Joint return where neither spouse has a valid SSN + period: 2025 + input: + people: + person1: + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: NONE + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + output: + tax_unit_is_joint: true + overtime_income_deduction_ssn_requirement_met: false + +- name: Single filer without a valid SSN + period: 2025 + input: + people: + person1: + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1] + output: + overtime_income_deduction_ssn_requirement_met: false + +- name: Only a dependent has a valid SSN + period: 2025 + input: + people: + person1: + ssn_card_type: NONE + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: CITIZEN + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1, person2] output: overtime_income_deduction_ssn_requirement_met: false diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml index 35655c72368..60feda59350 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml @@ -41,7 +41,7 @@ output: tip_income_deduction: 10_000 -- name: Ineligible cardholder present +- name: A joint return claims the tips of a spouse with a valid SSN when the other spouse has none period: 2025 input: people: @@ -50,6 +50,7 @@ is_tax_unit_head: true is_tax_unit_spouse: false tip_income: 1_000 + treasury_tipped_occupation_code: 102 person2: ssn_card_type: OTHER_NON_CITIZEN is_tax_unit_head: false @@ -58,8 +59,9 @@ tax_unit: members: [person1, person2] output: - tip_income_deduction_ssn_requirement_met: false - tip_income_deduction: 0 + # 26 U.S.C. 224(e)(1): only the spouse who received the tips needs an SSN. + tip_income_deduction_ssn_requirement_met: true + tip_income_deduction: 1_000 - name: Ineligible cardholder not present period: 2025 @@ -461,3 +463,133 @@ # 26 U.S.C. 7703(b): considered unmarried, so 224(f) does not apply. filing_status: [HEAD_OF_HOUSEHOLD, SEPARATE] tip_income_deduction: [25_000, 0] + +- name: Tips received by a spouse without a valid SSN do not count + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + tip_income: 10_000 + treasury_tipped_occupation_code: 102 + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + households: + household: + members: [head, spouse] + state_code: TX + output: + # 26 U.S.C. 224(e)(1): the spouse who received the tips needs the SSN. + filing_status: JOINT + tip_income_deduction_ssn_requirement_met: true + tip_income_deduction: 0 + +- name: On a joint return with tips on both sides, only the spouse with a valid SSN counts + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + tip_income: 15_000 + treasury_tipped_occupation_code: 102 + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + tip_income: 20_000 + treasury_tipped_occupation_code: 102 + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + households: + household: + members: [head, spouse] + state_code: TX + output: + # The $25,000 cap applies to the $15,000 that counts, not to $35,000. + filing_status: JOINT + tip_income_deduction_ssn_requirement_met: true + tip_income_deduction: 15_000 + +- name: A dependent's tips do not count toward the filer's deduction + period: 2025 + input: + people: + parent: + age: 45 + ssn_card_type: CITIZEN + employment_income: 60_000 + teen: + age: 17 + ssn_card_type: CITIZEN + employment_income: 8_000 + tip_income: 6_000 + treasury_tipped_occupation_code: 102 + fsla_overtime_premium: 1_000 + marital_units: + parent_marital_unit: + members: [parent] + teen_marital_unit: + members: [teen] + tax_units: + tax_unit: + members: [parent, teen] + households: + household: + members: [parent, teen] + state_code: TX + output: + # The teen's tips and wages are the teen's own income (26 U.S.C. 73(a)), outside the parent's AGI. + filing_status: HEAD_OF_HOUSEHOLD + is_tax_unit_dependent: [false, true] + adjusted_gross_income: 60_000 + tip_income_deduction: 0 + +- name: A filer's own tips count while a dependent's do not + period: 2025 + input: + people: + parent: + age: 45 + ssn_card_type: CITIZEN + employment_income: 60_000 + tip_income: 4_000 + treasury_tipped_occupation_code: 102 + teen: + age: 17 + ssn_card_type: CITIZEN + employment_income: 8_000 + tip_income: 6_000 + treasury_tipped_occupation_code: 102 + fsla_overtime_premium: 1_000 + marital_units: + parent_marital_unit: + members: [parent] + teen_marital_unit: + members: [teen] + tax_units: + tax_unit: + members: [parent, teen] + households: + household: + members: [parent, teen] + state_code: TX + output: + # Only the parent's $4,000 counts; the teen's $6,000 does not. + filing_status: HEAD_OF_HOUSEHOLD + is_tax_unit_dependent: [false, true] + adjusted_gross_income: 60_000 + tip_income_deduction: 4_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml new file mode 100644 index 00000000000..aab7f2cec26 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml @@ -0,0 +1,89 @@ +- name: Head and spouse with valid SSNs count; a dependent and an ITIN spouse do not + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + spouse: + age: 38 + ssn_card_type: NON_CITIZEN_VALID_EAD + child: + age: 17 + ssn_card_type: CITIZEN + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + tax_units: + tax_unit: + members: [head, spouse, child] + output: + is_tax_unit_head_or_spouse: [true, true, false] + # A dependent's tips are the dependent's own income (26 U.S.C. 73(a)). + tip_income_deduction_eligible_person: [true, true, false] + +- name: A spouse without a valid SSN does not count, whatever the card type + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + spouse: + age: 38 + ssn_card_type: OTHER_NON_CITIZEN + marital_units: + marital_unit: + members: [head, spouse] + tax_units: + tax_unit: + members: [head, spouse] + output: + tip_income_deduction_eligible_person: [true, false] + +- name: A head with no SSN card does not count + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: NONE + child: + age: 10 + ssn_card_type: CITIZEN + marital_units: + marital_unit: + members: [head] + child_marital_unit: + members: [child] + tax_units: + tax_unit: + members: [head, child] + output: + tip_income_deduction_eligible_person: [false, false] + +- name: An adult input as a dependent does not count + period: 2025 + input: + people: + head: + age: 40 + ssn_card_type: CITIZEN + is_tax_unit_dependent: false + parent: + age: 70 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + marital_units: + marital_unit: + members: [head] + parent_marital_unit: + members: [parent] + tax_units: + tax_unit: + members: [head, parent] + output: + is_tax_unit_head: [true, false] + tip_income_deduction_eligible_person: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ineligible_cardholder_present.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ineligible_cardholder_present.yaml deleted file mode 100644 index 85c71c8e197..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ineligible_cardholder_present.yaml +++ /dev/null @@ -1,53 +0,0 @@ -- name: Ineligible cardholder present - period: 2025 - input: - people: - person1: - ssn_card_type: CITIZEN - is_tax_unit_head: true - is_tax_unit_spouse: false - person2: - ssn_card_type: OTHER_NON_CITIZEN - is_tax_unit_head: false - is_tax_unit_spouse: true - tax_units: - tax_unit: - members: [person1, person2] - output: - tip_income_deduction_ssn_requirement_met: false - -- name: Ineligible cardholder present, but not head or spouse - period: 2025 - input: - people: - person1: - ssn_card_type: CITIZEN - is_tax_unit_head: true - is_tax_unit_spouse: false - person2: - ssn_card_type: OTHER_NON_CITIZEN - is_tax_unit_head: false - is_tax_unit_spouse: false - tax_units: - tax_unit: - members: [person1, person2] - output: - tip_income_deduction_ssn_requirement_met: true - -- name: Ineligible cardholder not present - period: 2025 - input: - people: - person1: - ssn_card_type: CITIZEN - is_tax_unit_head: true - is_tax_unit_spouse: false - person2: - ssn_card_type: NON_CITIZEN_VALID_EAD - is_tax_unit_head: true - is_tax_unit_spouse: false - tax_units: - tax_unit: - members: [person1, person2] - output: - tip_income_deduction_ssn_requirement_met: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.yaml new file mode 100644 index 00000000000..e5664c85b5e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.yaml @@ -0,0 +1,107 @@ +- name: Joint return where only the spouse lacks a valid SSN meets the requirement + period: 2025 + input: + people: + person1: + ssn_card_type: CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + output: + # 26 U.S.C. 224(e)(1) needs the SSN of the spouse who received the tips; + # see tip_income_deduction_eligible_person for whose amounts count. + tax_unit_is_joint: true + tip_income_deduction_ssn_requirement_met: true + +- name: Joint return where neither spouse has a valid SSN + period: 2025 + input: + people: + person1: + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: NONE + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [person1, person2] + output: + tax_unit_is_joint: true + tip_income_deduction_ssn_requirement_met: false + +- name: Single filer without a valid SSN + period: 2025 + input: + people: + person1: + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1] + output: + tip_income_deduction_ssn_requirement_met: false + +- name: Only a dependent has a valid SSN + period: 2025 + input: + people: + person1: + ssn_card_type: NONE + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: CITIZEN + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1, person2] + output: + tip_income_deduction_ssn_requirement_met: false + +- name: Ineligible cardholder present, but not head or spouse + period: 2025 + input: + people: + person1: + ssn_card_type: CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_head: false + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1, person2] + output: + tip_income_deduction_ssn_requirement_met: true + +- name: Ineligible cardholder not present + period: 2025 + input: + people: + person1: + ssn_card_type: CITIZEN + is_tax_unit_head: true + is_tax_unit_spouse: false + person2: + ssn_card_type: NON_CITIZEN_VALID_EAD + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + tax_unit: + members: [person1, person2] + output: + tip_income_deduction_ssn_requirement_met: true diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py index 8d8472b0159..ab7e017affc 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py @@ -28,7 +28,10 @@ def formula(tax_unit, period, parameters): # 1-A, line 19, drops any fraction of a $1,000 increment. increments = np.floor(magi_excess / p.phase_out.increment) phase_out_amount = increments * p.phase_out.increment * p.phase_out.rate - total_overtime_income = tax_unit.sum(overtime_income) + # 26 U.S.C. 225(d)(1): only overtime received by a head or spouse with + # a valid SSN counts. Dependents' overtime belongs on their own returns. + eligible_person = person("overtime_income_deduction_eligible_person", period) + total_overtime_income = tax_unit.sum(overtime_income * eligible_person) capped_overtime_income = min_(cap, total_overtime_income) deduction = max_(0, capped_overtime_income - phase_out_amount) # 26 U.S.C. 225(e): a married individual takes the deduction only on diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py new file mode 100644 index 00000000000..5ba3f1b03cc --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class overtime_income_deduction_eligible_person(Variable): + value_type = bool + entity = Person + label = "Person's overtime counts toward the overtime income deduction" + documentation = "Whether the person is the head or spouse of the tax unit and has a Social Security number valid for the overtime income deduction, so that qualified overtime compensation the person receives counts toward the unit's deduction." + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/225#d_1", + "https://www.irs.gov/pub/irs-pdf/i1040gi.pdf#page=106", + ) + + def formula(person, period, parameters): + ssn_card_type = person("ssn_card_type", period) + ssn_card_str = ssn_card_type.decode_to_str() + p = parameters(period).gov.irs.deductions.overtime_income + # 26 U.S.C. 225(d)(1) requires the SSN of the individual who received + # the overtime, so on a joint return a spouse without overtime needs + # none. + eligible_ssn_card_type = np.isin(ssn_card_str, p.eligible_ssn_card_type) + # A dependent's overtime is the dependent's own income (26 U.S.C. 73(a) + # for a child's services) and goes on the dependent's own return; + # irs_gross_income likewise leaves dependents' income out of the unit. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return eligible_ssn_card_type & head_or_spouse diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py index a2429af10cf..68c64dbd179 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py @@ -5,19 +5,11 @@ class overtime_income_deduction_ssn_requirement_met(Variable): value_type = bool entity = TaxUnit label = "SSN requirement met for the overtime income deduction" + documentation = "Whether the head or spouse has a Social Security number valid for the overtime income deduction. Only overtime received by such a head or spouse counts; see overtime_income_deduction_eligible_person." definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/225#d_1" def formula(tax_unit, period, parameters): person = tax_unit.members - ssn_card_type = person("ssn_card_type", period) - ssn_card_str = ssn_card_type.decode_to_str() - p = parameters(period).gov.irs.deductions.overtime_income - eligible_ssn_card_type = np.isin(ssn_card_str, p.eligible_ssn_card_type) - joint = tax_unit("tax_unit_is_joint", period) - head_or_spouse = person("is_tax_unit_head_or_spouse", period) - eligible_head_or_spouse = eligible_ssn_card_type & head_or_spouse - return where( - joint, - tax_unit.sum(eligible_head_or_spouse) == 2, - tax_unit.any(eligible_head_or_spouse), - ) + eligible = person("overtime_income_deduction_eligible_person", period) + return tax_unit.any(eligible) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py index cd0a786413d..7e8a168add5 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py @@ -31,7 +31,10 @@ def formula(tax_unit, period, parameters): increments = np.floor(magi_excess / p.phase_out.increment) phase_out_amount = increments * p.phase_out.increment * p.phase_out.rate qualified_tip_income = tip_income * occupation_requirement_met - total_tip_income = tax_unit.sum(qualified_tip_income) + # 26 U.S.C. 224(e)(1): only tips received by a head or spouse with a + # valid SSN count. Dependents' tips belong on their own returns. + eligible_person = person("tip_income_deduction_eligible_person", period) + total_tip_income = tax_unit.sum(qualified_tip_income * eligible_person) capped_tip_income = min_(p.cap, total_tip_income) deduction = max_(0, capped_tip_income - phase_out_amount) # 26 U.S.C. 224(f): a married individual takes the deduction only on diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py new file mode 100644 index 00000000000..063292901bd --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class tip_income_deduction_eligible_person(Variable): + value_type = bool + entity = Person + label = "Person's tips count toward the tip income deduction" + documentation = "Whether the person is the head or spouse of the tax unit and has a Social Security number valid for the tip income deduction, so that qualified tips the person receives count toward the unit's deduction." + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/224#e_1", + "https://www.irs.gov/pub/irs-pdf/i1040gi.pdf#page=101", + ) + + def formula(person, period, parameters): + ssn_card_type = person("ssn_card_type", period) + ssn_card_str = ssn_card_type.decode_to_str() + p = parameters(period).gov.irs.deductions.tip_income + # 26 U.S.C. 224(e)(1) requires the SSN of the individual who received + # the tips, so on a joint return a spouse without tips needs none. + eligible_ssn_card_type = np.isin(ssn_card_str, p.eligible_ssn_card_type) + # A dependent's tips are the dependent's own income (26 U.S.C. 73(a) + # for a child's services) and go on the dependent's own return; + # irs_gross_income likewise leaves dependents' income out of the unit. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return eligible_ssn_card_type & head_or_spouse diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py index 6e06600c6cf..817620a306e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py @@ -5,19 +5,11 @@ class tip_income_deduction_ssn_requirement_met(Variable): value_type = bool entity = TaxUnit label = "SSN requirement met for the tip income deduction" + documentation = "Whether the head or spouse has a Social Security number valid for the tip income deduction. Only tips received by such a head or spouse count; see tip_income_deduction_eligible_person." definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/224#e_1" def formula(tax_unit, period, parameters): person = tax_unit.members - ssn_card_type = person("ssn_card_type", period) - ssn_card_str = ssn_card_type.decode_to_str() - p = parameters(period).gov.irs.deductions.tip_income - eligible_ssn_card_type = np.isin(ssn_card_str, p.eligible_ssn_card_type) - joint = tax_unit("tax_unit_is_joint", period) - head_or_spouse = person("is_tax_unit_head_or_spouse", period) - eligible_head_or_spouse = eligible_ssn_card_type & head_or_spouse - return where( - joint, - tax_unit.sum(eligible_head_or_spouse) == 2, - tax_unit.any(eligible_head_or_spouse), - ) + eligible = person("tip_income_deduction_eligible_person", period) + return tax_unit.any(eligible) From 4e96212edc8112c9603429423b763e0fff7717da Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 22:35:42 -0400 Subject: [PATCH 2/4] Count filers by dependent status, and update tests that pinned the both-spouses rule Round-1 review: is_tax_unit_head_or_spouse can differ from ~is_tax_unit_dependent when dependents are input. When no adult filer is left, the role inference keeps input dependents as head candidates, so an input dependent counted, and a filer under 18 with an adult dependent did not. The eligibility variables now use ~is_tax_unit_dependent, the mask irs_gross_income uses. Four tip_and_overtime_income_deduction.yaml cases still asserted that a joint return with one spouse lacking a valid SSN gets nothing; they now expect the eligible spouse's amounts. New YAML cases cover the two input-dependent households, and the property draws include 17-year-old filers with adult dependents. Co-Authored-By: Claude Opus 5.5 --- .../fix-tips-overtime-dependents-ssn.fixed.md | 2 +- .../eligible_ssn_card_type.yaml | 2 +- .../tip_income/eligible_ssn_card_type.yaml | 2 +- ...t_tip_and_overtime_deduction_properties.py | 45 ++++++++++----- .../overtime_income_deduction.yaml | 54 ++++++++++++++++++ ...time_income_deduction_eligible_person.yaml | 42 ++++++++++++++ .../tip_and_overtime_income_deduction.yaml | 34 +++++------ .../tip_income/tip_income_deduction.yaml | 57 +++++++++++++++++++ .../tip_income_deduction_eligible_person.yaml | 42 ++++++++++++++ .../overtime_income_deduction.py | 4 +- ...ertime_income_deduction_eligible_person.py | 12 ++-- ...me_income_deduction_ssn_requirement_met.py | 2 +- .../tip_income/tip_income_deduction.py | 4 +- .../tip_income_deduction_eligible_person.py | 12 ++-- ...ip_income_deduction_ssn_requirement_met.py | 2 +- 15 files changed, 267 insertions(+), 49 deletions(-) diff --git a/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md b/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md index 53d12bc7132..116a0ad64e7 100644 --- a/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md +++ b/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md @@ -1 +1 @@ -Count only tips and overtime received by a head or spouse with a valid SSN toward the qualified tips and overtime deductions (26 U.S.C. 224(e)(1) and 225(d)(1)). A dependent's tips and overtime no longer raise the filer's deductions, and a joint return on which one spouse lacks a valid SSN now deducts the other spouse's tips and overtime instead of nothing. +Count only tips and overtime received by a filer (a tax unit member who is not a dependent) with a valid SSN toward the qualified tips and overtime deductions (26 U.S.C. 224(e)(1) and 225(d)(1)). A dependent's tips and overtime no longer raise the filer's deductions, and a joint return on which one spouse lacks a valid SSN now deducts the other spouse's tips and overtime instead of nothing. diff --git a/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml b/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml index 5e72d80e994..38280ee1292 100644 --- a/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/overtime_income/eligible_ssn_card_type.yaml @@ -1,4 +1,4 @@ -description: The IRS counts overtime toward the overtime income deduction only when the head or spouse who received it has one of these SSN card types. +description: The IRS counts overtime toward the overtime income deduction only when the filer (not a dependent) who received it has one of these SSN card types. values: 2018-01-01: - CITIZEN diff --git a/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml b/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml index fdf1946f5ba..2fb419d0697 100644 --- a/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/tip_income/eligible_ssn_card_type.yaml @@ -1,4 +1,4 @@ -description: The IRS counts tips toward the tip income deduction only when the head or spouse who received them has one of these SSN card types. +description: The IRS counts tips toward the tip income deduction only when the filer (not a dependent) who received them has one of these SSN card types. values: 2018-01-01: - CITIZEN diff --git a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py index 579c3965c99..34bac3f5b16 100644 --- a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py +++ b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py @@ -19,8 +19,12 @@ Each test call, and each Hypothesis example, builds one simulation of many tax units. Joint returns have two spouses, so the combined caps apply as on the -return. Seeded draws also vary each head's and spouse's SSN card type and add -dependents with tips and overtime of their own. The tests check: +return. Seeded draws also vary each head's and spouse's SSN card type, make +some heads 17, and add dependents (children and adults input as dependents) +with tips and overtime of their own. A 17-year-old filer with only adult +dependents is the case where the model's head-or-spouse inference picks an +input dependent as head; the dependent still counts for nothing. The tests +check: 1. Every seeded draw, evaluated under all five filing statuses, matches an independent restatement of Schedule 1-A lines 7-13 (tips) and 15-21 @@ -92,11 +96,14 @@ # One tax unit: filing status, modified AGI, the head's and (joint returns # only) the spouse's tips and overtime, the head's and spouse's SSN card -# types, and dependents as (age, SSN card type, tips, overtime). +# types, dependents as (age, SSN card type, tips, overtime), and the head's +# and spouse's ages. Every head and spouse is input as a filer (not a +# dependent) and every dependent as a dependent. Unit = namedtuple( "Unit", - "status magi head_tips spouse_tips head_overtime spouse_overtime ssns dependents", - defaults=(("CITIZEN", "CITIZEN"), ()), + "status magi head_tips spouse_tips head_overtime spouse_overtime ssns " + "dependents ages", + defaults=(("CITIZEN", "CITIZEN"), (), (40, 40)), ) @@ -117,7 +124,8 @@ def _caps(status): def _counted(unit): """Tips and overtime that count: the head's, and on a joint return the - spouse's, when that person has a valid SSN; never a dependent's.""" + spouse's, when that person has a valid SSN; never a dependent's, whatever + role the model infers for them.""" filers = [(unit.ssns[0], unit.head_tips, unit.head_overtime)] if unit.status == "JOINT": filers.append((unit.ssns[1], unit.spouse_tips, unit.spouse_overtime)) @@ -146,12 +154,14 @@ def _simulate(reference_system, units): people, tax_units, marital_units, households = {}, {}, {}, {} for i, unit in enumerate(units): # (name, age, SSN card type, tips, overtime, input as a dependent) - filers = [(f"head{i}", 40, unit.ssns[0], unit.head_tips, unit.head_overtime)] + filers = [ + (f"head{i}", unit.ages[0], unit.ssns[0], unit.head_tips, unit.head_overtime) + ] if unit.status == "JOINT": filers.append( ( f"spouse{i}", - 40, + unit.ages[1], unit.ssns[1], unit.spouse_tips, unit.spouse_overtime, @@ -193,11 +203,6 @@ def _simulate(reference_system, units): ) magi = simulation.calculate("agi_plus_section_911_931_933_exclusions", period) np.testing.assert_array_equal(magi, [unit.magi for unit in units]) - filer = simulation.calculate("is_tax_unit_head_or_spouse", period) - np.testing.assert_array_equal( - filer, - [not person["is_tax_unit_dependent"][period] for person in people.values()], - ) return {name: simulation.calculate(name, period) for name in OUTPUTS} @@ -247,7 +252,8 @@ def _draw(seed, n=150): ) for _ in range(int(rng.integers(0, 3))) ) - draws.append((magi, *amounts, ssns, dependents)) + ages = (int(rng.choice([40, 17], p=[0.8, 0.2])), 40) + draws.append((magi, *amounts, ssns, dependents, ages)) return draws @@ -444,6 +450,17 @@ def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): for unit in base ] assert sum(dependent_amounts) >= 20, name + # A filer under 18 whose adult dependents have amounts: the model + # infers one of those dependents as head. + minor_filer_with_adult_dependents = [ + unit.ages[0] < 18 + and any( + dependent[0] >= 18 and dependent[2 + output] > 0 + for dependent in unit.dependents + ) + for unit in base + ] + assert sum(minor_filer_with_adult_dependents) >= 3, name assert sum(invalid_amounts) >= 20, name assert (joint & (value["idle spouse, valid SSN"] > 0)).sum() >= 10, name assert (value["all valid SSNs"] > value["base"]).sum() >= 10, name diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml index 0777cad7155..1fb261b4b9a 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.yaml @@ -472,3 +472,57 @@ is_tax_unit_dependent: [false, true] adjusted_gross_income: 60_000 overtime_income_deduction: 3_000 + +- name: An adult input as a dependent deducts nothing even as the only adult in the unit + period: 2025 + input: + people: + adult: + age: 40 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + fsla_overtime_premium: 1_000 + tax_units: + tax_unit: + members: [adult] + filing_status: SINGLE + adjusted_gross_income: 80_000 + households: + household: + members: [adult] + state_code: TX + output: + # A dependent's overtime stays off the unit's return, like the rest of the + # dependent's income (irs_gross_income). + overtime_income_deduction: 0 + +- name: A filer under 18 deducts their own overtime, not an adult dependent's + period: 2025 + input: + people: + filer: + age: 17 + ssn_card_type: CITIZEN + is_tax_unit_dependent: false + fsla_overtime_premium: 3_000 + parent: + age: 45 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + fsla_overtime_premium: 500 + marital_units: + filer_marital_unit: + members: [filer] + parent_marital_unit: + members: [parent] + tax_units: + tax_unit: + members: [filer, parent] + filing_status: HEAD_OF_HOUSEHOLD + adjusted_gross_income: 80_000 + households: + household: + members: [filer, parent] + state_code: TX + output: + overtime_income_deduction: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml index 8a3dafc37ec..e6f949e3fcf 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.yaml @@ -87,3 +87,45 @@ output: is_tax_unit_head: [true, false] overtime_income_deduction_eligible_person: [true, false] + +- name: An adult input as a dependent does not count even when no other adult is a filer + period: 2025 + input: + people: + adult: + age: 40 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult] + output: + # The head-or-spouse inference keeps an input dependent as head when no + # adult filer is left, but the person's income is still left out of the + # unit (irs_gross_income). + is_tax_unit_head: true + overtime_income_deduction_eligible_person: false + +- name: A filer under 18 counts and an adult input as a dependent does not + period: 2025 + input: + people: + filer: + age: 17 + ssn_card_type: CITIZEN + is_tax_unit_dependent: false + parent: + age: 45 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + marital_units: + filer_marital_unit: + members: [filer] + parent_marital_unit: + members: [parent] + tax_units: + tax_unit: + members: [filer, parent] + output: + is_tax_unit_head: [false, true] + overtime_income_deduction_eligible_person: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_and_overtime_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_and_overtime_income_deduction.yaml index e2ecfc8e662..e9fef1e79cd 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_and_overtime_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_and_overtime_income_deduction.yaml @@ -16,7 +16,7 @@ taxable_income_deductions_if_itemizing: 500 taxable_income_deductions_if_not_itemizing: 500 -- name: Test for SSN card type - head eligible - no deduction allowed +- name: Test for SSN card type - only the head has a valid SSN, so only the head's amounts count period: 2026 input: people: @@ -42,12 +42,14 @@ casualty_loss_deduction: 0 misc_deduction: 0 output: - tip_income_deduction: 0 - overtime_income_deduction: 0 - taxable_income_deductions_if_itemizing: 0 - taxable_income_deductions_if_not_itemizing: 0 + # 26 U.S.C. 224(e)(1) and 225(d)(1): the spouse who received the amounts + # needs the SSN. + tip_income_deduction: 200 + overtime_income_deduction: 300 + taxable_income_deductions_if_itemizing: 500 + taxable_income_deductions_if_not_itemizing: 500 -- name: Test for SSN card type - spouse eligible - no deduction allowed +- name: Test for SSN card type - only the spouse has a valid SSN, so only the spouse's amounts count period: 2026 input: people: @@ -73,10 +75,10 @@ casualty_loss_deduction: 0 misc_deduction: 0 output: - tip_income_deduction: 0 - overtime_income_deduction: 0 - taxable_income_deductions_if_itemizing: 0 - taxable_income_deductions_if_not_itemizing: 0 + tip_income_deduction: 600 + overtime_income_deduction: 900 + taxable_income_deductions_if_itemizing: 1_500 + taxable_income_deductions_if_not_itemizing: 1_500 - name: Test for SSN card type - both eligible period: 2026 @@ -136,7 +138,7 @@ taxable_income_deductions_if_itemizing: 1_200 taxable_income_deductions_if_not_itemizing: 1_200 -- name: Phase out structure applies, joint filing status, agi below phase out start, head not eligible +- name: Phase out structure applies, joint filing status, agi below phase out start, only the spouse's overtime counts period: 2026 input: people: @@ -160,8 +162,8 @@ casualty_loss_deduction: 0 misc_deduction: 0 output: - taxable_income_deductions_if_itemizing: 0 - taxable_income_deductions_if_not_itemizing: 0 + taxable_income_deductions_if_itemizing: 900 + taxable_income_deductions_if_not_itemizing: 900 - name: Phase out structure applies, joint filing status, agi below phase out start, child without ssn card period: 2026 @@ -276,7 +278,7 @@ taxable_income_deductions_if_itemizing: 1_200 taxable_income_deductions_if_not_itemizing: 1_200 -- name: Tip income phase out structure applies, joint filing status, agi below phase out start, head not eligible +- name: Tip income phase out structure applies, joint filing status, agi below phase out start, only the spouse's tips count period: 2026 input: people: @@ -302,8 +304,8 @@ casualty_loss_deduction: 0 misc_deduction: 0 output: - taxable_income_deductions_if_itemizing: 0 - taxable_income_deductions_if_not_itemizing: 0 + taxable_income_deductions_if_itemizing: 900 + taxable_income_deductions_if_not_itemizing: 900 - name: Tip income phase out structure applies, joint filing status, agi below phase out start, child without ssn card period: 2026 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml index 60feda59350..ee772b95d6e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.yaml @@ -593,3 +593,60 @@ is_tax_unit_dependent: [false, true] adjusted_gross_income: 60_000 tip_income_deduction: 4_000 + +- name: An adult input as a dependent deducts nothing even as the only adult in the unit + period: 2025 + input: + people: + adult: + age: 40 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + tip_income: 6_000 + treasury_tipped_occupation_code: 102 + tax_units: + tax_unit: + members: [adult] + filing_status: SINGLE + adjusted_gross_income: 80_000 + households: + household: + members: [adult] + state_code: TX + output: + # A dependent's tips stay off the unit's return, like the rest of the + # dependent's income (irs_gross_income). + tip_income_deduction: 0 + +- name: A filer under 18 deducts their own tips, not an adult dependent's + period: 2025 + input: + people: + filer: + age: 17 + ssn_card_type: CITIZEN + is_tax_unit_dependent: false + tip_income: 4_000 + treasury_tipped_occupation_code: 102 + parent: + age: 45 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + tip_income: 1_000 + treasury_tipped_occupation_code: 102 + marital_units: + filer_marital_unit: + members: [filer] + parent_marital_unit: + members: [parent] + tax_units: + tax_unit: + members: [filer, parent] + filing_status: HEAD_OF_HOUSEHOLD + adjusted_gross_income: 80_000 + households: + household: + members: [filer, parent] + state_code: TX + output: + tip_income_deduction: 4_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml index aab7f2cec26..838f445e907 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.yaml @@ -87,3 +87,45 @@ output: is_tax_unit_head: [true, false] tip_income_deduction_eligible_person: [true, false] + +- name: An adult input as a dependent does not count even when no other adult is a filer + period: 2025 + input: + people: + adult: + age: 40 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [adult] + output: + # The head-or-spouse inference keeps an input dependent as head when no + # adult filer is left, but the person's income is still left out of the + # unit (irs_gross_income). + is_tax_unit_head: true + tip_income_deduction_eligible_person: false + +- name: A filer under 18 counts and an adult input as a dependent does not + period: 2025 + input: + people: + filer: + age: 17 + ssn_card_type: CITIZEN + is_tax_unit_dependent: false + parent: + age: 45 + ssn_card_type: CITIZEN + is_tax_unit_dependent: true + marital_units: + filer_marital_unit: + members: [filer] + parent_marital_unit: + members: [parent] + tax_units: + tax_unit: + members: [filer, parent] + output: + is_tax_unit_head: [false, true] + tip_income_deduction_eligible_person: [true, false] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py index ab7e017affc..e998f477de4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction.py @@ -28,8 +28,8 @@ def formula(tax_unit, period, parameters): # 1-A, line 19, drops any fraction of a $1,000 increment. increments = np.floor(magi_excess / p.phase_out.increment) phase_out_amount = increments * p.phase_out.increment * p.phase_out.rate - # 26 U.S.C. 225(d)(1): only overtime received by a head or spouse with - # a valid SSN counts. Dependents' overtime belongs on their own returns. + # 26 U.S.C. 225(d)(1): only overtime received by a filer with a valid + # SSN counts. Dependents' overtime belongs on their own returns. eligible_person = person("overtime_income_deduction_eligible_person", period) total_overtime_income = tax_unit.sum(overtime_income * eligible_person) capped_overtime_income = min_(cap, total_overtime_income) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py index 5ba3f1b03cc..f6a82cea916 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py @@ -5,7 +5,7 @@ class overtime_income_deduction_eligible_person(Variable): value_type = bool entity = Person label = "Person's overtime counts toward the overtime income deduction" - documentation = "Whether the person is the head or spouse of the tax unit and has a Social Security number valid for the overtime income deduction, so that qualified overtime compensation the person receives counts toward the unit's deduction." + documentation = "Whether the person is a filer (a tax unit member who is not a dependent, so the head or spouse) and has a Social Security number valid for the overtime income deduction, so that qualified overtime compensation the person receives counts toward the unit's deduction." definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/225#d_1", @@ -21,7 +21,9 @@ def formula(person, period, parameters): # none. eligible_ssn_card_type = np.isin(ssn_card_str, p.eligible_ssn_card_type) # A dependent's overtime is the dependent's own income (26 U.S.C. 73(a) - # for a child's services) and goes on the dependent's own return; - # irs_gross_income likewise leaves dependents' income out of the unit. - head_or_spouse = person("is_tax_unit_head_or_spouse", period) - return eligible_ssn_card_type & head_or_spouse + # for a child's services) and goes on the dependent's own return. This + # matches irs_gross_income, which leaves dependents' income out of the + # unit. An input dependent never counts, even where the head-or-spouse + # inference has to pick one because no other adult is a filer. + filer = ~person("is_tax_unit_dependent", period) + return eligible_ssn_card_type & filer diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py index 68c64dbd179..57fb049aac8 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_ssn_requirement_met.py @@ -5,7 +5,7 @@ class overtime_income_deduction_ssn_requirement_met(Variable): value_type = bool entity = TaxUnit label = "SSN requirement met for the overtime income deduction" - documentation = "Whether the head or spouse has a Social Security number valid for the overtime income deduction. Only overtime received by such a head or spouse counts; see overtime_income_deduction_eligible_person." + documentation = "Whether a filer (a tax unit member who is not a dependent) has a Social Security number valid for the overtime income deduction. Only overtime received by such a filer counts; see overtime_income_deduction_eligible_person." definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/225#d_1" diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py index 7e8a168add5..7e61bb1a85a 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction.py @@ -31,8 +31,8 @@ def formula(tax_unit, period, parameters): increments = np.floor(magi_excess / p.phase_out.increment) phase_out_amount = increments * p.phase_out.increment * p.phase_out.rate qualified_tip_income = tip_income * occupation_requirement_met - # 26 U.S.C. 224(e)(1): only tips received by a head or spouse with a - # valid SSN count. Dependents' tips belong on their own returns. + # 26 U.S.C. 224(e)(1): only tips received by a filer with a valid SSN + # count. Dependents' tips belong on their own returns. eligible_person = person("tip_income_deduction_eligible_person", period) total_tip_income = tax_unit.sum(qualified_tip_income * eligible_person) capped_tip_income = min_(p.cap, total_tip_income) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py index 063292901bd..8051250d534 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py @@ -5,7 +5,7 @@ class tip_income_deduction_eligible_person(Variable): value_type = bool entity = Person label = "Person's tips count toward the tip income deduction" - documentation = "Whether the person is the head or spouse of the tax unit and has a Social Security number valid for the tip income deduction, so that qualified tips the person receives count toward the unit's deduction." + documentation = "Whether the person is a filer (a tax unit member who is not a dependent, so the head or spouse) and has a Social Security number valid for the tip income deduction, so that qualified tips the person receives count toward the unit's deduction." definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/224#e_1", @@ -20,7 +20,9 @@ def formula(person, period, parameters): # the tips, so on a joint return a spouse without tips needs none. eligible_ssn_card_type = np.isin(ssn_card_str, p.eligible_ssn_card_type) # A dependent's tips are the dependent's own income (26 U.S.C. 73(a) - # for a child's services) and go on the dependent's own return; - # irs_gross_income likewise leaves dependents' income out of the unit. - head_or_spouse = person("is_tax_unit_head_or_spouse", period) - return eligible_ssn_card_type & head_or_spouse + # for a child's services) and go on the dependent's own return. This + # matches irs_gross_income, which leaves dependents' income out of the + # unit. An input dependent never counts, even where the head-or-spouse + # inference has to pick one because no other adult is a filer. + filer = ~person("is_tax_unit_dependent", period) + return eligible_ssn_card_type & filer diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py index 817620a306e..57dd9afc4b0 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_ssn_requirement_met.py @@ -5,7 +5,7 @@ class tip_income_deduction_ssn_requirement_met(Variable): value_type = bool entity = TaxUnit label = "SSN requirement met for the tip income deduction" - documentation = "Whether the head or spouse has a Social Security number valid for the tip income deduction. Only tips received by such a head or spouse count; see tip_income_deduction_eligible_person." + documentation = "Whether a filer (a tax unit member who is not a dependent) has a Social Security number valid for the tip income deduction. Only tips received by such a filer count; see tip_income_deduction_eligible_person." definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/224#e_1" From dcf7e5b9064d5a2d00f5855488b972072467beef Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 22:54:00 -0400 Subject: [PATCH 3/4] Make the paired-return guards observable, and fix the eligibility docs Round-2 review: the paired-return test passed with the role-based mask restored, because its guard counted returns where the mask makes no difference (joint returns, or deductions already phased out). The test now adds returns below both thresholds and caps, plus two minor-filer households whose only adult is an input dependent. Each guard counts only returns where breaking its property would change the deduction. The eligibility documentation no longer equates a filer with the head or spouse, which a filer under 18 may not be. Co-Authored-By: Claude Opus 5.5 --- ...t_tip_and_overtime_deduction_properties.py | 105 +++++++++++++++--- ...ertime_income_deduction_eligible_person.py | 2 +- .../tip_income_deduction_eligible_person.py | 2 +- 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py index 34bac3f5b16..787c186b302 100644 --- a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py +++ b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py @@ -51,8 +51,9 @@ 8. Giving every head and spouse a valid SSN never lowers either deduction. 5-8 compare the model with itself on paired returns, so they do not depend on -the restatement in 1, and each checks that its draw contains returns where the -property could fail. +the restatement in 1. Their returns come partly from the draws above and partly +from returns below both thresholds and caps, and each checks that its draw +contains returns where breaking the property would change the deduction. Modified AGI here is adjusted gross income with no section 911, 931 or 933 exclusions; the YAML tests cover the section 911 and 931 add-backs. AGIs are @@ -395,9 +396,64 @@ def _without_invalid_filers(unit): ) +def _observable(unit, output, extra): + """Whether counting `extra` more of output 0 (tips) or 1 (overtime) would + change the deduction: modified AGI at or below the threshold, so nothing + phases out, and room under the cap.""" + cap = _caps(unit.status)[output] + counted = _counted(unit)[output] + return ( + unit.status != "SEPARATE" + and unit.magi <= _threshold(unit.status) + and min(counted + extra, cap) > min(counted, cap) + ) + + +def _low_income_draw(seed, n): + """Returns below both thresholds with amounts under the caps, so each + paired comparison below can show a difference.""" + rng = np.random.default_rng(seed) + + def amount(): + return int(rng.choice([0, int(rng.integers(1, 6_001))])) + + units = [] + for _ in range(n): + status = str(rng.choice(CLAIMING)) + magi = float(rng.integers(20_000, DEFAULT_THRESHOLD - 10_000)) + amounts = [amount() for _ in range(4)] + ssns = (_ssn(rng), _ssn(rng)) + dependents = tuple( + (int(rng.choice([10, 17, 22, 70])), _ssn(rng), amount(), amount()) + for _ in range(int(rng.integers(0, 3))) + ) + ages = (int(rng.choice([40, 17], p=[0.8, 0.2])), 40) + units.append(Unit(status, magi, *amounts, ssns, dependents, ages)) + return units + + def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): rng = np.random.default_rng(2) - base = [Unit(str(rng.choice(CLAIMING)), *draw) for draw in _draw(seed=3, n=120)] + # Returns around the thresholds and caps, and returns well below them. + base = [Unit(str(rng.choice(CLAIMING)), *draw) for draw in _draw(seed=3, n=60)] + base += _low_income_draw(seed=4, n=100) + # Filers under 18 whose only adult is an input dependent, at low income: the + # model infers that dependent as head, so a role-based mask would count the + # dependent's amounts and drop the filer's (round-1 review). + base += [ + Unit( + status, + 60_000, + 4_000, + 0, + 3_000, + 0, + ("CITIZEN", "CITIZEN"), + ((45, "CITIZEN", 1_000, 500), (10, "CITIZEN", 0, 0)), + (17, 40), + ) + for status in ("SINGLE", "HEAD_OF_HOUSEHOLD") + ] idle_spouse = [unit._replace(spouse_tips=0, spouse_overtime=0) for unit in base] variants = { "base": base, @@ -435,32 +491,45 @@ def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): # 8. A valid SSN never lowers either deduction. assert np.all(value["all valid SSNs"] >= value["base"]), name - # Each property has returns in the draw where it could fail: a - # dependent or an invalid filer with an amount the deduction would - # count, and a joint return whose spouse lacks a valid SSN and whose - # head has something to claim. + # Each property has returns in the draw where breaking it would change + # the deduction: dependents' or invalid filers' amounts that would + # move the capped, unphased amount; a filer under 18 whose adult + # dependent has an amount; a joint return whose spouse lacks a valid + # SSN and whose head has something to claim. output = 0 if name == "tip_income_deduction" else 1 dependent_amounts = [ - any(dependent[2 + output] > 0 for dependent in unit.dependents) + _observable( + unit, + output, + sum(dependent[2 + output] for dependent in unit.dependents), + ) for unit in base ] invalid_amounts = [ - _counted(unit._replace(ssns=("CITIZEN", "CITIZEN")))[output] - > _counted(unit)[output] + _observable( + unit, + output, + _counted(unit._replace(ssns=("CITIZEN", "CITIZEN")))[output] + - _counted(unit)[output], + ) for unit in base ] - assert sum(dependent_amounts) >= 20, name - # A filer under 18 whose adult dependents have amounts: the model - # infers one of those dependents as head. minor_filer_with_adult_dependents = [ - unit.ages[0] < 18 - and any( - dependent[0] >= 18 and dependent[2 + output] > 0 - for dependent in unit.dependents + unit.status != "JOINT" + and unit.ages[0] < 18 + and _observable( + unit, + output, + sum( + dependent[2 + output] + for dependent in unit.dependents + if dependent[0] >= 18 + ), ) for unit in base ] + assert sum(dependent_amounts) >= 30, name + assert sum(invalid_amounts) >= 15, name assert sum(minor_filer_with_adult_dependents) >= 3, name - assert sum(invalid_amounts) >= 20, name assert (joint & (value["idle spouse, valid SSN"] > 0)).sum() >= 10, name assert (value["all valid SSNs"] > value["base"]).sum() >= 10, name diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py index f6a82cea916..8694b0d171b 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py @@ -5,7 +5,7 @@ class overtime_income_deduction_eligible_person(Variable): value_type = bool entity = Person label = "Person's overtime counts toward the overtime income deduction" - documentation = "Whether the person is a filer (a tax unit member who is not a dependent, so the head or spouse) and has a Social Security number valid for the overtime income deduction, so that qualified overtime compensation the person receives counts toward the unit's deduction." + documentation = "Whether the person is a filer (a tax unit member who is not a dependent) and has a Social Security number valid for the overtime income deduction, so that qualified overtime compensation the person receives counts toward the unit's deduction." definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/225#d_1", diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py index 8051250d534..93ad91bc19c 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py @@ -5,7 +5,7 @@ class tip_income_deduction_eligible_person(Variable): value_type = bool entity = Person label = "Person's tips count toward the tip income deduction" - documentation = "Whether the person is a filer (a tax unit member who is not a dependent, so the head or spouse) and has a Social Security number valid for the tip income deduction, so that qualified tips the person receives count toward the unit's deduction." + documentation = "Whether the person is a filer (a tax unit member who is not a dependent) and has a Social Security number valid for the tip income deduction, so that qualified tips the person receives count toward the unit's deduction." definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/224#e_1", From bbf9b859e117a620b643d856c8a322ba9694775a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 23:25:43 -0400 Subject: [PATCH 4/4] Count only valid-SSN dependents' amounts in the paired-test guards Round-3 review nit: the dependent and minor-filer guards counted amounts of dependents without a valid SSN, which a mask that ignores dependent status would still exclude, so those returns could not show the regression. The guards now count only valid-SSN dependents' amounts, and the minor-filer guard checks the role-based mask's counterfactual directly. Co-Authored-By: Claude Opus 5.5 --- ...t_tip_and_overtime_deduction_properties.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py index 787c186b302..a09ff7fd9da 100644 --- a/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py +++ b/policyengine_us/tests/core/test_tip_and_overtime_deduction_properties.py @@ -432,6 +432,16 @@ def amount(): return units +def _valid_dependent_amount(unit, output, adults_only=False): + """Output 0 (tips) or 1 (overtime) of dependents with valid SSNs, the + amounts a mask that ignored dependent status would count.""" + return sum( + dependent[2 + output] + for dependent in unit.dependents + if dependent[1] in VALID_SSN and (dependent[0] >= 18 or not adults_only) + ) + + def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): rng = np.random.default_rng(2) # Returns around the thresholds and caps, and returns well below them. @@ -498,11 +508,7 @@ def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): # SSN and whose head has something to claim. output = 0 if name == "tip_income_deduction" else 1 dependent_amounts = [ - _observable( - unit, - output, - sum(dependent[2 + output] for dependent in unit.dependents), - ) + _observable(unit, output, _valid_dependent_amount(unit, output)) for unit in base ] invalid_amounts = [ @@ -514,18 +520,13 @@ def test_dependents_and_filers_without_valid_ssns_add_nothing(reference_system): ) for unit in base ] + # A role-based mask counts these adult dependents, inferred as head + # (and spouse), and nothing once they are removed. minor_filer_with_adult_dependents = [ unit.status != "JOINT" and unit.ages[0] < 18 - and _observable( - unit, - output, - sum( - dependent[2 + output] - for dependent in unit.dependents - if dependent[0] >= 18 - ), - ) + and unit.magi <= _threshold(unit.status) + and _valid_dependent_amount(unit, output, adults_only=True) > 0 for unit in base ] assert sum(dependent_amounts) >= 30, name