diff --git a/changelog.d/amt-child-pre-2018.fixed.md b/changelog.d/amt-child-pre-2018.fixed.md new file mode 100644 index 00000000000..9e505a9b5d1 --- /dev/null +++ b/changelog.d/amt-child-pre-2018.fixed.md @@ -0,0 +1 @@ +Correct pre-2018 AMT for child filers by applying the limited exemption to AMT income, excluding joint returns from the child exemption cap, applying the support test to 18-year-olds and full-time students, and judging the child rule by the filer alone. diff --git a/policyengine_us/parameters/gov/irs/income/amt/exemption/child/amount.yaml b/policyengine_us/parameters/gov/irs/income/amt/exemption/child/amount.yaml index 36b10011c14..a2763e931e9 100644 --- a/policyengine_us/parameters/gov/irs/income/amt/exemption/child/amount.yaml +++ b/policyengine_us/parameters/gov/irs/income/amt/exemption/child/amount.yaml @@ -19,6 +19,12 @@ metadata: href: https://www.irs.gov/pub/irs-drop/rp-19-44.pdf#page=12 - title: 2019 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-18-57.pdf#page=15 + - title: 2017 IRS data release + href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=14 + - title: 2016 Instructions for Form 6251, Exemption Worksheet line 7 + href: https://www.irs.gov/pub/irs-prior/i6251--2016.pdf#page=10 + - title: 2015 Instructions for Form 6251, Exemption Worksheet line 7 + href: https://www.irs.gov/pub/irs-prior/i6251--2015.pdf#page=10 uprating: gov.irs.uprating values: 2013-01-01: 7_150 diff --git a/policyengine_us/parameters/gov/irs/income/amt/exemption/child/support_test/age_threshold.yaml b/policyengine_us/parameters/gov/irs/income/amt/exemption/child/support_test/age_threshold.yaml new file mode 100644 index 00000000000..488c3b9b3bd --- /dev/null +++ b/policyengine_us/parameters/gov/irs/income/amt/exemption/child/support_test/age_threshold.yaml @@ -0,0 +1,12 @@ +description: The IRS applies the support test to a child of at least this age when deciding whether the alternative minimum tax child exemption limitation applies. +values: + 2013-01-01: 18 +metadata: + unit: year + period: year + label: AMT child exemption limitation support test age threshold + reference: + - title: 26 U.S. Code § 1(g)(2)(A) (2016 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapA-partI-sec1.htm + - title: 2017 Instructions for Form 6251, Exemption Worksheet - Line 29 (Certain Children Under Age 24) + href: https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=10 diff --git a/policyengine_us/parameters/gov/irs/income/amt/exemption/child/support_test/rate.yaml b/policyengine_us/parameters/gov/irs/income/amt/exemption/child/support_test/rate.yaml new file mode 100644 index 00000000000..8781056fdc7 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/income/amt/exemption/child/support_test/rate.yaml @@ -0,0 +1,12 @@ +description: The IRS applies the alternative minimum tax child exemption limitation to a child at or above the support test age only if the child's earned income does not exceed this share of the child's support. +values: + 2013-01-01: 0.5 +metadata: + unit: /1 + period: year + label: AMT child exemption limitation support test rate + reference: + - title: 26 U.S. Code § 1(g)(2)(A)(ii)(II) (2016 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapA-partI-sec1.htm + - title: 2017 Instructions for Form 6251, Exemption Worksheet - Line 29 (Certain Children Under Age 24) + href: https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=10 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.yaml index 12d413489fb..31b9f9478ac 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.yaml @@ -140,7 +140,7 @@ employment_income: 0 filing_status: JOINT output: - amt_kiddie_tax_applies: true + amt_kiddie_tax_applies: false # 2025 Form 6251 Exemption Worksheet line 1: 137,000 for joint filers. amt_exemption: 137_000 @@ -176,7 +176,7 @@ employment_income: 0 filing_status: JOINT output: - amt_kiddie_tax_applies: true + amt_kiddie_tax_applies: false amt_exemption: 140_200 - name: 2026 single 18-year-old in the phase-out range @@ -191,3 +191,153 @@ # exemption_phase_out = 0.5 * 100,000 = 50,000 # amt_exemption = max(0, 90,100 - 50,000) = 40,100 amt_exemption: 40_100 + +# IRC 1(g)(2)(C), 59(j); 2017 Form 6251 lines 28-35, instructions p. 11: +# https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=11 +# The model backdates some 2018 parameters into 2017. Supply the historical +# upstream return amounts to isolate the AMT formulas; wages still compute +# adjusted_earnings and the erroneous child cap, rather than supplying the cap. +# 2017 regular rates: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=7 +# Standard deduction: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=15 +# Personal exemptions: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=18 +- name: A joint return by young spouses receives the ordinary 2017 AMT exemption + absolute_error_margin: 0.01 + period: 2017 + input: + people: + head: + age: 18 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + employment_income: 12_000 + taxable_interest_income: 121_000 + spouse: + age: 17 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + employment_income: 17_000 + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + tax_unit_itemizes: false + adjusted_gross_income: 150_000 + standard_deduction: 12_700 + taxable_income_deductions: 12_700 + exemptions: 8_100 + regular_tax_before_credits: 23_777.50 + households: + household: + members: [head, spouse] + state_code: TX + output: + # AGI = 12,000 + 17,000 + 121,000 = 150,000. + # Regular taxable income = 150,000 - 12,700 - 2 * 4,050 = 129,200. + # AMTI = 150,000; below joint phase-out threshold 160,900. + # IRC 1(g)(2)(C) excludes joint returns, so the 59(j) cap cannot apply. + # AMT base = 150,000 - 84,500 = 65,500; tentative tax = 17,030. + # Regular tax = 1,865 + 8,587.50 + 13,325 = 23,777.50; AMT = 0. + taxable_income: 129_200 + amt_income: 150_000 + amt_exemption: 84_500 + amt_income_less_exemptions: 65_500 + amt_base_tax: 17_030 + alternative_minimum_tax: 0 + amt_kiddie_tax_applies: false + +# Post-2017 confirmation controls: the SECURE Act override makes the +# historical predicate defect irrelevant to the federal exemption. +- name: Young joint filers receive the full exemption in 2021 + absolute_error_margin: 0.01 + period: 2021 + input: + people: + head: + age: 18 + is_tax_unit_head: true + is_tax_unit_dependent: false + employment_income: 12_000 + spouse: + age: 17 + is_tax_unit_spouse: true + is_tax_unit_dependent: false + employment_income: 17_000 + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + tax_unit_itemizes: false + households: + household: + members: [head, spouse] + state_code: TX + output: + adjusted_gross_income: 29_000 + taxable_income: 3_900 + amt_income: 29_000 + amt_exemption: 114_600 + amt_income_less_exemptions: 0 + alternative_minimum_tax: 0 + +- name: Young joint filers receive the full exemption in 2022 + absolute_error_margin: 0.01 + period: 2022 + input: + people: + head: + age: 18 + is_tax_unit_head: true + is_tax_unit_dependent: false + employment_income: 12_000 + spouse: + age: 17 + is_tax_unit_spouse: true + is_tax_unit_dependent: false + employment_income: 17_000 + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + tax_unit_itemizes: false + households: + household: + members: [head, spouse] + state_code: TX + output: + adjusted_gross_income: 29_000 + taxable_income: 3_100 + amt_income: 29_000 + amt_exemption: 118_100 + amt_income_less_exemptions: 0 + alternative_minimum_tax: 0 + +- name: The 2017 child cap counts only the separate filer's own earnings + period: 2017 + input: + people: + head: + age: 18 + employment_income: 10_000 + care_and_support_costs: 20_000 + is_tax_unit_head: true + is_tax_unit_spouse: false + spouse: + age: 30 + employment_income: 40_000 + is_tax_unit_head: false + is_tax_unit_spouse: true + tax_units: + unit: + members: [head, spouse] + filing_status: SEPARATE + amt_income: 60_000 + output: + # The filer's 10,000 is at most half of the 20,000 of support, so IRC 59(j) + # applies. The 2017 separate exemption is 42,250, with the phase-out from + # 80,450. The cap is 10,000 + 7,500 = 17,500, and IRC 59(j)(1)(A) leaves + # out the spouse's 40,000. The base is 60,000 - 17,500 = 42,500. + amt_kiddie_tax_applies: true + amt_exemption: 17_500 + amt_income_less_exemptions: 42_500 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.yaml index 2d42b4f2e3c..34f5ca0d540 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.yaml @@ -44,7 +44,7 @@ output: amt_income_less_exemptions: 1_000_000 -- name: Kiddie tax filer before 2018 uses regular taxable income and the child limitation +- name: Kiddie tax filer before 2018 uses AMTI and the child limitation period: 2017 input: amt_income: 200_000 @@ -55,8 +55,8 @@ output: # amt_exemption = min(54,300 - 0.25 * (200,000 - 120,700), 10,000 + 7,500) # = min(34,475, 17,500) = 17,500 - # 190,000 - 17,500 = 172,500 - amt_income_less_exemptions: 172_500 + # 200,000 - 17,500 = 182,500 + amt_income_less_exemptions: 182_500 - name: 2025 single 18-year-old with no earnings uses AMTI and the full exemption period: 2025 @@ -70,7 +70,7 @@ amt_kiddie_tax_applies: true amt_exemption: 88_100 # 2025 Form 6251 line 6 = line 4 - line 5 = 200,000 - 88,100. - # The pre-2018 child treatment would give 190,000 - 9,550 = 180,450. + # Applying a child cap would give 200,000 - 9,550 = 190,450. amt_income_less_exemptions: 111_900 - name: 2026 single 18-year-old with no earnings uses AMTI and the full exemption @@ -95,6 +95,226 @@ employment_income: 0 filing_status: JOINT output: - amt_kiddie_tax_applies: true + amt_kiddie_tax_applies: false # 100,000 is below the 137,000 joint exemption. amt_income_less_exemptions: 0 + +# IRC 55(b), 56(b)(1)(D), 59(j); 2017 Form 6251 lines 28-31: +# https://www.irs.gov/pub/irs-prior/f6251--2017.pdf#page=1 +# Supply sourced 2017 upstream deductions because the model backdates its +# 2018 standard-deduction parameter. The filer is not claimable elsewhere. +# Standard deduction: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=15 +# Personal exemption: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=18 +- name: A child filer uses AMTI rather than regular taxable income before 2018 + absolute_error_margin: 0.01 + period: 2017 + input: + people: + head: + age: 17 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + taxable_interest_income: 50_000 + tax_units: + unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: false + adjusted_gross_income: 50_000 + standard_deduction: 6_350 + taxable_income_deductions: 6_350 + exemptions: 4_050 + households: + household: + members: [head] + state_code: TX + output: + # Assume at least one living parent, as required by IRC 1(g)(2)(B). + # Earned income = 0; ordinary single exemption = 54,300. + # Section 59(j) limits ONLY the exemption: min(54,300, 0 + 7,500). + # No AMT deductions: AMTI = 50,000; base = 50,000 - 7,500 = 42,500. + # 26% * 42,500 = 11,050, irrespective of regular-tax deductions. + taxable_income: 39_600 + amt_income: 50_000 + amt_exemption: 7_500 + amt_income_less_exemptions: 42_500 + amt_base_tax: 11_050 + +# Positive-base control distinguishes AMTI from regular taxable income; +# a below-exemption child alone would not detect the wrong base. +- name: A 2025 child filer uses AMTI with the full ordinary exemption + absolute_error_margin: 0.01 + period: 2025 + input: + people: + head: + age: 17 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + taxable_interest_income: 200_000 + tax_units: + unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: false + households: + household: + members: [head] + state_code: TX + output: + # 2025 single exemption 88,100; no child cap under IRC 55(d)(4)(A)(iii). + # AMTI 200,000 - 88,100 = 111,900; 26% * 111,900 = 29,094. + # Using regular taxable income would wrongly give 184,250 - 88,100. + taxable_income: 184_250 + amt_income: 200_000 + amt_exemption: 88_100 + amt_income_less_exemptions: 111_900 + amt_base_tax: 29_094 + +# An itemizing 2017 child: 2017 Form 6251 line 1 (Form 1040 line 41) plus the +# line 3 (taxes) and line 5 (miscellaneous) add-backs, then the 59(j) cap. +# Upstream 2017 return amounts are supplied, as in the case above. +- name: An itemizing child filer adds back taxes and miscellaneous deductions before 2018 + absolute_error_margin: 0.01 + period: 2017 + input: + people: + head: + age: 17 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + taxable_interest_income: 60_000 + tax_units: + unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: true + adjusted_gross_income: 60_000 + # SALT 6,000 + misc 1,500 (2,700 less 2% * 60,000) + charity 1,000. + taxable_income_deductions: 8_500 + salt_deduction: 6_000 + misc_deduction: 1_500 + exemptions: 4_050 + households: + household: + members: [head] + state_code: TX + output: + # Taxable income = 60,000 - 8,500 - 4,050 = 47,450. + # Line 1 = 47,450 + 4,050 = 51,500; AMTI = 51,500 + 6,000 + 1,500 = 59,000. + # Exemption = min(54,300, 0 + 7,500) = 7,500 (no phase-out below 120,700). + # Base = 59,000 - 7,500 = 51,500; 26% * 51,500 = 13,390. + # The old regular-taxable-income base was 47,450 - 7,500 = 39,950. + taxable_income: 47_450 + amt_income: 59_000 + amt_exemption: 7_500 + amt_income_less_exemptions: 51_500 + amt_base_tax: 13_390 + +# Either side of the 59(j) cap: the cap equals the 54,300 single exemption +# when earned income is 54,300 - 7,500 = 46,800. +- name: A child's 59(j) cap binds just below the ordinary 2017 exemption + absolute_error_margin: 0.01 + period: 2017 + input: + people: + head: + age: 17 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + employment_income: 46_000 + taxable_interest_income: 30_000 + tax_units: + unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: false + adjusted_gross_income: 76_000 + standard_deduction: 6_350 + taxable_income_deductions: 6_350 + exemptions: 4_050 + households: + household: + members: [head] + state_code: TX + output: + # Taxable income = 76,000 - 6,350 - 4,050 = 65,600; AMTI = 76,000. + # Exemption = min(54,300, 46,000 + 7,500) = 53,500. + # Base = 76,000 - 53,500 = 22,500; 26% * 22,500 = 5,850. + taxable_income: 65_600 + amt_income: 76_000 + amt_kiddie_tax_applies: true + amt_exemption: 53_500 + amt_income_less_exemptions: 22_500 + amt_base_tax: 5_850 + +- name: A child's 59(j) cap does not bind just above the ordinary 2017 exemption + absolute_error_margin: 0.01 + period: 2017 + input: + people: + head: + age: 17 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + employment_income: 47_000 + taxable_interest_income: 30_000 + tax_units: + unit: + members: [head] + filing_status: SINGLE + tax_unit_itemizes: false + adjusted_gross_income: 77_000 + standard_deduction: 6_350 + taxable_income_deductions: 6_350 + exemptions: 4_050 + households: + household: + members: [head] + state_code: TX + output: + # Taxable income = 77,000 - 6,350 - 4,050 = 66,600; AMTI = 77,000. + # Exemption = min(54,300, 47,000 + 7,500 = 54,500) = 54,300. + # Base = 77,000 - 54,300 = 22,700; 26% * 22,700 = 5,902. + taxable_income: 66_600 + amt_income: 77_000 + amt_kiddie_tax_applies: true + amt_exemption: 54_300 + amt_income_less_exemptions: 22_700 + amt_base_tax: 5_902 + +# The support test reaches the exemption: an 18-year-old whose earnings +# exceed half of support gets the ordinary exemption, with no 59(j) cap. +- name: An 18-year-old who fails the support test gets the ordinary 2017 exemption + absolute_error_margin: 0.01 + period: 2017 + input: + people: + head: + age: 18 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + employment_income: 10_000 + care_and_support_costs: 15_000 + tax_units: + unit: + members: [head] + filing_status: SINGLE + amt_income: 60_000 + households: + household: + members: [head] + state_code: TX + output: + # 10,000 > 0.5 * 15,000 = 7,500, so IRC 1(g) does not apply. + # Exemption = 54,300 (no phase-out below 120,700); base = 5,700. + # Had the cap applied: min(54,300, 10,000 + 7,500) = 17,500. + amt_kiddie_tax_applies: false + amt_exemption: 54_300 + amt_income_less_exemptions: 5_700 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.yaml index 2a8ebeebdf4..f8adc12c0f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.yaml @@ -3,24 +3,29 @@ period: 2022 input: age_head: 18 + filing_status: SINGLE output: amt_kiddie_tax_applies: true -- name: Kiddie tax does not apply when spouse is 19+ (non-student) - period: 2022 +# IRC 1(g)(2) looks at the filer alone; only a joint return (C) excludes a +# married child, so an older spouse on a separate return does not. +- name: An older spouse does not remove a separate filer from the child rule + period: 2017 input: age_head: 18 - age_spouse: 19 + age_spouse: 30 + filing_status: SEPARATE output: - amt_kiddie_tax_applies: false + amt_kiddie_tax_applies: true -- name: Kiddie tax applies when both head and spouse under 19 +- name: Kiddie tax does not apply to a joint return when both spouses are under 19 period: 2022 input: age_head: 18 age_spouse: 18 + filing_status: JOINT output: - amt_kiddie_tax_applies: true + amt_kiddie_tax_applies: false - name: Kiddie tax does not apply when head is 19+ (non-student) period: 2022 @@ -87,7 +92,7 @@ output: amt_kiddie_tax_applies: false -- name: Kiddie tax does not apply when student spouse is 24+ +- name: Kiddie tax does not apply to a joint return with a student spouse aged 24 period: 2022 input: people: @@ -102,5 +107,123 @@ tax_units: tax_unit: members: [person1, person2] + filing_status: JOINT output: amt_kiddie_tax_applies: false + +- name: A young separate filer remains subject to the child limitation before 2018 + period: 2017 + input: + age_head: 18 + filing_status: SEPARATE + output: + amt_kiddie_tax_applies: true + +- name: Young full-time students filing jointly are excluded before 2018 + period: 2017 + input: + people: + head: + age: 22 + is_full_time_college_student: true + is_tax_unit_head: true + spouse: + age: 23 + is_full_time_college_student: true + is_tax_unit_spouse: true + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + output: + amt_kiddie_tax_applies: false + +# IRC 1(g)(2)(A)(ii)(II) (2016 ed.): a child aged 18, or a full-time student +# under 24, is covered only if earned income "does not exceed one-half of the +# amount of the individual's support". A child under 18 has no support test. +# 2017 instructions, Exemption Worksheet - Line 29: +# https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=10 +- name: An 18-year-old whose earnings equal half of support is covered + period: 2017 + input: + people: + head: + age: 18 + employment_income: 10_000 + care_and_support_costs: 20_000 + is_tax_unit_head: true + tax_units: + unit: + members: [head] + filing_status: SINGLE + output: + # 10,000 <= 0.5 * 20,000 = 10,000. + amt_kiddie_tax_applies: true + +- name: An 18-year-old whose earnings exceed half of support is not covered + period: 2017 + input: + people: + head: + age: 18 + employment_income: 10_000 + care_and_support_costs: 19_998 + is_tax_unit_head: true + tax_units: + unit: + members: [head] + filing_status: SINGLE + output: + # 10,000 > 0.5 * 19,998 = 9,999. + amt_kiddie_tax_applies: false + +- name: A full-time student aged 22 whose earnings are under half of support is covered + period: 2017 + input: + people: + head: + age: 22 + is_full_time_college_student: true + employment_income: 5_000 + care_and_support_costs: 12_000 + is_tax_unit_head: true + tax_units: + unit: + members: [head] + filing_status: SINGLE + output: + # 5,000 <= 0.5 * 12,000 = 6,000. + amt_kiddie_tax_applies: true + +- name: A 17-year-old is covered whatever their earnings + period: 2017 + input: + people: + head: + age: 17 + employment_income: 40_000 + is_tax_unit_head: true + tax_units: + unit: + members: [head] + filing_status: SINGLE + output: + # IRC 1(g)(2)(A)(i): under 18, so no support test. + amt_kiddie_tax_applies: true + +- name: An 18-year-old with earnings and no reported support is covered + period: 2017 + input: + people: + head: + age: 18 + employment_income: 10_000 + is_tax_unit_head: true + tax_units: + unit: + members: [head] + filing_status: SINGLE + output: + # care_and_support_costs is 0, so nobody reported support, and the model + # treats the test as met. + amt_kiddie_tax_applies: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/test_child_amt_exemption_properties.py b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/test_child_amt_exemption_properties.py new file mode 100644 index 00000000000..f8ba79017e9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/test_child_amt_exemption_properties.py @@ -0,0 +1,143 @@ +"""Properties of the AMT child exemption limitation that follow from the law. + +- IRC 59(j)(1) (2016 ed.): for a child to whom IRC 1(g) applies, the AMT + exemption cannot exceed earned income plus the indexed child amount. +- IRC 1(g)(2)(C): IRC 1(g) never applies to a joint return. +- IRC 55(d)(4)(A)(iii): IRC 59(j) does not apply from 2018, so a child's + exemption equals that of an otherwise identical adult. + +YAML cases pin hand-computed values; this test checks the properties across +generated households, without restating the model's formulas. +""" + +import hypothesis +import numpy as np +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import system + +STATUSES = ("SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE") +CAP_YEARS = (2015, 2017) +NO_CAP_YEARS = (2018, 2025, 2026) +YEARS = CAP_YEARS + NO_CAP_YEARS +ADULT_AGE = 40 + +# Fixed cases keep each property non-vacuous whatever Hypothesis draws: a +# capped child, an uncapped child, an 18-year-old on each side of the support +# test, and a joint return by spouses under 18 (no support test applies). +FIXED_CASES = ( + dict(status="SINGLE", age=17, student=False, earnings=0, support=0, amti=50_000), + dict( + status="SINGLE", age=17, student=False, earnings=47_000, support=0, amti=77_000 + ), + dict( + status="SEPARATE", + age=18, + student=False, + earnings=10_000, + support=20_000, + amti=60_000, + ), + dict( + status="SINGLE", + age=18, + student=False, + earnings=10_000, + support=15_000, + amti=60_000, + ), + dict( + status="JOINT", age=17, student=False, earnings=29_000, support=0, amti=150_000 + ), +) + + +def situation(cases): + """Each case appears twice: as given, and as an otherwise identical adult.""" + people, tax_units, households = {}, {}, {} + twins = [dict(case, twin=False) for case in cases] + [ + dict(case, age=ADULT_AGE, student=False, twin=True) for case in cases + ] + for i, case in enumerate(twins): + joint = case["status"] == "JOINT" + members = [] + for role in ("head", "spouse") if joint else ("head",): + person_id = f"{role}_{i}" + members.append(person_id) + people[person_id] = { + "age": {year: case["age"] for year in YEARS}, + "is_full_time_student": {year: case["student"] for year in YEARS}, + "is_tax_unit_head": {year: role == "head" for year in YEARS}, + "is_tax_unit_spouse": {year: role == "spouse" for year in YEARS}, + "is_tax_unit_dependent": {year: False for year in YEARS}, + "adjusted_earnings": { + year: case["earnings"] if role == "head" else 0 for year in YEARS + }, + "care_and_support_costs": {year: case["support"] for year in YEARS}, + } + tax_units[f"unit_{i}"] = { + "members": members, + "filing_status": {year: case["status"] for year in YEARS}, + "amt_income": {year: case["amti"] for year in YEARS}, + } + households[f"household_{i}"] = { + "members": members, + "state_code": {year: "TX" for year in YEARS}, + } + return {"people": people, "tax_units": tax_units, "households": households} + + +case_strategy = st.fixed_dictionaries( + { + "status": st.sampled_from(STATUSES), + "age": st.integers(14, 30), + "student": st.booleans(), + "earnings": st.integers(0, 150_000), + "support": st.integers(0, 150_000), + "amti": st.integers(0, 2_000_000), + } +) + + +@hypothesis.settings(max_examples=5, deadline=None, derandomize=True) +@hypothesis.given(cases=st.lists(case_strategy, min_size=1, max_size=20)) +def test_child_amt_exemption_properties(cases): + cases = [*FIXED_CASES, *cases] + n = len(cases) + sim = Simulation(tax_benefit_system=system, situation=situation(cases)) + earnings = np.array([case["earnings"] for case in cases], dtype=float) + + for year in YEARS: + applies = sim.calculate("amt_kiddie_tax_applies", year) + exemption = sim.calculate("amt_exemption", year) + status = sim.calculate("filing_status", year).decode_to_str() + child, adult = slice(0, n), slice(n, 2 * n) + p = system.parameters(str(year)).gov.irs.income.amt.exemption + + # IRC 1(g)(2)(C): never a joint return. + assert not applies[status == "JOINT"].any(), year + # An adult aged 40 is never a child under IRC 1(g)(2)(A). + assert not applies[adult].any(), year + # The exemption never exceeds the statutory amount for the status. + assert np.all(exemption <= p.amount[status]), year + + if year in CAP_YEARS: + # IRC 59(j)(1): earned income plus the indexed child amount. + limit = earnings + p.child.amount + covered = applies[child] + assert covered.any(), year + assert np.all(exemption[child][covered] <= limit[covered]), year + # The limit is the only difference from an identical adult. + np.testing.assert_array_equal( + exemption[child], + np.minimum(exemption[adult], np.where(covered, limit, np.inf)), + err_msg=str(year), + ) + # The limit binds for at least one fixed case. + assert np.any(exemption[child] < exemption[adult]), year + else: + # IRC 55(d)(4)(A)(iii): no limit, so age makes no difference. + np.testing.assert_array_equal( + exemption[child], exemption[adult], err_msg=str(year) + ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.py index f2451fe8354..1b29beecbed 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/exemption/amt_exemption.py @@ -55,7 +55,12 @@ def formula(tax_unit, period, parameters): # same children and the same 911(d)(2) definition, generally take # earned income from Form 1040 line 7 (taxable wages), so the cap # uses adjusted_earnings, which leaves out pre-tax payroll deductions. - adj_earnings = tax_unit_non_dep_sum("adjusted_earnings", tax_unit, period) + # IRC 59(j)(1)(A) counts the child's own earned income, and a covered + # filer never files jointly, so the cap reads only the head's earnings. + person = tax_unit.members + adj_earnings = tax_unit.sum( + person("adjusted_earnings", period) * person("is_tax_unit_head", period) + ) child_amount = p.exemption.child.amount exemption_cap = where( diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.py index 05d9a5c8670..d77d0c1105b 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/income/amt_income_less_exemptions.py @@ -7,33 +7,27 @@ class amt_income_less_exemptions(Variable): definition_period = YEAR label = "Alternative Minimum Tax Income less exemptions" unit = USD - documentation = "Alternative Minimum Tax (AMT) income less exemptions" + documentation = ( + "AMT income less the exemption, including the limited child exemption " + "under IRC 59(j) before 2018." + ) reference = [ "https://www.law.cornell.edu/uscode/text/26/55#b_1_B", + "https://www.law.cornell.edu/uscode/text/26/59#j", "https://www.law.cornell.edu/uscode/text/26/55#d_4_A_iii", + "https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=10", "https://www.irs.gov/pub/irs-pdf/i6251.pdf#page=9", ] def formula(tax_unit, period, parameters): - p = parameters(period).gov.irs.income.amt # Form 6251, Part I # Line 4 amt_income = tax_unit("amt_income", period) - if p.exemption.child.in_effect: - # For filers subject to the kiddie tax, the deductions are not - # added back - taxable_income = tax_unit("taxable_income", period) - kiddie_tax_applies = tax_unit("amt_kiddie_tax_applies", period) - applied_income = where(kiddie_tax_applies, taxable_income, amt_income) - else: - # IRC 55(d)(4)(A)(iii) turns off the child limitation for taxable - # years beginning after 2017; line 6 is line 4 less line 5 for - # every filer. - applied_income = amt_income # Form 6251, Part II top # Line 5 amt_exemption = tax_unit("amt_exemption", period) - # Line 6 - return max_(0, applied_income - amt_exemption) + # Line 6 (2017 lines 28-30): IRC 59(j) limits only the exemption, + # not the AMTI base. amt_exemption applies that cap before 2018. + return max_(0, amt_income - amt_exemption) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.py index 48785e4a738..6719e6753ac 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.py @@ -7,43 +7,58 @@ class amt_kiddie_tax_applies(Variable): definition_period = YEAR label = "Alternative Minimum Tax kiddie tax applies" documentation = ( - "Whether the kiddie tax applies to the tax unit. Used only for the " - "IRC 59(j) AMT child exemption limitation, which IRC 55(d)(4)(A)(iii) " - "turns off for taxable years beginning after 2017." + "Whether IRC 1(g) applies to the filer, for the IRC 59(j) AMT child " + "exemption limitation, which IRC 55(d)(4)(A)(iii) turns off for " + "taxable years beginning after 2017. The filer must be under 18, or " + "meet the IRC 152(c)(3) age test with earned income no more than " + "half of their support (care_and_support_costs; the model treats the " + "support test as met when nobody reports support), and must not file " + "a joint return. The model has no input for whether a parent is alive " + "at year end (IRC 1(g)(2)(B)), so it assumes one is." ) reference = [ - "https://www.law.cornell.edu/uscode/text/26/1#g_2_A", + "https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapA-partI-sec1.htm", + "https://www.law.cornell.edu/uscode/text/26/1#g_2", + "https://www.irs.gov/pub/irs-prior/i6251--2017.pdf#page=10", "https://www.irs.gov/pub/irs-prior/p929--2021.pdf", ] def formula(tax_unit, period, parameters): - # The kiddie tax applies to children under 19, or under 24 if a - # full-time student. Per IRC 1(g)(2)(A), the age requirements - # reference Section 152(c)(3), same as the dependent definition. - p = parameters(period).gov.irs.dependent.ineligible_age + p = parameters(period).gov.irs + age_limit = p.dependent.ineligible_age + support_test = p.income.amt.exemption.child.support_test - # Get student status from person-level data + # IRC 1(g)(2) looks at the filer alone, so only the head's facts + # count. A spouse matters only through the joint-return exclusion. person = tax_unit.members - is_student = person("is_full_time_student", period) is_head = person("is_tax_unit_head", period) - is_spouse = person("is_tax_unit_spouse", period) - - # Check if head or spouse is a full-time student - head_is_student = tax_unit.any(is_student & is_head) - spouse_is_student = tax_unit.any(is_student & is_spouse) - - # Get tax unit level ages + head_is_student = tax_unit.any(person("is_full_time_student", period) & is_head) age_head = tax_unit("age_head", period) - age_spouse = tax_unit("age_spouse", period) - - # Determine age limits based on student status - head_age_limit = where(head_is_student, p.student, p.non_student) - spouse_age_limit = where(spouse_is_student, p.student, p.non_student) - - # Check head age: head must be young (and exist - age != 0) - young_head = (age_head != 0) & (age_head < head_age_limit) - - # Check spouse age: spouse must be young (or not exist - age == 0) - no_or_young_spouse = (age_spouse == 0) | (age_spouse < spouse_age_limit) + # IRC 1(g)(2)(A)(i): under 18 at the close of the year. + under_support_test_age = (age_head != 0) & ( + age_head < support_test.age_threshold + ) + # IRC 1(g)(2)(A)(ii)(I): otherwise, the IRC 152(c)(3) age test + # (under 19, or under 24 if a full-time student). + meets_dependent_age_test = (age_head != 0) & ( + age_head < where(head_is_student, age_limit.student, age_limit.non_student) + ) + # IRC 1(g)(2)(A)(ii)(II): earned income (IRC 911(d)(2)) no more than + # half of the child's support. Every living person has some support, + # so zero means nobody reported it, and the model treats the test as + # met. + earned_income = tax_unit.sum(person("adjusted_earnings", period) * is_head) + support = tax_unit.sum(person("care_and_support_costs", period) * is_head) + support_reported = support > 0 + meets_support_test = ~support_reported | ( + earned_income <= support_test.rate * support + ) + meets_age_test = under_support_test_age | ( + meets_dependent_age_test & meets_support_test + ) - return young_head & no_or_young_spouse + # The model assumes a living parent (IRC 1(g)(2)(B)), because no input + # links the filer to a parent. IRC 1(g)(2)(C) excludes joint returns. + filing_status = tax_unit("filing_status", period) + joint = filing_status == filing_status.possible_values.JOINT + return meets_age_test & ~joint