From 47eaf889d4e2002eba30b4e255c07c629ab35626 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 09:52:05 -0400 Subject: [PATCH 1/5] Apply Georgia's overtime exclusion eligibility rules O.C.G.A. 48-7-27(a)(16)(A) limits the exclusion to overtime received by a full-time employee paid by an hourly wage, and (16)(B) extends it to hourly component overtime compensation from employers under the Railway Labor Act. The formula read neither rule. The exclusion now counts fsla_overtime_premium only for a person who is paid hourly and usually works at least 35 hours a week, the full-time line Chapter 7's job credits use. A new input carries Railway Labor Act overtime, which needs neither test and shares the $1,750 cap. Co-Authored-By: Claude Opus 5.5 --- .../fix-ga-overtime-full-time-hourly.fixed.md | 1 + .../overtime/full_time_weekly_hours.yaml | 21 + ...ertime_exclusion_eligibility_invariants.py | 377 ++++++++++++++++++ ...overtime_exclusion_dependent_invariants.py | 32 +- .../ga/tax/income/ga_hb463_integration.yaml | 5 + .../ga_qualified_overtime_exclusion.yaml | 303 ++++++++++++++ ...qualified_overtime_full_time_employee.yaml | 30 ++ .../income/ga_qualified_overtime_exclusion.py | 49 ++- ...a_qualified_overtime_full_time_employee.py | 27 ++ .../ga_rla_hourly_component_overtime.py | 23 ++ .../household/income/person/is_paid_hourly.py | 4 + 11 files changed, 843 insertions(+), 29 deletions(-) create mode 100644 changelog.d/fix-ga-overtime-full-time-hourly.fixed.md create mode 100644 policyengine_us/parameters/gov/states/ga/tax/income/agi/exclusions/overtime/full_time_weekly_hours.yaml create mode 100644 policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.yaml create mode 100644 policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.py create mode 100644 policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py diff --git a/changelog.d/fix-ga-overtime-full-time-hourly.fixed.md b/changelog.d/fix-ga-overtime-full-time-hourly.fixed.md new file mode 100644 index 00000000000..ec868c99e19 --- /dev/null +++ b/changelog.d/fix-ga-overtime-full-time-hourly.fixed.md @@ -0,0 +1 @@ +Apply the full-time and hourly tests and the Railway Labor Act rule in Georgia's qualified overtime exclusion, O.C.G.A. 48-7-27(a)(16). diff --git a/policyengine_us/parameters/gov/states/ga/tax/income/agi/exclusions/overtime/full_time_weekly_hours.yaml b/policyengine_us/parameters/gov/states/ga/tax/income/agi/exclusions/overtime/full_time_weekly_hours.yaml new file mode 100644 index 00000000000..168cd223b07 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ga/tax/income/agi/exclusions/overtime/full_time_weekly_hours.yaml @@ -0,0 +1,21 @@ +description: Georgia treats an employee who usually works at least this many hours a week as full-time under the qualified overtime compensation exclusion. + +# O.C.G.A. § 48-7-27(a)(16)(A) says "full-time employee" and does not define +# the term. The value follows the income tax chapter's own usage: Chapter 7's +# job credits set the term at "a minimum of 35 hours a week". +values: + 2026-01-01: 35 + +metadata: + period: year + unit: hour + label: Georgia qualified overtime exclusion full-time weekly hours + reference: + - title: Georgia HB463 (2025-2026), Section 2-4 adding O.C.G.A. § 48-7-27(a)(16)(A) + href: https://www.legis.ga.gov/api/legislation/document/20252026/249080#page=6 + - title: O.C.G.A. § 48-7-40(a)(5), new full-time employee job + href: https://codes.findlaw.com/ga/title-48-revenue-and-taxation/ga-code-sect-48-7-40/ + - title: O.C.G.A. § 48-7-40.1(a)(3), new full-time employee job + href: https://codes.findlaw.com/ga/title-48-revenue-and-taxation/ga-code-sect-48-7-40-1/ + - title: O.C.G.A. § 48-7-40.26(e)(10)(A), full-time employee + href: https://codes.findlaw.com/ga/title-48-revenue-and-taxation/ga-code-sect-48-7-40-26/ diff --git a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py new file mode 100644 index 00000000000..e3f0c9f5ec2 --- /dev/null +++ b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py @@ -0,0 +1,377 @@ +"""Invariants for who qualifies for Georgia's overtime exclusion. + +O.C.G.A. § 48-7-27(a)(16)(A) subtracts, for tax years 2026 through 2028, "any +amount of qualified overtime compensation, as such term is defined in Section +225 of the Internal Revenue Code, up to $1,750.00 received by a full-time +employee paid by an hourly wage". (16)(B) continues: "Notwithstanding +subparagraph (A) of this paragraph, for employers governed by the federal +National Railway Labor Act, the exemption provided in this paragraph shall +apply to hourly component overtime compensation as defined in applicable +collective bargaining agreements." + +Hypothesis draws batches of Georgia tax units (single, joint, and either with +dependents), and a seeded population of 300 units adds breadth. Each person +gets an hourly flag, usual weekly hours on both sides of the 35-hour line, +overtime premiums and Railway Labor Act overtime, some above the cap. Each +batch runs as vectorized simulations with one input changed at a time. For +every tax unit: + +1. Differential: the exclusion equals an independent numpy sum, over the head + and spouse, of min(premium if hourly and at least 35 hours, plus Railway + Labor Act overtime, $1,750), with both constants restated from the law + rather than read from the parameter files. +2. No leakage: zeroing the premium of everyone who fails either test leaves + the exclusion, Georgia subtractions and Georgia AGI unchanged. +3. Monotonicity: paying everyone hourly, raising hours, raising the premium or + raising Railway Labor Act overtime never lowers the exclusion. +4. Railway Labor Act overtime needs neither test: with no premium, changing + the hourly flag and hours leaves the exclusion unchanged. +5. Bounds: the exclusion lies between 0 and $1,750 times the number of filers. +6. Flow: a change in the exclusion moves Georgia subtractions by the same + amount and Georgia AGI by the opposite amount. +7. The cash tip exclusion, which has neither test, ignores hourly pay, hours + and Railway Labor Act overtime. +8. A person paid hourly always passes is_eligible_for_fsla_overtime, so the + exclusion gains nothing from reading it. + +The seeded population also checks that each failing group holds premiums, so +1 and 2 are not vacuous. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 # dollars +# O.C.G.A. § 48-7-27(a)(16)(A): "up to $1,750.00". +CAP = 1_750 +# Chapter 7's job credits set full-time at "a minimum of 35 hours a week" +# (O.C.G.A. § 48-7-40(a)(5)). +FULL_TIME_HOURS = 35 +HOURS = [0, 20, 34, 34.99, 35, 35.01, 40, 60] +TIPPED_CODE = 102 + +OUTPUTS = [ + "ga_cash_tip_exclusion", + "ga_qualified_overtime_exclusion", + "ga_subtractions", + "ga_agi", +] +AMOUNTS = [ + "is_paid_hourly", + "weekly_hours_worked_before_lsr", + "fsla_overtime_premium", + "ga_rla_hourly_component_overtime", +] + +overtime = st.one_of(st.just(0.0), st.integers(1, 4_000).map(float)) + + +@st.composite +def person_amounts(draw): + premium = draw(overtime) + rla_overtime = draw(st.one_of(st.just(0.0), overtime)) + tip_income = draw(st.one_of(st.just(0.0), st.integers(1, 4_000).map(float))) + return { + "employment_income": ( + float(draw(st.integers(0, 60_000))) + premium + rla_overtime + tip_income + ), + "is_paid_hourly": draw(st.booleans()), + "weekly_hours_worked_before_lsr": draw(st.sampled_from(HOURS)), + "fsla_overtime_premium": premium, + "ga_rla_hourly_component_overtime": rla_overtime, + "tip_income": tip_income, + "treasury_tipped_occupation_code": TIPPED_CODE, + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "joint"])) + n_dependents = draw(st.integers(0, 1)) + return { + "head": {"age": draw(st.integers(25, 70)), **draw(person_amounts())}, + "spouse": ( + {"age": draw(st.integers(25, 70)), **draw(person_amounts())} + if kind == "joint" + else None + ), + "dependents": [ + {"age": draw(st.integers(14, 18)), **draw(person_amounts())} + for _ in range(n_dependents) + ], + } + + +SEED = 20261031 + + +def _seeded_amounts(rng): + def some(low, high, p): + return float(round(rng.uniform(low, high))) if rng.random() < p else 0.0 + + premium = some(1, 4_000, 0.7) + rla_overtime = some(1, 4_000, 0.25) + tip_income = some(1, 4_000, 0.3) + return { + "employment_income": ( + some(1, 60_000, 0.8) + premium + rla_overtime + tip_income + ), + "is_paid_hourly": bool(rng.random() < 0.6), + "weekly_hours_worked_before_lsr": float(rng.choice(HOURS)), + "fsla_overtime_premium": premium, + "ga_rla_hourly_component_overtime": rla_overtime, + "tip_income": tip_income, + "treasury_tipped_occupation_code": TIPPED_CODE, + } + + +def _seeded_units(n=300): + rng = np.random.default_rng(SEED) + units = [] + for _ in range(n): + joint = rng.random() < 0.5 + units.append( + { + "head": {"age": int(rng.integers(25, 71)), **_seeded_amounts(rng)}, + "spouse": ( + {"age": int(rng.integers(25, 71)), **_seeded_amounts(rng)} + if joint + else None + ), + "dependents": [ + {"age": int(rng.integers(14, 19)), **_seeded_amounts(rng)} + for _ in range(int(rng.integers(0, 2))) + ], + } + ) + return units + + +def _situation(units, year, change): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + for i, u in enumerate(units): + head = f"head_{i}" + people[head] = { + "is_tax_unit_head": True, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": False, + **change(u["head"]), + } + members, couple = [head], [head] + if u["spouse"] is not None: + spouse = f"spouse_{i}" + people[spouse] = { + "is_tax_unit_head": False, + "is_tax_unit_spouse": True, + "is_tax_unit_dependent": False, + **change(u["spouse"]), + } + members.append(spouse) + couple.append(spouse) + groups["marital_units"][f"couple_{i}"] = {"members": couple} + for j, d in enumerate(u["dependents"]): + child = f"dependent_{i}_{j}" + people[child] = { + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + **change(d), + } + members.append(child) + groups["marital_units"][f"single_{i}_{j}"] = {"members": [child]} + groups["tax_units"][f"tax_unit_{i}"] = {"members": members} + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "GA"}, + } + people = { + name: {k: {year: v} for k, v in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +def _run(units, year, change=lambda amounts: amounts): + sim = Simulation(situation=_situation(units, year, change)) + out = {name: np.asarray(sim.calculate(name, year), dtype=float) for name in OUTPUTS} + for name in AMOUNTS: + out[name] = np.asarray(sim.calculate(name, year), dtype=float) + out["hourly"] = out["is_paid_hourly"] > 0 + out["filer"] = ~np.asarray(sim.calculate("is_tax_unit_dependent", year), dtype=bool) + out["fsla_eligible"] = np.asarray( + sim.calculate("is_eligible_for_fsla_overtime", year), dtype=bool + ) + out["unit"] = sim.populations["tax_unit"].members_entity_id + return out + + +def _unit_sum(run, values): + return np.bincount( + run["unit"], + weights=values, + minlength=len(run["ga_qualified_overtime_exclusion"]), + ) + + +def _passes(amounts): + return ( + amounts["is_paid_hourly"] + and amounts["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS + ) + + +def _expected(run): + passes = run["hourly"] & (run["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS) + counted = ( + run["fsla_overtime_premium"] * passes + + (run["ga_rla_hourly_component_overtime"]) + ) + return _unit_sum(run, run["filer"] * np.minimum(counted, CAP)) + + +def _at_least(lower, higher, name): + assert (higher[name] >= lower[name] - TOLERANCE).all(), name + + +def _check(units, year, *, require_failing_premiums=False): + base = _run(units, year) + exclusion = "ga_qualified_overtime_exclusion" + + # 1. Differential against numpy. + np.testing.assert_allclose(base[exclusion], _expected(base), atol=TOLERANCE) + + # 2. A premium that fails either test reaches nothing. + no_failing_premium = _run( + units, + year, + lambda a: a if _passes(a) else {**a, "fsla_overtime_premium": 0.0}, + ) + for name in (exclusion, "ga_subtractions", "ga_agi"): + np.testing.assert_allclose( + base[name], no_failing_premium[name], atol=TOLERANCE, err_msg=name + ) + + # 3. Monotonicity in each input. + all_hourly = _run(units, year, lambda a: {**a, "is_paid_hourly": True}) + more_hours = _run( + units, + year, + lambda a: { + **a, + "weekly_hours_worked_before_lsr": a["weekly_hours_worked_before_lsr"] + 5, + }, + ) + more_premium = _run( + units, + year, + lambda a: { + **a, + "fsla_overtime_premium": a["fsla_overtime_premium"] + 100, + }, + ) + more_rla = _run( + units, + year, + lambda a: { + **a, + "ga_rla_hourly_component_overtime": a["ga_rla_hourly_component_overtime"] + + 100, + }, + ) + for higher in (all_hourly, more_hours, more_premium, more_rla): + _at_least(base, higher, exclusion) + + # 4. Railway Labor Act overtime needs neither test. + rla_only = _run(units, year, lambda a: {**a, "fsla_overtime_premium": 0.0}) + rla_only_failing = _run( + units, + year, + lambda a: { + **a, + "fsla_overtime_premium": 0.0, + "is_paid_hourly": False, + "weekly_hours_worked_before_lsr": 0.0, + }, + ) + np.testing.assert_allclose( + rla_only[exclusion], rla_only_failing[exclusion], atol=TOLERANCE + ) + + # 5. Bounds: 0 to $1,750 per filer. + filers = _unit_sum(base, base["filer"].astype(float)) + assert (base[exclusion] >= -TOLERANCE).all() + assert (base[exclusion] <= CAP * filers + TOLERANCE).all() + + # 6. The exclusion flows dollar for dollar. + change = all_hourly[exclusion] - base[exclusion] + np.testing.assert_allclose( + all_hourly["ga_subtractions"] - base["ga_subtractions"], + change, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + all_hourly["ga_agi"] - base["ga_agi"], -change, atol=TOLERANCE + ) + + # 7. The tip exclusion has neither test. + for other in (all_hourly, more_hours, more_rla, rla_only_failing): + np.testing.assert_allclose( + other["ga_cash_tip_exclusion"], + base["ga_cash_tip_exclusion"], + atol=TOLERANCE, + ) + + # 8. Reading is_eligible_for_fsla_overtime would add nothing. + assert base["fsla_eligible"][base["hourly"]].all() + + if require_failing_premiums: + full_time = base["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS + has_premium = base["filer"] & (base["fsla_overtime_premium"] > 0) + assert (has_premium & base["hourly"] & ~full_time).sum() >= 10 + assert (has_premium & ~base["hourly"] & full_time).sum() >= 10 + assert (has_premium & base["hourly"] & full_time).sum() >= 10 + assert (change > 0).sum() >= 10 + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(tax_units(), min_size=10, max_size=30)) +def test_overtime_exclusion_eligibility_invariants(units): + _check(units, 2026) + + +@pytest.mark.parametrize("year", [2026, 2028]) +def test_seeded_population(year): + _check(_seeded_units(), year, require_failing_premiums=True) + + +@pytest.mark.parametrize( + "hours,counted", + [(34.99, False), (35, True), (35.01, True)], +) +def test_full_time_threshold_edge(hours, counted): + amounts = { + "age": 40, + "employment_income": 50_000.0, + "is_paid_hourly": True, + "weekly_hours_worked_before_lsr": hours, + "fsla_overtime_premium": 1_000.0, + "ga_rla_hourly_component_overtime": 0.0, + "tip_income": 0.0, + "treasury_tipped_occupation_code": TIPPED_CODE, + } + run = _run([{"head": amounts, "spouse": None, "dependents": []}], 2026) + assert run["ga_qualified_overtime_exclusion"][0] == (1_000 if counted else 0) diff --git a/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py b/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py index bacf65da749..30ba19d122b 100644 --- a/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py +++ b/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py @@ -8,13 +8,16 @@ wage", and (17)(A) "any amount up to $1,750.00 received in cash tips". A dependent's tips and overtime are the dependent's own income (26 U.S.C. 73(a) for a child's services). They are not in the filer's federal AGI, so -the filer cannot subtract them. +the filer cannot subtract them. test_ga_overtime_exclusion_eligibility_ +invariants.py covers the full-time and hourly tests. Hypothesis draws batches of Georgia tax units (single, head of household with dependents, joint with and without dependents), and a seeded population of 150 units adds breadth. Heads, spouses and dependents each get wages, -tips in or outside a Treasury tipped occupation, and overtime premiums, some -above the cap. Each batch runs as one vectorized simulation, twice: with the +tips in or outside a Treasury tipped occupation, and overtime premiums and +Railway Labor Act overtime, some above the cap. Everyone works full-time and +earns an hourly wage, so a dependent's overtime would count but for the rule +under test. Each batch runs as one vectorized simulation, twice: with the dependents' tips, occupation codes and overtime as drawn and with them set to zero. For every tax unit: @@ -45,11 +48,13 @@ CAP = 1_750 # A Treasury Tipped Occupation Code (102, bartenders) and none. TIPPED_CODE = 102 +FULL_TIME_HOURLY = {"is_paid_hourly": True, "weekly_hours_worked_before_lsr": 40} DEPENDENT_INPUTS = [ "tip_income", "treasury_tipped_occupation_code", "fsla_overtime_premium", + "ga_rla_hourly_component_overtime", ] OUTPUTS = [ "ga_cash_tip_exclusion", @@ -68,13 +73,18 @@ def person_amounts(draw): tip_income = draw(tips) overtime_premium = draw(overtime) - # Tips and the overtime premium are parts of employment income. + rla_overtime = draw(overtime) + # Tips and both overtime amounts are parts of employment income. base_wages = draw(st.integers(0, 60_000)) return { - "employment_income": float(base_wages) + tip_income + overtime_premium, + "employment_income": ( + float(base_wages) + tip_income + overtime_premium + rla_overtime + ), "tip_income": tip_income, "treasury_tipped_occupation_code": draw(st.sampled_from([0, TIPPED_CODE])), "fsla_overtime_premium": overtime_premium, + "ga_rla_hourly_component_overtime": rla_overtime, + **FULL_TIME_HOURLY, } @@ -105,11 +115,16 @@ def some(low, high, p=0.5): tip_income = some(1, 6_000, 0.6) overtime_premium = some(1, 4_000, 0.6) + rla_overtime = some(1, 4_000, 0.2) return { - "employment_income": some(1, 60_000, 0.8) + tip_income + overtime_premium, + "employment_income": ( + some(1, 60_000, 0.8) + tip_income + overtime_premium + rla_overtime + ), "tip_income": tip_income, "treasury_tipped_occupation_code": TIPPED_CODE if rng.random() < 0.7 else 0, "fsla_overtime_premium": overtime_premium, + "ga_rla_hourly_component_overtime": rla_overtime, + **FULL_TIME_HOURLY, } @@ -209,9 +224,12 @@ def _unit_sum(run, values): def _amounts(run): """Each person's own tips (tipped occupation only) and overtime, capped.""" tipped = run["treasury_tipped_occupation_code"] > 0 + overtime_total = ( + run["fsla_overtime_premium"] + (run["ga_rla_hourly_component_overtime"]) + ) return ( np.minimum(run["tip_income"] * tipped, CAP), - np.minimum(run["fsla_overtime_premium"], CAP), + np.minimum(overtime_total, CAP), ) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml index 38040a2d6b4..a593bb846f0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml @@ -156,6 +156,7 @@ age: 35 is_tax_unit_head: true is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 50_000 fsla_overtime_premium: 3_000 tax_units: @@ -178,6 +179,7 @@ age: 35 is_tax_unit_head: true is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 40_000 fsla_overtime_premium: 1_000 tax_units: @@ -222,12 +224,14 @@ age: 38 is_tax_unit_head: true is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 50_000 fsla_overtime_premium: 3_000 spouse: age: 36 is_tax_unit_spouse: true is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 50_000 fsla_overtime_premium: 3_000 tax_units: @@ -254,6 +258,7 @@ age: 35 is_tax_unit_head: true is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 50_000 fsla_overtime_premium: 3_000 tip_income: 4_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml index 9c740823439..10a1a1f715b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml @@ -11,6 +11,8 @@ is_tax_unit_dependent: true employment_income: 8_000 fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 tax_units: tax_unit: members: [parent, teen] @@ -37,6 +39,8 @@ age: 17 employment_income: 8_000 fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 tax_units: tax_unit: members: [parent, teen] @@ -56,16 +60,22 @@ is_tax_unit_head: true employment_income: 40_000 fsla_overtime_premium: 3_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 spouse: age: 40 is_tax_unit_spouse: true employment_income: 40_000 fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 teen: age: 17 is_tax_unit_dependent: true employment_income: 8_000 fsla_overtime_premium: 2_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 tax_units: tax_unit: members: [head, spouse, teen] @@ -87,6 +97,8 @@ is_tax_unit_head: true employment_income: 8_000 fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 tax_units: tax_unit: members: [teen] @@ -97,3 +109,294 @@ state_code: GA output: ga_qualified_overtime_exclusion: 1_000 + +- name: A full-time hourly employee at the 35-hour threshold keeps the exclusion + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 35 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + # O.C.G.A. § 48-7-27(a)(16)(A): "a full-time employee paid by an hourly + # wage". Chapter 7's credits set full-time at "a minimum of 35 hours a + # week" (§ 48-7-40(a)(5)). + ga_qualified_overtime_exclusion: 1_000 + ga_subtractions: 1_000 + ga_agi: 49_000 + +- name: An hourly employee below the full-time threshold gets no exclusion + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 34.5 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_exclusion: 0 + ga_subtractions: 0 + ga_agi: 50_000 + +- name: A full-time salaried employee gets no exclusion for overtime + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: false + weekly_hours_worked_before_lsr: 45 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + # Salaried non-exempt pay can be qualified overtime compensation under + # IRC § 225, but (16)(A) reaches only an employee "paid by an hourly wage". + ga_qualified_overtime_exclusion: 0 + +- name: A record with overtime but no hourly or hours input gets no exclusion + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + fsla_overtime_premium: 1_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_full_time_employee: false + ga_qualified_overtime_exclusion: 0 + +- name: Full-time status can be set directly when hours are not known + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + ga_qualified_overtime_full_time_employee: true + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_exclusion: 1_000 + +- name: A part-year full-time hourly employee keeps the exclusion + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 20_000 + fsla_overtime_premium: 500 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 + weeks_worked: 20 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + # Full-time describes the employee's usual hours in the weeks worked, not + # an average over the whole year. + ga_qualified_overtime_exclusion: 500 + +- name: On a joint return each spouse passes or fails the tests alone + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 40_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 + spouse: + age: 40 + is_tax_unit_spouse: true + employment_income: 40_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 20 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: GA + output: + # Head: 1,000. Spouse: part-time, so none. + ga_qualified_overtime_exclusion: 1_000 + +- name: Railway Labor Act overtime needs neither test and keeps the cap + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 80_000 + ga_rla_hourly_component_overtime: 3_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + # O.C.G.A. § 48-7-27(a)(16)(B). The DOR's 2026 Employer's Withholding Tax + # Guide, p. 4, has employers report the full amount in W-2 Box 14a, code + # GA OT, and applies "the $1,750 limitation" when the employee files. + ga_qualified_overtime_exclusion: 1_750 + ga_subtractions: 1_750 + ga_agi: 78_250 + +- name: Railway Labor Act overtime and qualified overtime share one cap + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 80_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 + ga_rla_hourly_component_overtime: 1_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_exclusion: 1_750 + +- name: Part-time overtime stays out while Railway Labor Act overtime counts + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 40_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 20 + ga_rla_hourly_component_overtime: 500 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_exclusion: 500 + +- name: A dependent's Railway Labor Act overtime stays off the filer's return + period: 2026 + input: + people: + parent: + age: 45 + is_tax_unit_head: true + employment_income: 60_000 + teen: + age: 18 + is_tax_unit_dependent: true + employment_income: 8_000 + ga_rla_hourly_component_overtime: 1_000 + tax_units: + tax_unit: + members: [parent, teen] + households: + household: + members: [parent, teen] + state_code: GA + output: + ga_qualified_overtime_exclusion: 0 + +- name: The tests reach 2028, the exclusion's last year + period: 2028 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: false + weekly_hours_worked_before_lsr: 40 + ga_rla_hourly_component_overtime: 200 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_exclusion: 200 + ga_subtractions: 200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.yaml new file mode 100644 index 00000000000..74f6e1e6ace --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.yaml @@ -0,0 +1,30 @@ +- name: 35 usual weekly hours is full-time + period: 2026 + input: + weekly_hours_worked_before_lsr: 35 + state_code: GA + output: + ga_qualified_overtime_full_time_employee: true + +- name: Just under 35 usual weekly hours is not full-time + period: 2026 + input: + weekly_hours_worked_before_lsr: 34.9 + state_code: GA + output: + ga_qualified_overtime_full_time_employee: false + +- name: A record without hours is not full-time + period: 2026 + input: + state_code: GA + output: + ga_qualified_overtime_full_time_employee: false + +- name: Only Georgia applies the test + period: 2026 + input: + weekly_hours_worked_before_lsr: 40 + state_code: SC + output: + ga_qualified_overtime_full_time_employee: false diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py index c350e656f1f..5d8a2459131 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py @@ -10,36 +10,41 @@ class ga_qualified_overtime_exclusion(Variable): defined_for = StateCode.GA # GA HB463 Section 2-4 adds O.C.G.A. § 48-7-27(a)(16). reference = ( - "https://www.legis.ga.gov/api/legislation/document/20252026/249080#page=6" + "https://www.legis.ga.gov/api/legislation/document/20252026/249080#page=6", + "https://dor.georgia.gov/document/document/2026-employers-tax-guide-updated-june-2026/download#page=4", ) def formula(tax_unit, period, parameters): - # GA HB463 § 48-7-27(a)(16) (TY 2026-2028): each full-time hourly - # employee may exclude up to $1,750 of qualified overtime compensation - # (defined by reference to IRC § 225) from Georgia taxable income. + # O.C.G.A. § 48-7-27(a)(16) (TY 2026-2028): two amounts count toward + # one $1,750 limit per employee. + # - (16)(A): qualified overtime compensation, defined by reference + # to IRC § 225, "received by a full-time employee paid by an hourly + # wage". `fsla_overtime_premium` holds the § 225 amount, and it + # counts only for a person who passes both tests. Reading + # `is_eligible_for_fsla_overtime` as well would add nothing: it is + # always true for a person paid hourly. + # - (16)(B): "notwithstanding subparagraph (A)", for employers + # governed by the federal Railway Labor Act, hourly component + # overtime compensation as collective bargaining agreements define + # it. This replaces (A)'s class of compensation, so neither test + # applies, while "the exemption provided in this paragraph" keeps + # the limit. 29 U.S.C. § 213(b)(2)-(3) takes rail and air carrier + # employees out of FLSA overtime, so this pay is a separate input. # - # Approximation: we use `fsla_overtime_premium`, the federally-defined - # Fair Labor Standards Act overtime premium, which by construction - # applies only to FLSA non-exempt workers (typically hourly). Known - # limitations: - # - The bill's "full-time" qualifier is not strictly enforced; - # a part-time hourly worker who happens to exceed 40 hours in - # some weeks would be credited the exclusion under the model. - # - § 48-7-27(a)(16)(B) extends the exclusion to railway workers - # under the National Railway Labor Act, defined by collective - # bargaining agreements. Railway workers are FLSA-exempt under - # 29 U.S.C. § 213(b)(2), so `fsla_overtime_premium` returns zero - # for them and they receive no exclusion under the model. This - # is a known under-modeling of a specifically eligible group. - # - The employer reporting obligations in § 48-7-27(a)(16)(C)-(D) - # are administrative and do not affect household tax liability. + # The employer reporting rules in (16)(C)-(D) do not affect liability. # # § 48-7-27(a) subtracts the exclusion from the taxpayer's federal # adjusted gross income. A dependent's overtime is the dependent's own # income (26 U.S.C. 73(a) for a child's services), left out of the # filer's federal AGI (see irs_gross_income), so it never counts. person = tax_unit.members - overtime = person("fsla_overtime_premium", period) + p = parameters(period).gov.states.ga.tax.income.agi.exclusions.overtime + full_time_hourly = person("is_paid_hourly", period) & person( + "ga_qualified_overtime_full_time_employee", period + ) + qualified_overtime = person("fsla_overtime_premium", period) * full_time_hourly + rla_overtime = person("ga_rla_hourly_component_overtime", period) not_dependent = ~person("is_tax_unit_dependent", period) - cap = parameters(period).gov.states.ga.tax.income.agi.exclusions.overtime.cap - return tax_unit.sum(min_(overtime, cap) * not_dependent) + return tax_unit.sum( + min_(qualified_overtime + rla_overtime, p.cap) * not_dependent + ) diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.py new file mode 100644 index 00000000000..faf6dd1bb89 --- /dev/null +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_full_time_employee.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class ga_qualified_overtime_full_time_employee(Variable): + value_type = bool + entity = Person + label = "Full-time employee for the Georgia qualified overtime exclusion" + documentation = ( + "Whether the person is a full-time employee under O.C.G.A. § " + "48-7-27(a)(16)(A). The formula compares usual weekly hours with the " + "full-time threshold, so a record without hours is not full-time. Set " + "this variable directly when full-time status is known and hours " + "are not." + ) + definition_period = YEAR + defined_for = StateCode.GA + reference = ( + "https://www.legis.ga.gov/api/legislation/document/20252026/249080#page=6" + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.ga.tax.income.agi.exclusions.overtime + # Usual hours in the weeks worked, so a part-year full-time employee + # stays full-time. Like other eligibility tests, this reads hours + # before labor supply responses, which do not change eligibility. + hours = person("weekly_hours_worked_before_lsr", period) + return hours >= p.full_time_weekly_hours diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py new file mode 100644 index 00000000000..42fe62bc931 --- /dev/null +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class ga_rla_hourly_component_overtime(Variable): + value_type = float + entity = Person + label = "Georgia Railway Labor Act hourly component overtime compensation" + unit = USD + documentation = ( + "Annual hourly component overtime compensation, as the applicable " + "collective bargaining agreement defines it, from an employer governed " + "by the federal Railway Labor Act, included in employment_income. " + "Employers report it in Form W-2, Box 14a, code GA OT, before the " + "$1,750 limit. The default dataset does not carry it, so " + "microsimulations on that dataset count none." + ) + definition_period = YEAR + defined_for = StateCode.GA + uprating = "calibration.gov.irs.soi.employment_income" + reference = ( + "https://www.legis.ga.gov/api/legislation/document/20252026/249080#page=6", + "https://dor.georgia.gov/document/document/2026-employers-tax-guide-updated-june-2026/download#page=4", + ) diff --git a/policyengine_us/variables/household/income/person/is_paid_hourly.py b/policyengine_us/variables/household/income/person/is_paid_hourly.py index a5861ab92b2..f07211e5c54 100644 --- a/policyengine_us/variables/household/income/person/is_paid_hourly.py +++ b/policyengine_us/variables/household/income/person/is_paid_hourly.py @@ -5,4 +5,8 @@ class is_paid_hourly(Variable): value_type = bool entity = Person label = "is paid hourly" + documentation = ( + "Whether the person is paid by the hour, imputed from CPS ORG donors. " + "The default is false, so a record without it fails hourly-pay tests." + ) definition_period = YEAR From 9bcaa1bdb22affa8732af16d4e1ff20fcbeb4691 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 09:55:06 -0400 Subject: [PATCH 2/5] Keep the full-time test for Railway Labor Act overtime (16)(B) overrides (A) only where they conflict. "Hourly component" conflicts with "paid by an hourly wage", since pay with an hourly component need not be an hourly wage, but nothing in (B) conflicts with "full-time employee". The DOR's guide keeps (A)'s $1,750 limit on (B) amounts, so (A) still operates on them. Co-Authored-By: Claude Opus 5.5 --- ...ertime_exclusion_eligibility_invariants.py | 77 ++++++++++--------- .../ga_qualified_overtime_exclusion.yaml | 34 +++++++- .../income/ga_qualified_overtime_exclusion.py | 31 ++++---- .../ga_rla_hourly_component_overtime.py | 3 +- 4 files changed, 90 insertions(+), 55 deletions(-) diff --git a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py index e3f0c9f5ec2..ea0082f0512 100644 --- a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py +++ b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py @@ -17,15 +17,16 @@ every tax unit: 1. Differential: the exclusion equals an independent numpy sum, over the head - and spouse, of min(premium if hourly and at least 35 hours, plus Railway - Labor Act overtime, $1,750), with both constants restated from the law - rather than read from the parameter files. -2. No leakage: zeroing the premium of everyone who fails either test leaves - the exclusion, Georgia subtractions and Georgia AGI unchanged. + and spouse who work at least 35 hours, of min(premium if hourly, plus + Railway Labor Act overtime, $1,750), with both constants restated from the + law rather than read from the parameter files. +2. No leakage: zeroing the premium of everyone who fails either test and the + Railway Labor Act overtime of everyone under 35 hours leaves the exclusion, + Georgia subtractions and Georgia AGI unchanged. 3. Monotonicity: paying everyone hourly, raising hours, raising the premium or raising Railway Labor Act overtime never lowers the exclusion. -4. Railway Labor Act overtime needs neither test: with no premium, changing - the hourly flag and hours leaves the exclusion unchanged. +4. Railway Labor Act overtime needs no hourly wage: with no premium, changing + the hourly flag leaves the exclusion unchanged. 5. Bounds: the exclusion lies between 0 and $1,750 times the number of filers. 6. Flow: a change in the exclusion moves Georgia subtractions by the same amount and Georgia AGI by the opposite amount. @@ -34,7 +35,7 @@ 8. A person paid hourly always passes is_eligible_for_fsla_overtime, so the exclusion gains nothing from reading it. -The seeded population also checks that each failing group holds premiums, so +The seeded population also checks that each failing group holds amounts, so 1 and 2 are not vacuous. """ @@ -217,20 +218,28 @@ def _unit_sum(run, values): ) -def _passes(amounts): - return ( - amounts["is_paid_hourly"] - and amounts["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS - ) +def _without_failing_amounts(amounts): + full_time = amounts["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS + return { + **amounts, + "fsla_overtime_premium": ( + amounts["fsla_overtime_premium"] + if full_time and amounts["is_paid_hourly"] + else 0.0 + ), + "ga_rla_hourly_component_overtime": ( + amounts["ga_rla_hourly_component_overtime"] if full_time else 0.0 + ), + } def _expected(run): - passes = run["hourly"] & (run["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS) + full_time = run["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS counted = ( - run["fsla_overtime_premium"] * passes - + (run["ga_rla_hourly_component_overtime"]) + run["fsla_overtime_premium"] * run["hourly"] + + run["ga_rla_hourly_component_overtime"] ) - return _unit_sum(run, run["filer"] * np.minimum(counted, CAP)) + return _unit_sum(run, run["filer"] * full_time * np.minimum(counted, CAP)) def _at_least(lower, higher, name): @@ -244,15 +253,11 @@ def _check(units, year, *, require_failing_premiums=False): # 1. Differential against numpy. np.testing.assert_allclose(base[exclusion], _expected(base), atol=TOLERANCE) - # 2. A premium that fails either test reaches nothing. - no_failing_premium = _run( - units, - year, - lambda a: a if _passes(a) else {**a, "fsla_overtime_premium": 0.0}, - ) + # 2. An amount that fails its tests reaches nothing. + no_failing_amounts = _run(units, year, _without_failing_amounts) for name in (exclusion, "ga_subtractions", "ga_agi"): np.testing.assert_allclose( - base[name], no_failing_premium[name], atol=TOLERANCE, err_msg=name + base[name], no_failing_amounts[name], atol=TOLERANCE, err_msg=name ) # 3. Monotonicity in each input. @@ -285,20 +290,19 @@ def _check(units, year, *, require_failing_premiums=False): for higher in (all_hourly, more_hours, more_premium, more_rla): _at_least(base, higher, exclusion) - # 4. Railway Labor Act overtime needs neither test. - rla_only = _run(units, year, lambda a: {**a, "fsla_overtime_premium": 0.0}) - rla_only_failing = _run( + # 4. Railway Labor Act overtime needs no hourly wage. + rla_hourly = _run( units, year, - lambda a: { - **a, - "fsla_overtime_premium": 0.0, - "is_paid_hourly": False, - "weekly_hours_worked_before_lsr": 0.0, - }, + lambda a: {**a, "fsla_overtime_premium": 0.0, "is_paid_hourly": True}, + ) + rla_salaried = _run( + units, + year, + lambda a: {**a, "fsla_overtime_premium": 0.0, "is_paid_hourly": False}, ) np.testing.assert_allclose( - rla_only[exclusion], rla_only_failing[exclusion], atol=TOLERANCE + rla_hourly[exclusion], rla_salaried[exclusion], atol=TOLERANCE ) # 5. Bounds: 0 to $1,750 per filer. @@ -318,7 +322,7 @@ def _check(units, year, *, require_failing_premiums=False): ) # 7. The tip exclusion has neither test. - for other in (all_hourly, more_hours, more_rla, rla_only_failing): + for other in (all_hourly, more_hours, more_rla, rla_salaried): np.testing.assert_allclose( other["ga_cash_tip_exclusion"], base["ga_cash_tip_exclusion"], @@ -331,9 +335,12 @@ def _check(units, year, *, require_failing_premiums=False): if require_failing_premiums: full_time = base["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS has_premium = base["filer"] & (base["fsla_overtime_premium"] > 0) + has_rla = base["filer"] & (base["ga_rla_hourly_component_overtime"] > 0) assert (has_premium & base["hourly"] & ~full_time).sum() >= 10 assert (has_premium & ~base["hourly"] & full_time).sum() >= 10 assert (has_premium & base["hourly"] & full_time).sum() >= 10 + assert (has_rla & ~base["hourly"] & full_time).sum() >= 10 + assert (has_rla & ~full_time).sum() >= 10 assert (change > 0).sum() >= 10 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml index 10a1a1f715b..59b9c482f20 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml @@ -283,7 +283,7 @@ # Head: 1,000. Spouse: part-time, so none. ga_qualified_overtime_exclusion: 1_000 -- name: Railway Labor Act overtime needs neither test and keeps the cap +- name: Railway Labor Act overtime needs no hourly wage and keeps the cap period: 2026 input: people: @@ -291,6 +291,8 @@ age: 40 is_tax_unit_head: true employment_income: 80_000 + is_paid_hourly: false + weekly_hours_worked_before_lsr: 40 ga_rla_hourly_component_overtime: 3_000 tax_units: tax_unit: @@ -308,6 +310,29 @@ ga_subtractions: 1_750 ga_agi: 78_250 +- name: Part-time Railway Labor Act overtime gets no exclusion + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 30_000 + weekly_hours_worked_before_lsr: 20 + ga_rla_hourly_component_overtime: 1_000 + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + # (16)(B) changes the compensation that counts and leaves (16)(A)'s + # "full-time employee" in place. + ga_qualified_overtime_exclusion: 0 + - name: Railway Labor Act overtime and qualified overtime share one cap period: 2026 input: @@ -331,7 +356,7 @@ output: ga_qualified_overtime_exclusion: 1_750 -- name: Part-time overtime stays out while Railway Labor Act overtime counts +- name: A salaried premium stays out while Railway Labor Act overtime counts period: 2026 input: people: @@ -340,8 +365,8 @@ is_tax_unit_head: true employment_income: 40_000 fsla_overtime_premium: 1_000 - is_paid_hourly: true - weekly_hours_worked_before_lsr: 20 + is_paid_hourly: false + weekly_hours_worked_before_lsr: 40 ga_rla_hourly_component_overtime: 500 tax_units: tax_unit: @@ -366,6 +391,7 @@ age: 18 is_tax_unit_dependent: true employment_income: 8_000 + weekly_hours_worked_before_lsr: 40 ga_rla_hourly_component_overtime: 1_000 tax_units: tax_unit: diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py index 5d8a2459131..b64a0cc2d29 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py @@ -16,20 +16,23 @@ class ga_qualified_overtime_exclusion(Variable): def formula(tax_unit, period, parameters): # O.C.G.A. § 48-7-27(a)(16) (TY 2026-2028): two amounts count toward - # one $1,750 limit per employee. + # one $1,750 limit per full-time employee. # - (16)(A): qualified overtime compensation, defined by reference # to IRC § 225, "received by a full-time employee paid by an hourly # wage". `fsla_overtime_premium` holds the § 225 amount, and it - # counts only for a person who passes both tests. Reading + # counts only for a person paid hourly. Reading # `is_eligible_for_fsla_overtime` as well would add nothing: it is # always true for a person paid hourly. # - (16)(B): "notwithstanding subparagraph (A)", for employers - # governed by the federal Railway Labor Act, hourly component - # overtime compensation as collective bargaining agreements define - # it. This replaces (A)'s class of compensation, so neither test - # applies, while "the exemption provided in this paragraph" keeps - # the limit. 29 U.S.C. § 213(b)(2)-(3) takes rail and air carrier - # employees out of FLSA overtime, so this pay is a separate input. + # governed by the federal Railway Labor Act, "the exemption + # provided in this paragraph" applies to hourly component overtime + # compensation as collective bargaining agreements define it. That + # class takes the place of the § 225 amount and of the hourly wage + # test, since pay with an hourly component need not be an hourly + # wage. (B) leaves the limit and full-time status alone, so both + # stay, and the DOR's guide keeps the limit. 29 U.S.C. § 213(b)(2)- + # (3) takes rail and air carrier employees out of FLSA overtime, + # so this pay is a separate input. # # The employer reporting rules in (16)(C)-(D) do not affect liability. # @@ -39,12 +42,10 @@ def formula(tax_unit, period, parameters): # filer's federal AGI (see irs_gross_income), so it never counts. person = tax_unit.members p = parameters(period).gov.states.ga.tax.income.agi.exclusions.overtime - full_time_hourly = person("is_paid_hourly", period) & person( - "ga_qualified_overtime_full_time_employee", period - ) - qualified_overtime = person("fsla_overtime_premium", period) * full_time_hourly + hourly = person("is_paid_hourly", period) + full_time = person("ga_qualified_overtime_full_time_employee", period) + qualified_overtime = person("fsla_overtime_premium", period) * hourly rla_overtime = person("ga_rla_hourly_component_overtime", period) + overtime = (qualified_overtime + rla_overtime) * full_time not_dependent = ~person("is_tax_unit_dependent", period) - return tax_unit.sum( - min_(qualified_overtime + rla_overtime, p.cap) * not_dependent - ) + return tax_unit.sum(min_(overtime, p.cap) * not_dependent) diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py index 42fe62bc931..fb1d72f2f95 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py @@ -9,7 +9,8 @@ class ga_rla_hourly_component_overtime(Variable): documentation = ( "Annual hourly component overtime compensation, as the applicable " "collective bargaining agreement defines it, from an employer governed " - "by the federal Railway Labor Act, included in employment_income. " + "by the federal Railway Labor Act, included in employment_income. It " + "counts only for a full-time employee, with or without an hourly wage. " "Employers report it in Form W-2, Box 14a, code GA OT, before the " "$1,750 limit. The default dataset does not carry it, so " "microsimulations on that dataset count none." From 4cf747e922413dd4f4f7d127bcaa1d3eada42e7d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 10:51:00 -0400 Subject: [PATCH 3/5] Keep both employee tests for Railway Labor Act overtime (16)(B) replaces only the class of compensation. Nothing else in (A) conflicts with it, so the $1,750 limit, "full-time employee" and "paid by an hourly wage" all stay. The DOR's guide places the Railway Labor Act rule inside the full-time hourly description. Restore the coverage the new defaults emptied from the integration cases: the 2029 sunset and zero-overtime fixtures now pass both tests, and the 2028 case asserts the flow into Georgia subtractions. Drop the Python threshold examples, which the YAML cases cover, and the check that only restated another variable's formula. Co-Authored-By: Claude Opus 5.5 --- ...ertime_exclusion_eligibility_invariants.py | 110 ++++++------------ .../ga/tax/income/ga_hb463_integration.yaml | 7 ++ .../ga_qualified_overtime_exclusion.yaml | 48 ++++---- .../income/ga_qualified_overtime_exclusion.py | 38 +++--- .../ga_rla_hourly_component_overtime.py | 2 +- 5 files changed, 93 insertions(+), 112 deletions(-) diff --git a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py index ea0082f0512..bba75bbee33 100644 --- a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py +++ b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py @@ -9,6 +9,9 @@ apply to hourly component overtime compensation as defined in applicable collective bargaining agreements." +Each case below compares runs or sums a population, which a YAML case can't do. +The YAML file for the variable holds the worked examples. + Hypothesis draws batches of Georgia tax units (single, joint, and either with dependents), and a seeded population of 300 units adds breadth. Each person gets an hourly flag, usual weekly hours on both sides of the 35-hour line, @@ -17,23 +20,21 @@ every tax unit: 1. Differential: the exclusion equals an independent numpy sum, over the head - and spouse who work at least 35 hours, of min(premium if hourly, plus - Railway Labor Act overtime, $1,750), with both constants restated from the - law rather than read from the parameter files. -2. No leakage: zeroing the premium of everyone who fails either test and the - Railway Labor Act overtime of everyone under 35 hours leaves the exclusion, - Georgia subtractions and Georgia AGI unchanged. + and spouse who are paid hourly and work at least 35 hours, of min(premium + plus Railway Labor Act overtime, $1,750), with both constants restated from + the law rather than read from the parameter files. +2. No leakage: zeroing both overtime amounts of everyone who fails either test + leaves the exclusion, Georgia subtractions and Georgia AGI unchanged. 3. Monotonicity: paying everyone hourly, raising hours, raising the premium or raising Railway Labor Act overtime never lowers the exclusion. -4. Railway Labor Act overtime needs no hourly wage: with no premium, changing - the hourly flag leaves the exclusion unchanged. +4. The two amounts are interchangeable: swapping each person's premium and + Railway Labor Act overtime leaves the exclusion unchanged, since one limit + and the same tests cover both. 5. Bounds: the exclusion lies between 0 and $1,750 times the number of filers. 6. Flow: a change in the exclusion moves Georgia subtractions by the same amount and Georgia AGI by the opposite amount. 7. The cash tip exclusion, which has neither test, ignores hourly pay, hours and Railway Labor Act overtime. -8. A person paid hourly always passes is_eligible_for_fsla_overtime, so the - exclusion gains nothing from reading it. The seeded population also checks that each failing group holds amounts, so 1 and 2 are not vacuous. @@ -203,9 +204,6 @@ def _run(units, year, change=lambda amounts: amounts): out[name] = np.asarray(sim.calculate(name, year), dtype=float) out["hourly"] = out["is_paid_hourly"] > 0 out["filer"] = ~np.asarray(sim.calculate("is_tax_unit_dependent", year), dtype=bool) - out["fsla_eligible"] = np.asarray( - sim.calculate("is_eligible_for_fsla_overtime", year), dtype=bool - ) out["unit"] = sim.populations["tax_unit"].members_entity_id return out @@ -219,27 +217,30 @@ def _unit_sum(run, values): def _without_failing_amounts(amounts): - full_time = amounts["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS + if ( + amounts["is_paid_hourly"] + and amounts["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS + ): + return amounts return { **amounts, - "fsla_overtime_premium": ( - amounts["fsla_overtime_premium"] - if full_time and amounts["is_paid_hourly"] - else 0.0 - ), - "ga_rla_hourly_component_overtime": ( - amounts["ga_rla_hourly_component_overtime"] if full_time else 0.0 - ), + "fsla_overtime_premium": 0.0, + "ga_rla_hourly_component_overtime": 0.0, + } + + +def _swap_overtime(amounts): + return { + **amounts, + "fsla_overtime_premium": amounts["ga_rla_hourly_component_overtime"], + "ga_rla_hourly_component_overtime": amounts["fsla_overtime_premium"], } def _expected(run): - full_time = run["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS - counted = ( - run["fsla_overtime_premium"] * run["hourly"] - + run["ga_rla_hourly_component_overtime"] - ) - return _unit_sum(run, run["filer"] * full_time * np.minimum(counted, CAP)) + passes = run["hourly"] & (run["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS) + counted = run["fsla_overtime_premium"] + run["ga_rla_hourly_component_overtime"] + return _unit_sum(run, run["filer"] * passes * np.minimum(counted, CAP)) def _at_least(lower, higher, name): @@ -290,20 +291,9 @@ def _check(units, year, *, require_failing_premiums=False): for higher in (all_hourly, more_hours, more_premium, more_rla): _at_least(base, higher, exclusion) - # 4. Railway Labor Act overtime needs no hourly wage. - rla_hourly = _run( - units, - year, - lambda a: {**a, "fsla_overtime_premium": 0.0, "is_paid_hourly": True}, - ) - rla_salaried = _run( - units, - year, - lambda a: {**a, "fsla_overtime_premium": 0.0, "is_paid_hourly": False}, - ) - np.testing.assert_allclose( - rla_hourly[exclusion], rla_salaried[exclusion], atol=TOLERANCE - ) + # 4. One limit and the same tests cover both amounts. + swapped = _run(units, year, _swap_overtime) + np.testing.assert_allclose(base[exclusion], swapped[exclusion], atol=TOLERANCE) # 5. Bounds: 0 to $1,750 per filer. filers = _unit_sum(base, base["filer"].astype(float)) @@ -322,25 +312,20 @@ def _check(units, year, *, require_failing_premiums=False): ) # 7. The tip exclusion has neither test. - for other in (all_hourly, more_hours, more_rla, rla_salaried): + for other in (all_hourly, more_hours, more_rla): np.testing.assert_allclose( other["ga_cash_tip_exclusion"], base["ga_cash_tip_exclusion"], atol=TOLERANCE, ) - # 8. Reading is_eligible_for_fsla_overtime would add nothing. - assert base["fsla_eligible"][base["hourly"]].all() - if require_failing_premiums: full_time = base["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS - has_premium = base["filer"] & (base["fsla_overtime_premium"] > 0) - has_rla = base["filer"] & (base["ga_rla_hourly_component_overtime"] > 0) - assert (has_premium & base["hourly"] & ~full_time).sum() >= 10 - assert (has_premium & ~base["hourly"] & full_time).sum() >= 10 - assert (has_premium & base["hourly"] & full_time).sum() >= 10 - assert (has_rla & ~base["hourly"] & full_time).sum() >= 10 - assert (has_rla & ~full_time).sum() >= 10 + for amount in ("fsla_overtime_premium", "ga_rla_hourly_component_overtime"): + held = base["filer"] & (base[amount] > 0) + assert (held & base["hourly"] & ~full_time).sum() >= 10, amount + assert (held & ~base["hourly"] & full_time).sum() >= 10, amount + assert (held & base["hourly"] & full_time).sum() >= 10, amount assert (change > 0).sum() >= 10 @@ -363,22 +348,3 @@ def test_overtime_exclusion_eligibility_invariants(units): @pytest.mark.parametrize("year", [2026, 2028]) def test_seeded_population(year): _check(_seeded_units(), year, require_failing_premiums=True) - - -@pytest.mark.parametrize( - "hours,counted", - [(34.99, False), (35, True), (35.01, True)], -) -def test_full_time_threshold_edge(hours, counted): - amounts = { - "age": 40, - "employment_income": 50_000.0, - "is_paid_hourly": True, - "weekly_hours_worked_before_lsr": hours, - "fsla_overtime_premium": 1_000.0, - "ga_rla_hourly_component_overtime": 0.0, - "tip_income": 0.0, - "treasury_tipped_occupation_code": TIPPED_CODE, - } - run = _run([{"head": amounts, "spouse": None, "dependents": []}], 2026) - assert run["ga_qualified_overtime_exclusion"][0] == (1_000 if counted else 0) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml index a593bb846f0..902771d2874 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_hb463_integration.yaml @@ -200,6 +200,8 @@ head: age: 35 is_tax_unit_head: true + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 40_000 fsla_overtime_premium: 0 tax_units: @@ -274,6 +276,7 @@ output: ga_qualified_overtime_exclusion: 1_750 ga_cash_tip_exclusion: 1_750 + ga_subtractions: 3_500 - name: GA HB463 - TY 2029 overtime/tips exclusions self-repealed; no subtraction flows # Both paragraphs stand repealed 12/31/2028; subtractions list drops both @@ -286,6 +289,8 @@ head: age: 35 is_tax_unit_head: true + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 employment_income: 50_000 fsla_overtime_premium: 3_000 tip_income: 4_000 @@ -303,6 +308,8 @@ # and reserved on December 31, 2028. The subtractions list drops # ga_qualified_overtime_exclusion and ga_cash_tip_exclusion from # 2029-01-01, so neither flows through ga_subtractions. + ga_qualified_overtime_exclusion: 1_750 + ga_cash_tip_exclusion: 1_750 ga_subtractions: 0 - name: GA HB463 - TY 2026 cash tip exclusion caps at $1,750 for tipped occupations diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml index 59b9c482f20..b9763e40d08 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml @@ -283,7 +283,7 @@ # Head: 1,000. Spouse: part-time, so none. ga_qualified_overtime_exclusion: 1_000 -- name: Railway Labor Act overtime needs no hourly wage and keeps the cap +- name: Railway Labor Act overtime counts up to the cap period: 2026 input: people: @@ -291,7 +291,7 @@ age: 40 is_tax_unit_head: true employment_income: 80_000 - is_paid_hourly: false + is_paid_hourly: true weekly_hours_worked_before_lsr: 40 ga_rla_hourly_component_overtime: 3_000 tax_units: @@ -318,6 +318,7 @@ age: 40 is_tax_unit_head: true employment_income: 30_000 + is_paid_hourly: true weekly_hours_worked_before_lsr: 20 ga_rla_hourly_component_overtime: 1_000 tax_units: @@ -329,11 +330,12 @@ members: [head] state_code: GA output: - # (16)(B) changes the compensation that counts and leaves (16)(A)'s - # "full-time employee" in place. + # (16)(B) replaces the compensation that counts. "Full-time employee paid + # by an hourly wage" stays. The DOR's guide, p. 22, places the Railway + # Labor Act rule inside that description. ga_qualified_overtime_exclusion: 0 -- name: Railway Labor Act overtime and qualified overtime share one cap +- name: Salaried Railway Labor Act overtime gets no exclusion period: 2026 input: people: @@ -341,8 +343,7 @@ age: 40 is_tax_unit_head: true employment_income: 80_000 - fsla_overtime_premium: 1_000 - is_paid_hourly: true + is_paid_hourly: false weekly_hours_worked_before_lsr: 40 ga_rla_hourly_component_overtime: 1_000 tax_units: @@ -354,20 +355,20 @@ members: [head] state_code: GA output: - ga_qualified_overtime_exclusion: 1_750 + ga_qualified_overtime_exclusion: 0 -- name: A salaried premium stays out while Railway Labor Act overtime counts +- name: Railway Labor Act overtime and qualified overtime share one cap period: 2026 input: people: head: age: 40 is_tax_unit_head: true - employment_income: 40_000 + employment_income: 80_000 fsla_overtime_premium: 1_000 - is_paid_hourly: false + is_paid_hourly: true weekly_hours_worked_before_lsr: 40 - ga_rla_hourly_component_overtime: 500 + ga_rla_hourly_component_overtime: 1_000 tax_units: tax_unit: members: [head] @@ -377,7 +378,7 @@ members: [head] state_code: GA output: - ga_qualified_overtime_exclusion: 500 + ga_qualified_overtime_exclusion: 1_750 - name: A dependent's Railway Labor Act overtime stays off the filer's return period: 2026 @@ -391,6 +392,7 @@ age: 18 is_tax_unit_dependent: true employment_income: 8_000 + is_paid_hourly: true weekly_hours_worked_before_lsr: 40 ga_rla_hourly_component_overtime: 1_000 tax_units: @@ -410,19 +412,25 @@ head: age: 40 is_tax_unit_head: true - employment_income: 50_000 + employment_income: 40_000 + fsla_overtime_premium: 1_000 + is_paid_hourly: true + weekly_hours_worked_before_lsr: 40 + spouse: + age: 40 + is_tax_unit_spouse: true + employment_income: 40_000 fsla_overtime_premium: 1_000 is_paid_hourly: false weekly_hours_worked_before_lsr: 40 - ga_rla_hourly_component_overtime: 200 tax_units: tax_unit: - members: [head] - filing_status: SINGLE + members: [head, spouse] + filing_status: JOINT households: household: - members: [head] + members: [head, spouse] state_code: GA output: - ga_qualified_overtime_exclusion: 200 - ga_subtractions: 200 + ga_qualified_overtime_exclusion: 1_000 + ga_subtractions: 1_000 diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py index b64a0cc2d29..a443e7ae93e 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.py @@ -15,24 +15,21 @@ class ga_qualified_overtime_exclusion(Variable): ) def formula(tax_unit, period, parameters): - # O.C.G.A. § 48-7-27(a)(16) (TY 2026-2028): two amounts count toward - # one $1,750 limit per full-time employee. + # O.C.G.A. § 48-7-27(a)(16) (TY 2026-2028) excludes up to $1,750 per + # employee, for "a full-time employee paid by an hourly wage". Two + # amounts count toward that one limit: # - (16)(A): qualified overtime compensation, defined by reference - # to IRC § 225, "received by a full-time employee paid by an hourly - # wage". `fsla_overtime_premium` holds the § 225 amount, and it - # counts only for a person paid hourly. Reading - # `is_eligible_for_fsla_overtime` as well would add nothing: it is - # always true for a person paid hourly. + # to IRC § 225, which `fsla_overtime_premium` holds. Reading + # `is_eligible_for_fsla_overtime` as well would add nothing: the + # variable is true whenever `is_paid_hourly` is. # - (16)(B): "notwithstanding subparagraph (A)", for employers # governed by the federal Railway Labor Act, "the exemption # provided in this paragraph" applies to hourly component overtime # compensation as collective bargaining agreements define it. That - # class takes the place of the § 225 amount and of the hourly wage - # test, since pay with an hourly component need not be an hourly - # wage. (B) leaves the limit and full-time status alone, so both - # stay, and the DOR's guide keeps the limit. 29 U.S.C. § 213(b)(2)- - # (3) takes rail and air carrier employees out of FLSA overtime, - # so this pay is a separate input. + # class replaces the § 225 amount, which 29 U.S.C. § 213(b)(2)-(3) + # puts out of reach for rail and air carrier employees. (B) changes + # nothing else in (A), so the limit and both employee tests stay. + # It has no years of its own, so it relies on (A). # # The employer reporting rules in (16)(C)-(D) do not affect liability. # @@ -42,10 +39,13 @@ def formula(tax_unit, period, parameters): # filer's federal AGI (see irs_gross_income), so it never counts. person = tax_unit.members p = parameters(period).gov.states.ga.tax.income.agi.exclusions.overtime - hourly = person("is_paid_hourly", period) - full_time = person("ga_qualified_overtime_full_time_employee", period) - qualified_overtime = person("fsla_overtime_premium", period) * hourly - rla_overtime = person("ga_rla_hourly_component_overtime", period) - overtime = (qualified_overtime + rla_overtime) * full_time + overtime = add( + person, + period, + ["fsla_overtime_premium", "ga_rla_hourly_component_overtime"], + ) + eligible = person("is_paid_hourly", period) & person( + "ga_qualified_overtime_full_time_employee", period + ) not_dependent = ~person("is_tax_unit_dependent", period) - return tax_unit.sum(min_(overtime, p.cap) * not_dependent) + return tax_unit.sum(min_(overtime * eligible, p.cap) * not_dependent) diff --git a/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py b/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py index fb1d72f2f95..97e9953030e 100644 --- a/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py +++ b/policyengine_us/variables/gov/states/ga/tax/income/ga_rla_hourly_component_overtime.py @@ -10,7 +10,7 @@ class ga_rla_hourly_component_overtime(Variable): "Annual hourly component overtime compensation, as the applicable " "collective bargaining agreement defines it, from an employer governed " "by the federal Railway Labor Act, included in employment_income. It " - "counts only for a full-time employee, with or without an hourly wage. " + "counts only for a full-time employee paid by an hourly wage. " "Employers report it in Form W-2, Box 14a, code GA OT, before the " "$1,750 limit. The default dataset does not carry it, so " "microsimulations on that dataset count none." From a7a854312a94918c7e35ec73b8d9b92159c3dd5a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 11:03:18 -0400 Subject: [PATCH 4/5] Trim the Georgia overtime properties to what YAML can't do The bounds followed from the differential, and the tip check belongs in a YAML case, so this keeps the properties that compare runs or sum a population. Co-Authored-By: Claude Opus 5.5 --- ...ertime_exclusion_eligibility_invariants.py | 37 ++++--------------- ...overtime_exclusion_dependent_invariants.py | 2 +- .../ga_qualified_overtime_exclusion.yaml | 26 +++++++++++++ 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py index bba75bbee33..d9f4c57f8bb 100644 --- a/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py +++ b/policyengine_us/tests/core/test_ga_overtime_exclusion_eligibility_invariants.py @@ -30,11 +30,11 @@ 4. The two amounts are interchangeable: swapping each person's premium and Railway Labor Act overtime leaves the exclusion unchanged, since one limit and the same tests cover both. -5. Bounds: the exclusion lies between 0 and $1,750 times the number of filers. -6. Flow: a change in the exclusion moves Georgia subtractions by the same +5. Flow: a change in the exclusion moves Georgia subtractions by the same amount and Georgia AGI by the opposite amount. -7. The cash tip exclusion, which has neither test, ignores hourly pay, hours - and Railway Labor Act overtime. + +Case 1 also bounds the exclusion between 0 and $1,750 times the number of +filers. The seeded population also checks that each failing group holds amounts, so 1 and 2 are not vacuous. @@ -54,10 +54,8 @@ # (O.C.G.A. § 48-7-40(a)(5)). FULL_TIME_HOURS = 35 HOURS = [0, 20, 34, 34.99, 35, 35.01, 40, 60] -TIPPED_CODE = 102 OUTPUTS = [ - "ga_cash_tip_exclusion", "ga_qualified_overtime_exclusion", "ga_subtractions", "ga_agi", @@ -76,17 +74,14 @@ def person_amounts(draw): premium = draw(overtime) rla_overtime = draw(st.one_of(st.just(0.0), overtime)) - tip_income = draw(st.one_of(st.just(0.0), st.integers(1, 4_000).map(float))) return { "employment_income": ( - float(draw(st.integers(0, 60_000))) + premium + rla_overtime + tip_income + float(draw(st.integers(0, 60_000))) + premium + rla_overtime ), "is_paid_hourly": draw(st.booleans()), "weekly_hours_worked_before_lsr": draw(st.sampled_from(HOURS)), "fsla_overtime_premium": premium, "ga_rla_hourly_component_overtime": rla_overtime, - "tip_income": tip_income, - "treasury_tipped_occupation_code": TIPPED_CODE, } @@ -117,17 +112,12 @@ def some(low, high, p): premium = some(1, 4_000, 0.7) rla_overtime = some(1, 4_000, 0.25) - tip_income = some(1, 4_000, 0.3) return { - "employment_income": ( - some(1, 60_000, 0.8) + premium + rla_overtime + tip_income - ), + "employment_income": some(1, 60_000, 0.8) + premium + rla_overtime, "is_paid_hourly": bool(rng.random() < 0.6), "weekly_hours_worked_before_lsr": float(rng.choice(HOURS)), "fsla_overtime_premium": premium, "ga_rla_hourly_component_overtime": rla_overtime, - "tip_income": tip_income, - "treasury_tipped_occupation_code": TIPPED_CODE, } @@ -295,12 +285,7 @@ def _check(units, year, *, require_failing_premiums=False): swapped = _run(units, year, _swap_overtime) np.testing.assert_allclose(base[exclusion], swapped[exclusion], atol=TOLERANCE) - # 5. Bounds: 0 to $1,750 per filer. - filers = _unit_sum(base, base["filer"].astype(float)) - assert (base[exclusion] >= -TOLERANCE).all() - assert (base[exclusion] <= CAP * filers + TOLERANCE).all() - - # 6. The exclusion flows dollar for dollar. + # 5. The exclusion flows dollar for dollar. change = all_hourly[exclusion] - base[exclusion] np.testing.assert_allclose( all_hourly["ga_subtractions"] - base["ga_subtractions"], @@ -311,14 +296,6 @@ def _check(units, year, *, require_failing_premiums=False): all_hourly["ga_agi"] - base["ga_agi"], -change, atol=TOLERANCE ) - # 7. The tip exclusion has neither test. - for other in (all_hourly, more_hours, more_rla): - np.testing.assert_allclose( - other["ga_cash_tip_exclusion"], - base["ga_cash_tip_exclusion"], - atol=TOLERANCE, - ) - if require_failing_premiums: full_time = base["weekly_hours_worked_before_lsr"] >= FULL_TIME_HOURS for amount in ("fsla_overtime_premium", "ga_rla_hourly_component_overtime"): diff --git a/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py b/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py index 30ba19d122b..abe7a8a8a4c 100644 --- a/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py +++ b/policyengine_us/tests/core/test_ga_tip_overtime_exclusion_dependent_invariants.py @@ -225,7 +225,7 @@ def _amounts(run): """Each person's own tips (tipped occupation only) and overtime, capped.""" tipped = run["treasury_tipped_occupation_code"] > 0 overtime_total = ( - run["fsla_overtime_premium"] + (run["ga_rla_hourly_component_overtime"]) + run["fsla_overtime_premium"] + run["ga_rla_hourly_component_overtime"] ) return ( np.minimum(run["tip_income"] * tipped, CAP), diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml index b9763e40d08..7604f71e5ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/tax/income/ga_qualified_overtime_exclusion.yaml @@ -434,3 +434,29 @@ output: ga_qualified_overtime_exclusion: 1_000 ga_subtractions: 1_000 + +- name: The cash tip exclusion has neither employee test + period: 2026 + input: + people: + head: + age: 40 + is_tax_unit_head: true + employment_income: 40_000 + is_paid_hourly: false + weekly_hours_worked_before_lsr: 20 + fsla_overtime_premium: 1_000 + ga_rla_hourly_component_overtime: 1_000 + tip_income: 4_000 + tip_income_deduction_occupation_requirement_met: true + tax_units: + tax_unit: + members: [head] + filing_status: SINGLE + households: + household: + members: [head] + state_code: GA + output: + ga_qualified_overtime_exclusion: 0 + ga_cash_tip_exclusion: 1_750 From 4559470480375787a590aa2fa34aad571a42731c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 11:24:23 -0400 Subject: [PATCH 5/5] Record the Georgia cash tip guidance as a source conflict The DOR's 2026 employer guide says the cash tip provision does not apply to Railway Labor Act employees, and paragraph (17) has no such condition. The code already follows the statute, so this adds a backlog entry and no model change. Co-Authored-By: Claude Opus 5.5 --- source_conflicts/candidates.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index a7407abf0cd..1cea80fc061 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -2107,6 +2107,36 @@ candidates: - https://github.com/PolicyEngine/policyengine-us/pull/9969 state: unreviewed note: 'PE-US uses 60% of SMI for size 13, within the federal ceiling, for the limit and the payment bands (fl_liheap_income_limit.py). Distinguish the federal maximum from Florida''s chosen limit and verify the apparently erroneous row.' +- id: us-ga:cash-tips-rla-employees + program: income-tax + years: + from: 2026 + to: 2028 + type: tax + question: Does Georgia's cash tip subtraction reach an employee under the Railway Labor Act, although the Department + of Revenue's 2026 employer guide says it does not? + sources: + - kind: statute + citation: O.C.G.A. §48-7-27(a)(17)(A), added by HB463 (2026) Section 2-4 + url: https://www.legis.ga.gov/api/legislation/document/20252026/249080#page=7 + - kind: guidance + citation: Georgia DOR, 2026 Employer's Withholding Tax Guide, p. 5 + url: https://dor.georgia.gov/document/document/2026-employers-tax-guide-updated-june-2026/download#page=5 + readings: + statute: Paragraph (17) subtracts up to $1,750 of cash tips with no Railway Labor Act condition. HB463 names that + Act only in paragraph (16)(B). + other: The guide says the provision does not apply to employees whose employment is subject to the federal Railway + Labor Act. + example: An airline employee with $4,000 of cash tips in a qualifying occupation subtracts $1,750 under the statute + and nothing under the guide. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/ga/tax/income/ga_cash_tip_exclusion.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10562 + state: ruled + note: The model has no Railway Labor Act status for this test, so the exclusion applies to every employee. - id: us-ga:liheap-income-floor-vs-smi-limit program: liheap years: