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..116a0ad64e7 --- /dev/null +++ b/changelog.d/fix-tips-overtime-dependents-ssn.fixed.md @@ -0,0 +1 @@ +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 90377ed5557..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 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 filer (not a dependent) 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..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 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 filer (not a dependent) 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..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 @@ -9,13 +9,27 @@ 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, 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 - (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 +42,18 @@ 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. 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 @@ -35,6 +61,8 @@ magnitudes. """ +from collections import namedtuple + import numpy as np import pytest from hypothesis import Phase, given, settings @@ -60,6 +88,24 @@ 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, 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 ages", + defaults=(("CITIZEN", "CITIZEN"), (), (40, 40)), +) @pytest.fixture(scope="module") @@ -77,6 +123,18 @@ 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, 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)) + 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 +148,50 @@ 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}", unit.ages[0], unit.ssns[0], unit.head_tips, unit.head_overtime) + ] + if unit.status == "JOINT": + filers.append( + ( + f"spouse{i}", + unit.ages[1], + 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 +203,29 @@ 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]) 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 +243,18 @@ 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))) + ) + ages = (int(rng.choice([40, 17], p=[0.8, 0.2])), 40) + draws.append((magi, *amounts, ssns, dependents, ages)) return draws @@ -184,17 +276,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 +384,153 @@ 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 _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 _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. + 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, + "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 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 = [ + _observable(unit, output, _valid_dependent_amount(unit, output)) + for unit in base + ] + invalid_amounts = [ + _observable( + unit, + output, + _counted(unit._replace(ssns=("CITIZEN", "CITIZEN")))[output] + - _counted(unit)[output], + ) + 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 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 + assert sum(invalid_amounts) >= 15, name + assert sum(minor_filer_with_adult_dependents) >= 3, 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..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 @@ -318,3 +318,211 @@ # 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 + +- 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 new file mode 100644 index 00000000000..e6f949e3fcf --- /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,131 @@ +- 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] + +- 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/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_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 35655c72368..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 @@ -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,190 @@ # 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 + +- 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 new file mode 100644 index 00000000000..838f445e907 --- /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,131 @@ +- 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] + +- 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/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..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,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 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) 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..8694b0d171b --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/overtime_income/overtime_income_deduction_eligible_person.py @@ -0,0 +1,29 @@ +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 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", + "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. 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 a2429af10cf..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,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 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" 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..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,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 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) 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..93ad91bc19c --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/tip_income/tip_income_deduction_eligible_person.py @@ -0,0 +1,28 @@ +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 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", + "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. 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 6e06600c6cf..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,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 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" 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)