diff --git a/changelog.d/al-agi-exclude-dependents.fixed.md b/changelog.d/al-agi-exclude-dependents.fixed.md new file mode 100644 index 00000000000..732e2b98393 --- /dev/null +++ b/changelog.d/al-agi-exclude-dependents.fixed.md @@ -0,0 +1 @@ +Leave tax unit dependents' income and deductions out of Alabama adjusted gross income; dependents report them on their own Alabama returns. diff --git a/changelog.d/al-dependent-exemption-boundary.fixed.md b/changelog.d/al-dependent-exemption-boundary.fixed.md new file mode 100644 index 00000000000..f07e92969f4 --- /dev/null +++ b/changelog.d/al-dependent-exemption-boundary.fixed.md @@ -0,0 +1 @@ +Pay Alabama's $1,000 dependent exemption at AGI of exactly $20,000 (2021 and before) or $50,000 (2022 on), and $500 at exactly $100,000, as Ala. Code 40-18-19(a)(9) provides. diff --git a/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml b/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml index 03b60f72f2e..38052a3c519 100644 --- a/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml +++ b/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml @@ -1,4 +1,7 @@ description: Alabama provides these deductions for the state adjusted gross income. +# Alabama adjusted gross income leaves out a tax unit dependent's income and +# deductions, which belong on the dependent's own return (Ala. Code +# ยงยง 40-18-2(a)(1) and 40-18-27(a); Ala. Admin. Code r. 810-3-27-.01(1)). values: 2021-01-01: # IRA distributions prior to 1982 are not deductible. @@ -6,12 +9,9 @@ values: - alimony_expense # Line 4 - early_withdrawal_penalty # Line 5 - self_employed_health_insurance_ald # Line 7 - # Alabama AGI counts dependents' income, so their deductions too. - - dependents_self_employed_health_insurance_ald - health_savings_account_ald # Line 8 - self_employed_pension_contribution_ald # Line 9 - - dependents_self_employed_pension_contribution_ald - - us_govt_interest + - us_govt_interest_person - taxable_public_pension_income 2023-01-01: # IRA distributions prior to 1982 are not deductible. @@ -19,12 +19,9 @@ values: - alimony_expense # Line 4 - early_withdrawal_penalty # Line 5 - self_employed_health_insurance_ald # Line 7 - # Alabama AGI counts dependents' income, so their deductions too. - - dependents_self_employed_health_insurance_ald - health_savings_account_ald # Line 8 - self_employed_pension_contribution_ald # Line 9 - - dependents_self_employed_pension_contribution_ald - - us_govt_interest + - us_govt_interest_person - al_retirement_exemption #Schedule RS Line 10 - taxable_public_pension_income diff --git a/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py b/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py index 7891f445fca..42cf2203a87 100644 --- a/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py @@ -23,7 +23,9 @@ def formula(tax_unit, period, parameters): else: eligible = is_dependent count = tax_unit.sum(eligible) - per_dependent = where(p.amount < 0, p_base.dependent.calc(al_agi), p.amount) + per_dependent = where( + p.amount < 0, p_base.dependent.calc(al_agi, right=True), p.amount + ) return count * per_dependent class reform(Reform): diff --git a/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py b/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py index feaa64002e2..0dce861037d 100644 --- a/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py +++ b/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py @@ -20,7 +20,10 @@ def formula(tax_unit, period, parameters): overtime_income = person("fsla_overtime_premium", period) # Cap at per-taxpayer limit per AL HB527 Section 40-18-15(a)(29) person_capped = min_(overtime_income, p.cap) - return tax_unit.sum(person_capped) + # A dependent's overtime is on the dependent's own return, like the + # rest of their income (see al_agi). + is_dependent = person("is_tax_unit_dependent", period) + return tax_unit.sum(person_capped * ~is_dependent) class al_agi(Variable): value_type = float @@ -39,8 +42,8 @@ def formula(tax_unit, period, parameters): period ).gov.states.al.tax.income.agi.gross_income_sources deductions = parameters(period).gov.states.al.tax.income.agi.deductions - gross_income = add(tax_unit, period, gross_sources) - agi_deductions = add(tax_unit, period, deductions) + gross_income = tax_unit_non_dep_add(tax_unit, period, gross_sources) + agi_deductions = tax_unit_non_dep_add(tax_unit, period, deductions) base_al_agi = gross_income - agi_deductions # If HB527 is in effect, subtract the overtime deduction overtime_deduction = tax_unit("al_hb527_overtime_deduction", period) diff --git a/policyengine_us/tests/core/test_al_agi_dependent_invariants.py b/policyengine_us/tests/core/test_al_agi_dependent_invariants.py new file mode 100644 index 00000000000..4715b562e27 --- /dev/null +++ b/policyengine_us/tests/core/test_al_agi_dependent_invariants.py @@ -0,0 +1,345 @@ +"""Invariants for tax unit dependents in Alabama adjusted gross income. + +Alabama taxes every resident individual on their own income (Ala. Code +40-18-2(a)(1)), and every taxpayer over the filing threshold files a return +stating their own items of gross income (40-18-27(a)). Only a husband and +wife may combine income on one return (Ala. Admin. Code r. 810-3-27-.01(1)), +and the Form 40 booklet tells resident dependents to file their own returns. +So a tax unit dependent's income and deductions never belong in the filer's +Alabama AGI. + +Hypothesis draws batches of Alabama tax units (single, head of family with +dependents, joint with and without dependents), and a seeded population of +150 units adds breadth. Each batch runs as one vectorized simulation, twice: +with the dependents' income and deduction inputs as drawn and with them set +to zero. For every tax unit: + +1. The dependents' inputs never change Alabama AGI, the standard deduction + or the personal and dependent exemptions. Alabama income tax is unchanged + too wherever the itemized deductions and the federal income tax deduction + are. Two leaks outside AGI remain, and are left out here: Alabama + itemized deductions still count a dependent's FICA and self-employment + tax (PolicyEngine/policyengine-us#9941), and a dependent's dividends or + business income can still change the filer's federal tax through the + federal preferential-rate base and QBI deduction. +2. Differential: Alabama AGI equals an independent numpy sum, over the head + and spouse, of the parameterized gross income sources less the + person-level deductions, less the tax-unit deductions. For units without + dependents this is the previous all-member sum, so they see no change. +3. The dependent exemption per dependent is $1,000 for Alabama AGI up to and + including $50,000, $500 up to and including $100,000 and $300 above + (Ala. Code 40-18-19(a)(9)d-e, 2022 on), so it never rises with AGI. +""" + +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 + +# Person-level inputs behind Alabama's gross income sources and deductions. +DEPENDENT_INPUTS = [ + "employment_income", + "self_employment_income", + "self_employed_health_insurance_premiums", + "self_employed_pension_contributions", + "taxable_interest_income", + "us_govt_interest_person", + "qualified_dividend_income", + "alimony_income", + "alimony_expense", + "taxable_ira_distributions", + "traditional_ira_contributions", + "early_withdrawal_penalty", + "rental_income", + "long_term_capital_gains", + "taxable_public_pension_income", +] +UNIT_OUTPUTS = [ + "al_agi", + "al_standard_deduction", + "al_personal_exemption", + "al_dependent_exemption", + "al_itemized_deductions", + "al_federal_income_tax_deduction", + "al_income_tax", + "tax_unit_dependents", +] +# Outputs that depend on Alabama AGI but not on federal tax or itemized +# deductions. +INVARIANT_OUTPUTS = [ + "al_agi", + "al_standard_deduction", + "al_personal_exemption", + "al_dependent_exemption", +] + +amount = st.integers(1, 40_000).map(float) +small = st.one_of(st.just(0.0), st.integers(1, 5_000).map(float)) + + +def _maybe(draw, strategy): + return draw(strategy) if draw(st.booleans()) else 0.0 + + +@st.composite +def person_amounts(draw): + interest = _maybe(draw, st.integers(1, 5_000).map(float)) + public_pension = _maybe(draw, st.integers(1, 20_000).map(float)) + return { + "employment_income": _maybe(draw, amount), + # Draw some business losses: a dependent's loss must not lower the + # filer's AGI either. + "self_employment_income": _maybe(draw, st.integers(-10_000, 40_000).map(float)), + "self_employed_health_insurance_premiums": draw(small), + "self_employed_pension_contributions": draw(small), + "taxable_interest_income": interest, + # U.S. obligation interest is part of the person's interest income. + "us_govt_interest_person": float(draw(st.integers(0, int(interest)))), + "qualified_dividend_income": draw(small), + "alimony_income": draw(small), + "alimony_expense": draw(small), + "taxable_ira_distributions": draw(small), + "traditional_ira_contributions": draw(small), + "early_withdrawal_penalty": draw(st.integers(0, 300).map(float)), + "rental_income": _maybe(draw, st.integers(-5_000, 20_000).map(float)), + "long_term_capital_gains": draw(small), + # A public pension is both in pension income and subtracted from + # Alabama gross income. + "taxable_public_pension_income": public_pension, + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "hof", "joint", "joint_dependents"])) + n_dependents = draw(st.integers(1, 2)) if kind in ("hof", "joint_dependents") else 0 + return { + "head": {"age": draw(st.integers(25, 75)), **draw(person_amounts())}, + "spouse": ( + {"age": draw(st.integers(25, 75)), **draw(person_amounts())} + if kind.startswith("joint") + else None + ), + "dependents": [ + {"age": draw(st.integers(5, 23)), **draw(person_amounts())} + for _ in range(n_dependents) + ], + } + + +SEED = 20261006 + + +def _seeded_amounts(rng): + def some(low, high, p=0.5): + return float(round(rng.uniform(low, high))) if rng.random() < p else 0.0 + + interest = some(1, 5_000) + public_pension = some(1, 20_000, 0.3) + return { + "employment_income": some(1, 40_000, 0.6), + "self_employment_income": some(-10_000, 40_000, 0.4), + "self_employed_health_insurance_premiums": some(1, 5_000, 0.3), + "self_employed_pension_contributions": some(1, 5_000, 0.3), + "taxable_interest_income": interest, + "us_govt_interest_person": float(round(rng.uniform(0, interest))), + "qualified_dividend_income": some(1, 5_000, 0.3), + "alimony_income": some(1, 5_000, 0.2), + "alimony_expense": some(1, 5_000, 0.2), + "taxable_ira_distributions": some(1, 5_000, 0.3), + "traditional_ira_contributions": some(1, 5_000, 0.3), + "early_withdrawal_penalty": some(0, 300, 0.2), + "rental_income": some(-5_000, 20_000, 0.3), + "long_term_capital_gains": some(1, 5_000, 0.3), + "taxable_public_pension_income": public_pension, + } + + +def _seeded_units(n=150): + rng = np.random.default_rng(SEED) + units = [] + for _ in range(n): + kind = rng.choice(["single", "hof", "joint", "joint_dependents"]) + n_dependents = ( + int(rng.integers(1, 3)) if kind in ("hof", "joint_dependents") else 0 + ) + units.append( + { + "head": {"age": int(rng.integers(25, 76)), **_seeded_amounts(rng)}, + "spouse": ( + {"age": int(rng.integers(25, 76)), **_seeded_amounts(rng)} + if kind.startswith("joint") + else None + ), + "dependents": [ + {"age": int(rng.integers(5, 24)), **_seeded_amounts(rng)} + for _ in range(n_dependents) + ], + } + ) + return units + + +def _situation(units, year, *, zero_dependents): + 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, + **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, + **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}" + amounts = dict(d) + if zero_dependents: + amounts.update({name: 0.0 for name in DEPENDENT_INPUTS}) + people[child] = { + "is_full_time_student": d["age"] >= 19, + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + **amounts, + } + 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: "AL"}, + } + people = { + name: {k: {year: v} for k, v in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +def _agi_items(sim, year): + """Alabama's AGI items split by entity, read from the parameter lists.""" + p = sim.tax_benefit_system.parameters(year).gov.states.al.tax.income.agi + variables = sim.tax_benefit_system.variables + items = {} + for kind, names in ( + ("gross", p.gross_income_sources), + ("deduction", p.deductions), + ): + for name in names: + entity = variables[name].entity.key + items[(kind, name)] = ( + entity, + np.asarray(sim.calculate(name, year), dtype=float), + ) + return items + + +def _run(units, year, *, zero_dependents): + sim = Simulation(situation=_situation(units, year, zero_dependents=zero_dependents)) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in UNIT_OUTPUTS + } + out["is_dependent"] = np.asarray( + sim.calculate("is_tax_unit_dependent", year), dtype=bool + ) + out["unit"] = sim.populations["tax_unit"].members_entity_id + out["items"] = _agi_items(sim, year) + return out + + +def _unit_sum(run, values): + return np.bincount(run["unit"], weights=values, minlength=len(run["al_agi"])) + + +def _check(units, year): + run = _run(units, year, zero_dependents=False) + zeroed = _run(units, year, zero_dependents=True) + filer = ~run["is_dependent"] + has_dependent = run["tax_unit_dependents"] > 0 + + # 1. Dependents' items never reach the filer's Alabama return. + for name in INVARIANT_OUTPUTS: + np.testing.assert_allclose( + run[name], zeroed[name], atol=TOLERANCE, err_msg=name + ) + same_other_deductions = np.isclose( + run["al_federal_income_tax_deduction"], + zeroed["al_federal_income_tax_deduction"], + atol=TOLERANCE, + ) & np.isclose( + run["al_itemized_deductions"], + zeroed["al_itemized_deductions"], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + run["al_income_tax"][same_other_deductions], + zeroed["al_income_tax"][same_other_deductions], + atol=TOLERANCE, + ) + + # 2. Differential against numpy sums over the head and spouse. + reference = np.zeros_like(run["al_agi"]) + all_members = np.zeros_like(run["al_agi"]) + for (kind, name), (entity, values) in run["items"].items(): + sign = 1 if kind == "gross" else -1 + if entity == "person": + reference += sign * _unit_sum(run, filer * values) + all_members += sign * _unit_sum(run, values) + else: + assert entity == "tax_unit", name + reference += sign * values + all_members += sign * values + np.testing.assert_allclose(run["al_agi"], reference, atol=TOLERANCE) + np.testing.assert_allclose( + run["al_agi"][~has_dependent], + all_members[~has_dependent], + atol=TOLERANCE, + ) + + # 3. Statutory dependent exemption tiers, "equal to or less than" each + # threshold (Ala. Code 40-18-19(a)(9)d-e). + agi = run["al_agi"] + per_dependent = np.select([agi <= 50_000, agi <= 100_000], [1_000, 500], 300) + np.testing.assert_allclose( + run["al_dependent_exemption"], + run["tax_unit_dependents"] * per_dependent, + atol=TOLERANCE, + ) + + +# 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_dependents_stay_off_the_filers_alabama_return(units): + _check(units, 2025) + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_seeded_population(year): + _check(_seeded_units(), year) diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml index 42ae27d40ee..ec47cc567f9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml @@ -41,7 +41,7 @@ al_agi: 60_000 al_income_tax: 2_442.75 -- name: A dependent's self-employed health insurance and plan deductions follow their income into Alabama AGI +- name: A dependent's self-employment income and deductions stay on the dependent's own return period: 2025 absolute_error_margin: 0.01 input: @@ -62,12 +62,107 @@ members: [parent, child] state_code: AL output: - # Alabama AGI sums every member's income, so it counts the child's 10,000 - # of Schedule C income and subtracts the child's own self-employed health - # insurance (1,000) and SEP (500) deductions: 50,000 - 1,500. The federal - # AGI leaves all of them on the child's return. + # The child's 10,000 of Schedule C income and their own self-employed + # health insurance (1,000) and SEP (500) deductions all go on the child's + # Alabama return (Ala. Code 40-18-27(a); Form 40 booklet p. 5, + # "Dependent's and Student's Income"), as on the federal return. adjusted_gross_income: 40_000 - al_agi: 48_500 + al_agi: 40_000 + +- name: A dependent's wages stay off the parent's Alabama AGI + period: 2021 + input: + people: + parent: + age: 40 + employment_income: 10_000 + child: + age: 16 + employment_income: 5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # The child files their own Alabama return if their income is over the + # filing threshold (2021 Form 40 booklet p. 5). + adjusted_gross_income: 10_000 + al_agi: 10_000 + +- name: A joint return combines the spouses' income but not a dependent's + period: 2025 + input: + people: + head: + age: 40 + employment_income: 30_000 + spouse: + age: 40 + taxable_interest_income: 2_000 + child: + age: 15 + qualified_dividend_income: 3_000 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: AL + output: + # Ala. Admin. Code r. 810-3-27-.01(1)(b)1: a husband and wife "may elect to + # file a joint return reporting their combined income and deductions". + # 30,000 + 2,000; the child's dividends are on the child's own return. + al_agi: 32_000 + +- name: A dependent's business loss does not reduce the parent's Alabama AGI + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: -5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + al_agi: 40_000 + +- name: Only the filers' own U.S. government interest is subtracted from Alabama AGI + period: 2025 + input: + people: + parent: + age: 40 + taxable_interest_income: 6_000 + us_govt_interest_person: 1_000 + child: + age: 15 + employment_income: 3_000 + taxable_interest_income: 2_000 + us_govt_interest_person: 2_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # 6,000 of interest less the parent's 1,000 of exempt U.S. obligation + # interest. The child's wages and interest, and the child's U.S. + # obligation interest, are on the child's own return. + al_agi: 5_000 - name: A beneficiary's estate or trust income is in Alabama AGI period: 2025 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml index 8e38892af6a..92ca542576c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml @@ -3,6 +3,6 @@ input: state_code: AL interest_income: 10_000 - us_govt_interest: 5_000 + us_govt_interest_person: 5_000 output: al_agi: 5_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml index df18552ce77..06eebdf5f70 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml @@ -60,3 +60,62 @@ tax_unit_dependents: 3 output: al_dependent_exemption: 900 + +# Ala. Code 40-18-19(a)(9)b-e: $1,000 for AGI "equal to or less than" $20,000 +# (2007-2021) or $50,000 (2022 on); $500 for AGI "in excess of" that and +# "equal to or less than" $100,000; $300 above. The Form 40 booklet charts read +# 0 - 20,000 / 20,001 - 100,000 (2021) and 0 - 50,000 / 50,001 - 100,000 +# / Over 100,000 (2025), each on PDF p. 10. +- name: 2021 Alabama dependent exemption is $1,000 at exactly $20,000 of AGI + period: 2021 + input: + state_code: AL + al_agi: 20_000 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 1_000 + +- name: 2021 Alabama dependent exemption is $500 just over $20,000 of AGI + period: 2021 + input: + state_code: AL + al_agi: 20_001 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 500 + +- name: 2025 Alabama dependent exemption is $1,000 at exactly $50,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 50_000 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 1_000 + +- name: 2025 Alabama dependent exemption is $500 just over $50,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 50_001 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 500 + +- name: 2025 Alabama dependent exemption is $500 at exactly $100,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 100_000 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 500 + +- name: 2025 Alabama dependent exemption is $300 just over $100,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 100_001 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml index c2c04efc79d..2f0c519c9a9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml @@ -25,3 +25,71 @@ output: al_federal_income_tax_deduction: 1_010_474 al_income_tax: 79_786 + +- name: A dependent's wages do not raise the parent's Alabama tax, 2021 + period: 2021 + input: + people: + parent: + age: 40 + employment_income: 20_000 + child: + age: 16 + employment_income: 10_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # The child's 10,000 of wages go on the child's own return (2021 Form 40 + # booklet p. 5), so the parent's head of family return has AGI of 20,000: + # standard deduction 4,700 (chart, AGI 0-23,499), personal exemption + # 3,000 and dependent exemption 1,000 (AGI not over 20,000). Federal tax + # after refundable credits is zero, so no federal tax deduction. + # Taxable income 11,300; the model's existing statutory-rate calculation + # gives 2% x 500 + 4% x 2,500 + 5% x 8,300 = 525. The Form 40 tax table + # (2021 booklet p. 25) instead gives 528 for this head of family return. + al_agi: 20_000 + al_standard_deduction: 4_700 + al_personal_exemption: 3_000 + al_dependent_exemption: 1_000 + al_federal_income_tax_deduction: 0 + al_taxable_income: 11_300 + al_income_tax: 525 + +- name: A dependent's wages do not raise the parent's Alabama tax, 2025 + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 24_000 + child: + age: 16 + employment_income: 6_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # The parent's head of family return has AGI of 24,000 (2025 Form 40 + # booklet p. 5): standard deduction 5,200 (chart, AGI 0-25,999), personal + # exemption 3,000 and dependent exemption 1,000 (AGI not over 50,000). + # Federal tax is offset by the nonrefundable child tax credit, so no + # federal tax deduction. Taxable income 14,800; the model's existing + # statutory-rate calculation gives 2% x 500 + 4% x 2,500 + 5% x 11,800 + # = 700. The Form 40 tax table (2025 booklet p. 25) instead gives 703 + # for this head of family return. + al_agi: 24_000 + al_standard_deduction: 5_200 + al_personal_exemption: 3_000 + al_dependent_exemption: 1_000 + al_federal_income_tax_deduction: 0 + al_taxable_income: 14_800 + al_income_tax: 700 diff --git a/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml b/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml index d917451753a..442114e2f65 100644 --- a/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml +++ b/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml @@ -222,3 +222,32 @@ output: # Deduction ($10k) exceeds base AGI ($5k), result should be capped at 0 al_agi: 0 + +- name: AL HB527 - A dependent's overtime and wages stay on the dependent's own return + absolute_error_margin: 0.1 + period: 2025 + reforms: policyengine_us.reforms.states.al.hb527.al_hb527_overtime_deduction.al_hb527_overtime_deduction + input: + gov.contrib.states.al.hb527.in_effect: true + gov.contrib.states.al.hb527.cap: 1_000 + people: + parent: + age: 45 + employment_income: 52_000 + fsla_overtime_premium: 500 + child: + age: 17 + employment_income: 8_000 + fsla_overtime_premium: 800 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_name: AL + output: + # Only the parent's 500 of overtime is deducted on the parent's return. + al_hb527_overtime_deduction: 500 + # 52,000 - 500; the child's 8,000 of wages are on the child's own return. + al_agi: 51_500 diff --git a/policyengine_us/variables/gov/states/al/tax/income/al_agi.py b/policyengine_us/variables/gov/states/al/tax/income/al_agi.py index 23d8bd3f4c5..7e851ca9b85 100644 --- a/policyengine_us/variables/gov/states/al/tax/income/al_agi.py +++ b/policyengine_us/variables/gov/states/al/tax/income/al_agi.py @@ -5,11 +5,29 @@ class al_agi(Variable): value_type = float entity = TaxUnit label = "Alabama adjusted gross income" + documentation = ( + "Alabama adjusted gross income of the head and spouse. A tax unit " + "dependent's income and deductions belong on the dependent's own " + "Alabama return, so they are left out." + ) defined_for = StateCode.AL unit = USD definition_period = YEAR - # The Code of Alabama 1975 - reference = "https://alison.legislature.state.al.us/code-of-alabama" + reference = ( + "https://alison.legislature.state.al.us/code-of-alabama?section=40-18-14", + # Every resident individual is taxed on their own income, and every + # taxpayer over the filing threshold files their own return. + "https://alison.legislature.state.al.us/code-of-alabama?section=40-18-2", + "https://alison.legislature.state.al.us/code-of-alabama?section=40-18-27", + # Ala. Admin. Code r. 810-3-27-.01(1): only a husband and wife may + # combine their income on one return. + "https://admincode.legislature.state.al.us/administrative-code/810-3-27", + # Dependents who meet the filing requirements file their own return. + "https://www.revenue.alabama.gov/wp-content/uploads/2026/01/25f40bk.pdf#page=5", + ) - adds = "gov.states.al.tax.income.agi.gross_income_sources" - subtracts = "gov.states.al.tax.income.agi.deductions" + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.al.tax.income.agi + gross_income = tax_unit_non_dep_add(tax_unit, period, p.gross_income_sources) + deductions = tax_unit_non_dep_add(tax_unit, period, p.deductions) + return gross_income - deductions diff --git a/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py b/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py index 165931c3946..91f3455b95c 100644 --- a/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py +++ b/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py @@ -15,5 +15,7 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.al.tax.income.exemptions al_agi = tax_unit("al_agi", period) dependents = tax_unit("tax_unit_dependents", period) - exemption_per_dependent = p.dependent.calc(al_agi) + # The amount drops only once AGI is over a threshold ("equal to or + # less than" $20,000 or $50,000 keeps $1,000). + exemption_per_dependent = p.dependent.calc(al_agi, right=True) return dependents * exemption_per_dependent