diff --git a/changelog.d/ctc-limit-worksheet-a-credits.fixed.md b/changelog.d/ctc-limit-worksheet-a-credits.fixed.md new file mode 100644 index 00000000000..b7ea75c757a --- /dev/null +++ b/changelog.d/ctc-limit-worksheet-a-credits.fixed.md @@ -0,0 +1 @@ +Order the residential clean energy credit after the Child Tax Credit, as Schedule 8812 Credit Limit Worksheets A and B and Form 5695 do: the CTC's tax-liability limit subtracts that credit only when Credit Limit Worksheet B applies, the credit's own limit subtracts every other non-refundable credit (including the CTC that cannot be refunded), and it no longer precedes the energy efficient home improvement, elderly or disabled, and clean vehicle credits. Colorado's federal CTC replica uses Schedule 8812 line 13 from 2023, as the 2023 DR 0104CN does. diff --git a/policyengine_us/parameters/gov/irs/credits/clean_vehicle/new/preceding_credits.yaml b/policyengine_us/parameters/gov/irs/credits/clean_vehicle/new/preceding_credits.yaml index bf7b30abe53..91bee8a4966 100644 --- a/policyengine_us/parameters/gov/irs/credits/clean_vehicle/new/preceding_credits.yaml +++ b/policyengine_us/parameters/gov/irs/credits/clean_vehicle/new/preceding_credits.yaml @@ -1,12 +1,12 @@ description: Non-refundable tax credits which precede the New Clean Vehicle Credit. values: + # Section 25D(c) orders the residential clean energy credit after these. 2013-01-01: - - foreign_tax_credit + - foreign_tax_credit - cdcc - non_refundable_american_opportunity_credit - lifetime_learning_credit - savers_credit - - residential_clean_energy_credit - energy_efficient_home_improvement_credit - elderly_disabled_credit @@ -17,3 +17,7 @@ metadata: reference: - title: IRS Schedule 3, Additional Credits and Payments href: https://www.irs.gov/pub/irs-pdf/f1040s3.pdf + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c + - title: 2025 Instructions for Form 8936, line 11 + href: https://www.irs.gov/pub/irs-pdf/i8936.pdf#page=6 diff --git a/policyengine_us/parameters/gov/irs/credits/clean_vehicle/used/preceding_credits.yaml b/policyengine_us/parameters/gov/irs/credits/clean_vehicle/used/preceding_credits.yaml index 262014a84e2..12e0f8b7348 100644 --- a/policyengine_us/parameters/gov/irs/credits/clean_vehicle/used/preceding_credits.yaml +++ b/policyengine_us/parameters/gov/irs/credits/clean_vehicle/used/preceding_credits.yaml @@ -1,12 +1,12 @@ description: Non-refundable tax credits which precede the Used Clean Vehicle Credit. values: + # Section 25D(c) orders the residential clean energy credit after these. 2013-01-01: - - foreign_tax_credit + - foreign_tax_credit - cdcc - non_refundable_american_opportunity_credit - lifetime_learning_credit - savers_credit - - residential_clean_energy_credit - energy_efficient_home_improvement_credit - elderly_disabled_credit - new_clean_vehicle_credit @@ -18,3 +18,7 @@ metadata: reference: - title: IRS Schedule 3, Additional Credits and Payments href: https://www.irs.gov/pub/irs-pdf/f1040s3.pdf + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c + - title: 2025 Instructions for Form 8936, line 16 + href: https://www.irs.gov/pub/irs-pdf/i8936.pdf#page=6 diff --git a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml new file mode 100644 index 00000000000..b538937049e --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml @@ -0,0 +1,18 @@ +description: The IRS denies the refundable Child Tax Credit to filers who elect to exclude any amount from gross income under section 911 (the foreign earned income and housing exclusions) if this is true. It has no effect when the credit is fully refundable (2021). +values: + 2013-01-01: false + 2015-01-01: true +metadata: + unit: bool + period: year + label: Refundable Child Tax Credit denied to section 911 exclusion filers + reference: + - title: 26 U.S. Code § 24(d)(3) + href: https://www.law.cornell.edu/uscode/text/26/24#d_3 + # Section 807 of the Trade Preferences Extension Act of 2015 added this + # rule as section 24(d)(5) for taxable years beginning after 2014; Pub. L. + # 115-141 redesignated it as paragraph (3) in 2018. + - title: Public Law 114-27, § 807 + href: https://www.govinfo.gov/content/pkg/PLAW-114publ27/pdf/PLAW-114publ27.pdf#page=58 + - title: 2025 Instructions for Schedule 8812, Part II-A + href: https://www.irs.gov/pub/irs-prior/i1040s8--2025.pdf#page=3 diff --git a/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/README.md b/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/README.md new file mode 100644 index 00000000000..c14cd8f355d --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/README.md @@ -0,0 +1 @@ +# Child Tax Credit tax liability limit diff --git a/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/preceding_credits.yaml b/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/preceding_credits.yaml new file mode 100644 index 00000000000..7c1d642f8cb --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/preceding_credits.yaml @@ -0,0 +1,54 @@ +description: The IRS subtracts these non-refundable credits from income tax before credits to find the tax liability that limits the non-refundable Child Tax Credit. +values: + # The foreign tax credit also precedes the CTC before 2022, but the model + # applies it against regular tax only from 2022 (see non_refundable.yaml). + 2013-01-01: + - cdcc + - non_refundable_american_opportunity_credit + - lifetime_learning_credit + - savers_credit + - energy_efficient_home_improvement_credit + - elderly_disabled_credit + # The Child and Dependent Care Credit was refundable for 2021 only. + 2021-01-01: + - non_refundable_american_opportunity_credit + - lifetime_learning_credit + - savers_credit + - energy_efficient_home_improvement_credit + - elderly_disabled_credit + - new_clean_vehicle_credit + # Line numbers are those of the 2023-2025 Schedule 3. In 2022 the energy + # efficient home improvement credit came from Form 5695, line 30, and the + # used clean vehicle credit did not yet exist (it is zero before 2023). + # Not modeled: Schedule 3, line 6l (Form 8978) in every year, and the + # alternative motor vehicle credit (Form 8910) through 2022. + 2022-01-01: + - foreign_tax_credit # Schedule 3, line 1 + - cdcc # Schedule 3, line 2 + - non_refundable_american_opportunity_credit # Schedule 3, line 3 + - lifetime_learning_credit # Schedule 3, line 3 + - savers_credit # Schedule 3, line 4 + - energy_efficient_home_improvement_credit # Schedule 3, line 5b + - elderly_disabled_credit # Schedule 3, line 6d + - new_clean_vehicle_credit # Schedule 3, line 6f + - used_clean_vehicle_credit # Schedule 3, line 6m + # The residential clean energy credit (Schedule 3, line 5a) is not listed: + # section 25D(c) orders it after every other credit in subpart A, including + # the Child Tax Credit. See subsequent_credits.yaml for the case in which it + # still reduces this limit. + +metadata: + unit: list + period: year + label: Non-refundable credits preceding the Child Tax Credit + reference: + - title: 2025 Instructions for Schedule 8812 (Form 1040), Credit Limit Worksheet A, line 2 + href: https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=4 + - title: 2022 Instructions for Schedule 8812 (Form 1040), Credit Limit Worksheet A, line 2 + href: https://www.irs.gov/pub/irs-prior/i1040s8--2022.pdf#page=4 + - title: 2020 Publication 972, Child Tax Credit and Credit for Other Dependents Worksheet, line 12 + href: https://www.irs.gov/pub/irs-prior/p972--2020.pdf#page=7 + - title: 26 U.S. Code § 26(a) - Limitation based on amount of tax + href: https://www.law.cornell.edu/uscode/text/26/26#a + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c diff --git a/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/subsequent_credits.yaml b/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/subsequent_credits.yaml new file mode 100644 index 00000000000..5f54f0cb60a --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/ctc_tax_liability_limit/subsequent_credits.yaml @@ -0,0 +1,23 @@ +description: The IRS subtracts these non-refundable credits, which are ordered after the Child Tax Credit, from the tax liability that limits the non-refundable Child Tax Credit when the filer completes Credit Limit Worksheet B. +values: + # The adoption credit (Schedule 3, line 6c), mortgage interest credit + # (line 6g) and District of Columbia first-time homebuyer credit (line 6h) + # also belong here but are not modeled. + 2013-01-01: + - residential_clean_energy_credit # Form 5695, line 15 (Schedule 3, line 5a from 2023) + +metadata: + unit: list + period: year + label: Non-refundable credits following the Child Tax Credit + reference: + - title: 2025 Instructions for Schedule 8812 (Form 1040), Credit Limit Worksheet A, line 4 + href: https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=4 + - title: 2025 Instructions for Schedule 8812 (Form 1040), Credit Limit Worksheet B, line 15 + href: https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=6 + - title: 2020 Publication 972, Line 14 Worksheet, line 15 + href: https://www.irs.gov/pub/irs-prior/p972--2020.pdf#page=9 + - title: 26 U.S. Code § 24(d)(1)(B) - Portion of credit refundable + href: https://www.law.cornell.edu/uscode/text/26/24#d_1_B + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c diff --git a/policyengine_us/parameters/gov/irs/credits/elderly_or_disabled/preceding_credits.yaml b/policyengine_us/parameters/gov/irs/credits/elderly_or_disabled/preceding_credits.yaml index 7f99fcce934..4bf6a376585 100644 --- a/policyengine_us/parameters/gov/irs/credits/elderly_or_disabled/preceding_credits.yaml +++ b/policyengine_us/parameters/gov/irs/credits/elderly_or_disabled/preceding_credits.yaml @@ -1,12 +1,12 @@ description: Non-refundable tax credits which precede the Elderly or Disabled Credit. values: + # Section 25D(c) orders the residential clean energy credit after these. 2013-01-01: - - foreign_tax_credit + - foreign_tax_credit - cdcc - non_refundable_american_opportunity_credit - lifetime_learning_credit - savers_credit - - residential_clean_energy_credit - energy_efficient_home_improvement_credit metadata: @@ -16,3 +16,7 @@ metadata: reference: - title: IRS Schedule 3, Additional Credits and Payments href: https://www.irs.gov/pub/irs-pdf/f1040s3.pdf + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c + - title: 2025 Instructions for Schedule R (Form 1040), Credit Limit Worksheet, line 2 + href: https://www.irs.gov/pub/irs-pdf/i1040sr.pdf#page=4 diff --git a/policyengine_us/parameters/gov/irs/credits/energy_efficient_home_improvement/preceding_credits.yaml b/policyengine_us/parameters/gov/irs/credits/energy_efficient_home_improvement/preceding_credits.yaml index 5bc4cab8012..5cd5596097c 100644 --- a/policyengine_us/parameters/gov/irs/credits/energy_efficient_home_improvement/preceding_credits.yaml +++ b/policyengine_us/parameters/gov/irs/credits/energy_efficient_home_improvement/preceding_credits.yaml @@ -1,12 +1,12 @@ description: Non-refundable tax credits which precede the Energy Efficient Home Improvement Credit. values: + # Section 25D(c) orders the residential clean energy credit after these. 2013-01-01: - - foreign_tax_credit + - foreign_tax_credit - cdcc - non_refundable_american_opportunity_credit - lifetime_learning_credit - savers_credit - - residential_clean_energy_credit metadata: unit: list @@ -15,3 +15,7 @@ metadata: reference: - title: IRS Schedule 3, Additional Credits and Payments href: https://www.irs.gov/pub/irs-pdf/f1040s3.pdf + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c + - title: 2025 Instructions for Form 5695, Energy Efficient Home Improvement Credit Limit Worksheet, line 2 + href: https://www.irs.gov/pub/irs-pdf/i5695.pdf#page=7 diff --git a/policyengine_us/parameters/gov/irs/credits/residential_clean_energy/preceding_credits.yaml b/policyengine_us/parameters/gov/irs/credits/residential_clean_energy/preceding_credits.yaml index 3a8035d05e5..89841582ab1 100644 --- a/policyengine_us/parameters/gov/irs/credits/residential_clean_energy/preceding_credits.yaml +++ b/policyengine_us/parameters/gov/irs/credits/residential_clean_energy/preceding_credits.yaml @@ -1,16 +1,39 @@ -description: Non-refundable tax credits which precede the Residential Clean Energy Credit. +description: Non-refundable tax credits which precede the Residential Clean Energy Credit. Section 25D(c) orders this credit after every other credit in subpart A; the Child Tax Credit's share is handled in residential_clean_energy_credit_credit_limit. values: 2013-01-01: - - foreign_tax_credit + - foreign_tax_credit - cdcc - non_refundable_american_opportunity_credit - lifetime_learning_credit - savers_credit + - energy_efficient_home_improvement_credit + - elderly_disabled_credit + # The Child and Dependent Care Credit was refundable for 2021 only. + 2021-01-01: + - foreign_tax_credit + - non_refundable_american_opportunity_credit + - lifetime_learning_credit + - savers_credit + - energy_efficient_home_improvement_credit + - elderly_disabled_credit + - new_clean_vehicle_credit + 2022-01-01: + - foreign_tax_credit # Schedule 3, line 1 + - cdcc # Schedule 3, line 2 + - non_refundable_american_opportunity_credit # Schedule 3, line 3 + - lifetime_learning_credit # Schedule 3, line 3 + - savers_credit # Schedule 3, line 4 + - energy_efficient_home_improvement_credit # Form 5695, line 32 (line 30 in 2022) + - elderly_disabled_credit # Schedule 3, line 6d + - new_clean_vehicle_credit # Schedule 3, line 6f + - used_clean_vehicle_credit # Schedule 3, line 6m metadata: unit: list period: year label: Non-refundable tax credits preceding the Residential Clean Energy Credit reference: - - title: IRS Schedule 3, Additional Credits and Payments - href: https://www.irs.gov/pub/irs-pdf/f1040s3.pdf + - title: 26 U.S. Code § 25D(c) - Carryforward of unused credit + href: https://www.law.cornell.edu/uscode/text/26/25D#c + - title: 2025 Instructions for Form 5695, Residential Clean Energy Credit Limit Worksheet, line 2 + href: https://www.irs.gov/pub/irs-pdf/i5695.pdf#page=5 diff --git a/policyengine_us/parameters/gov/states/co/tax/income/credits/ctc/federal_credit_limitation_after_worksheet_b.yaml b/policyengine_us/parameters/gov/states/co/tax/income/credits/ctc/federal_credit_limitation_after_worksheet_b.yaml new file mode 100644 index 00000000000..1df02124352 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/tax/income/credits/ctc/federal_credit_limitation_after_worksheet_b.yaml @@ -0,0 +1,15 @@ +description: Colorado limits its replica of the federal Child Tax Credit by Schedule 8812 line 13, which reflects Credit Limit Worksheet B, if this is true; otherwise Colorado subtracts its own list of federal credits from federal income tax. +values: + # DR 0104CN lines 4-6 subtract listed credits from Form 1040 line 18. + 2022-01-01: false + # DR 0104CN line 4 is Schedule 8812 line 13 (Credit Limit Worksheet A, line 5). + 2023-01-01: true +metadata: + unit: bool + period: year + label: Colorado federal CTC limitation after Credit Limit Worksheet B + reference: + - title: 2022 Colorado Child Tax Credit (DR 0104CN), lines 4-6 + href: https://tax.colorado.gov/sites/tax/files/documents/DR_0104CN_2022.pdf#page=1 + - title: 2023 Colorado Child Tax Credit (DR 0104CN), line 4 instructions + href: https://tax.colorado.gov/sites/tax/files/documents/DR0104CN_2023.pdf#page=2 diff --git a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py index 225697a4d0d..b858c81db6a 100644 --- a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py +++ b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py @@ -199,22 +199,35 @@ def formula(person, period, parameters): return pre_baby_bonus_amount + baby_bonus_amount def modify_parameters(parameters): - parameters.gov.irs.credits.non_refundable.update( + credits = parameters.gov.irs.credits + credits_before_residential_clean_energy = [ + "cdcc", + "elderly_disabled_credit", + "non_refundable_american_opportunity_credit", + "lifetime_learning_credit", + "savers_credit", + "energy_efficient_home_improvement_credit", + "new_clean_vehicle_credit", + "used_clean_vehicle_credit", + "other_dependent_credit", + ] + credits.non_refundable.update( start=instant("2025-01-01"), stop=instant("2039-12-31"), - value=[ - "cdcc", - "elderly_disabled_credit", - "non_refundable_american_opportunity_credit", - "lifetime_learning_credit", - "savers_credit", - "residential_clean_energy_credit", - "energy_efficient_home_improvement_credit", - "new_clean_vehicle_credit", - "used_clean_vehicle_credit", - "other_dependent_credit", - ], + value=credits_before_residential_clean_energy + + ["residential_clean_energy_credit"], ) + # 26 U.S.C. 25D(c) orders the residential clean energy credit after + # every other credit, including the separate other dependent credit. + for preceding_credits in ( + credits.ctc_tax_liability_limit.preceding_credits, + credits.residential_clean_energy.preceding_credits, + ): + preceding_credits.update( + start=instant("2025-01-01"), + stop=instant("2039-12-31"), + value=credits_before_residential_clean_energy, + ) parameters.gov.irs.credits.ctc.refundable.fully_refundable.update( start=instant("2025-01-01"), stop=instant("2039-12-31"), diff --git a/policyengine_us/reforms/crfb/non_refundable_ss_credit.py b/policyengine_us/reforms/crfb/non_refundable_ss_credit.py index 66753340c08..959b2108285 100644 --- a/policyengine_us/reforms/crfb/non_refundable_ss_credit.py +++ b/policyengine_us/reforms/crfb/non_refundable_ss_credit.py @@ -101,6 +101,28 @@ def modify_parameters(parameters): "ss_credit", ], ) + # The credit precedes the Child Tax Credit and, like every credit, + # the residential clean energy credit (26 U.S.C. 25D(c)). + credits = parameters.gov.irs.credits + for preceding_credits in ( + credits.ctc_tax_liability_limit.preceding_credits, + credits.residential_clean_energy.preceding_credits, + ): + preceding_credits.update( + start=instant("2026-01-01"), + value=[ + "foreign_tax_credit", + "cdcc", + "non_refundable_american_opportunity_credit", + "lifetime_learning_credit", + "savers_credit", + "energy_efficient_home_improvement_credit", + "elderly_disabled_credit", + "new_clean_vehicle_credit", + "used_clean_vehicle_credit", + "ss_credit", + ], + ) return parameters class reform(Reform): diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/credit_limit_worksheets_integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/credit_limit_worksheets_integration.yaml new file mode 100644 index 00000000000..f107dc98300 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/credit_limit_worksheets_integration.yaml @@ -0,0 +1,185 @@ +# Schedule 8812 Credit Limit Worksheets A and B with the residential clean +# energy credit (Form 5695, Part I), following the 2025 instructions: +# https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=4 (Schedule 8812) +# https://www.irs.gov/pub/irs-pdf/i5695.pdf#page=5 (Form 5695, line 14) +# +# Every case is a married couple in Texas with $60,000 of wages in 2025: +# Form 1040, line 18: taxable income 60,000 - 31,500 = 28,500; +# tax = 10% x 23,850 + 12% x 4,650 = 2,943. +# Each qualifying child carries a $2,200 CTC, of which up to $1,700 can be +# refunded; the earned income phase-in is 15% x (60,000 - 2,500) = 8,625. + +- name: Two children; the residential clean energy credit is limited and displaces the refundable CTC. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + spouse: + age: 40 + child1: + age: 5 + child2: + age: 8 + tax_units: + tax_unit: + members: [head, spouse, child1, child2] + solar_electric_property_expenditures: 10_000 + households: + household: + members: [head, spouse, child1, child2] + state_code: TX + output: + income_tax_before_credits: 2_943 + # Credit Limit Worksheet A, lines 1-3: no line 2 credits. + ctc_tax_liability_after_preceding_credits: 2_943 + # Credit Limit Worksheet B: line 1 = 4,400; line 2 = 3,400; line 12 = 8,625; + # line 13 = min(3,400, 8,625) = 3,400; line 14 = 4,400 - 3,400 = 1,000. + ctc_credit_limit_worksheet_b_applies: true + ctc_non_refundable_minimum: 1_000 + # Form 5695, line 14 worksheet: 2,943 - 1,000 = 1,943. The credit before + # the limit is 30% x 10,000 = 3,000, so 1,943 is allowed and 1,057 carries + # forward. + residential_clean_energy_credit_potential: 3_000 + residential_clean_energy_credit_credit_limit: 1_943 + residential_clean_energy_credit: 1_943 + # Worksheet B, line 15 = 1,943 = Worksheet A, line 4; line 5 = 2,943 - 1,943. + ctc_limiting_tax_liability: 1_000 + # Schedule 8812, line 14 = min(4,400, 1,000); line 27 = min(4,400 - 1,000, + # 3,400, 8,625). + non_refundable_ctc: 1_000 + refundable_ctc: 3_400 + income_tax_capped_non_refundable_credits: 2_943 + income_tax_before_refundable_credits: 0 + +- name: Two children; the residential clean energy credit is not limited but still reduces the CTC limit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + spouse: + age: 40 + child1: + age: 5 + child2: + age: 8 + tax_units: + tax_unit: + members: [head, spouse, child1, child2] + solar_electric_property_expenditures: 2_000 + households: + household: + members: [head, spouse, child1, child2] + state_code: TX + output: + # Form 5695: the credit of 30% x 2,000 = 600 fits under the 1,943 limit. + residential_clean_energy_credit_credit_limit: 1_943 + residential_clean_energy_credit: 600 + # Worksheet A, line 5 = 2,943 - 600. + ctc_limiting_tax_liability: 2_343 + # Schedule 8812, line 14 = 2,343; line 27 = min(4,400 - 2,343, 3,400, 8,625). + # Without the residential clean energy credit, line 27 would be 1,457. + non_refundable_ctc: 2_343 + refundable_ctc: 2_057 + income_tax_capped_non_refundable_credits: 2_943 + +- name: Two children and no residential clean energy credit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + spouse: + age: 40 + child1: + age: 5 + child2: + age: 8 + tax_units: + tax_unit: + members: [head, spouse, child1, child2] + households: + household: + members: [head, spouse, child1, child2] + state_code: TX + output: + ctc_limiting_tax_liability: 2_943 + # Schedule 8812, line 27 = min(4,400 - 2,943, 3,400, 8,625). + refundable_ctc: 1_457 + non_refundable_ctc: 2_943 + +- name: Credit for other dependents only; Worksheet B does not apply, so the CTC precedes the residential clean energy credit in full. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + spouse: + age: 40 + dependent: + age: 17 # Too old for the CTC; qualifies for the $500 credit for other dependents. + tax_units: + tax_unit: + members: [head, spouse, dependent] + solar_electric_property_expenditures: 10_000 + households: + household: + members: [head, spouse, dependent] + state_code: TX + output: + income_tax_before_credits: 2_943 + ctc: 500 + ctc_credit_limit_worksheet_b_applies: false + # Worksheet A, line 4 is zero: the limit is line 3. + ctc_limiting_tax_liability: 2_943 + non_refundable_ctc: 500 + refundable_ctc: 0 + # Form 5695, line 14 worksheet: 2,943 - 500 (Form 1040, line 19) = 2,443. + residential_clean_energy_credit_credit_limit: 2_443 + residential_clean_energy_credit: 2_443 + income_tax_capped_non_refundable_credits: 2_943 + +- name: The energy efficient home improvement credit precedes both the CTC and the residential clean energy credit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + spouse: + age: 40 + child1: + age: 5 + child2: + age: 8 + tax_units: + tax_unit: + members: [head, spouse, child1, child2] + solar_electric_property_expenditures: 10_000 + energy_efficient_home_improvement_credit_potential: 300 + households: + household: + members: [head, spouse, child1, child2] + state_code: TX + output: + # Form 5695, line 31 worksheet: nothing on Schedule 3, lines 1-4, 6d or 6l. + energy_efficient_home_improvement_credit: 300 + # Credit Limit Worksheet A, line 3 = 2,943 - 300 (Schedule 3, line 5b). + ctc_tax_liability_after_preceding_credits: 2_643 + # Form 5695, line 14 worksheet: 2,943 - 300 - 1,000 (Worksheet B, line 14). + residential_clean_energy_credit: 1_643 + # Worksheet A, line 5 = 2,643 - 1,643. + ctc_limiting_tax_liability: 1_000 + refundable_ctc: 3_400 + # 300 + 1,643 + 1,000 = 2,943. + income_tax_capped_non_refundable_credits: 2_943 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml new file mode 100644 index 00000000000..4b7b2ec6e97 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml @@ -0,0 +1,43 @@ +- name: Applies with a qualifying child. + period: 2025 + input: + ctc_qualifying_children: 1 + output: + ctc_credit_limit_worksheet_b_applies: true + +- name: Does not apply without a qualifying child (Schedule 8812, line 4 is zero). + period: 2025 + input: + ctc_qualifying_children: 0 + output: + ctc_credit_limit_worksheet_b_applies: false + +- name: Does not apply to filers of Form 2555. + period: 2025 + input: + ctc_qualifying_children: 2 + foreign_earned_income_exclusion: 10_000 + output: + ctc_credit_limit_worksheet_b_applies: false + +- name: Does not apply in 2021, when the CTC was fully refundable. + period: 2021 + input: + ctc_qualifying_children: 2 + output: + ctc_credit_limit_worksheet_b_applies: false + +- name: Applies before 2021 too (2020 Publication 972, Line 14 Worksheet). + period: 2020 + input: + ctc_qualifying_children: 1 + output: + ctc_credit_limit_worksheet_b_applies: true + +- name: Before 2015 Worksheet B still applies to Form 2555 filers (the section 911 bar starts in 2015). + period: 2014 + input: + ctc_qualifying_children: 2 + foreign_earned_income_exclusion: 10_000 + output: + ctc_credit_limit_worksheet_b_applies: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.yaml index 0478b6d0c42..ee45af5c291 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.yaml @@ -6,3 +6,31 @@ non_refundable_ctc: 10 output: ctc_limiting_tax_liability: 80 + +- name: Credit Limit Worksheet B applies, so the residential clean energy credit reduces the limit. + period: 2025 + input: + ctc_tax_liability_after_preceding_credits: 2_943 # Worksheet A, line 3 + ctc_credit_limit_worksheet_b_applies: true + residential_clean_energy_credit: 1_943 # Worksheet B, line 15 + output: + ctc_limiting_tax_liability: 1_000 # Worksheet A, line 5 + +- name: Without Credit Limit Worksheet B, the residential clean energy credit does not reduce the limit. + period: 2025 + input: + ctc_tax_liability_after_preceding_credits: 2_943 + ctc_credit_limit_worksheet_b_applies: false + residential_clean_energy_credit: 1_943 + output: + ctc_limiting_tax_liability: 2_943 + +- name: A reform that empties the list of subsequent credits leaves the limit at line 3. + period: 2025 + input: + gov.irs.credits.ctc_tax_liability_limit.subsequent_credits: [] + ctc_tax_liability_after_preceding_credits: 2_943 + ctc_credit_limit_worksheet_b_applies: true + residential_clean_energy_credit: 1_943 + output: + ctc_limiting_tax_liability: 2_943 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_non_refundable_minimum.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_non_refundable_minimum.yaml new file mode 100644 index 00000000000..cab306c78a8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_non_refundable_minimum.yaml @@ -0,0 +1,26 @@ +- name: The refundable maximum binds. + period: 2025 + input: + ctc: 4_400 # Credit Limit Worksheet B, line 1 + ctc_refundable_maximum: 3_400 # Line 2: two children at $1,700 + ctc_phase_in: 8_625 # Line 12: 15% x ($60,000 - $2,500) + output: + ctc_non_refundable_minimum: 1_000 # Line 14: 4,400 - min(3,400, 8,625) + +- name: The earned income phase-in binds. + period: 2025 + input: + ctc: 4_400 + ctc_refundable_maximum: 3_400 + ctc_phase_in: 1_000 + output: + ctc_non_refundable_minimum: 3_400 # 4,400 - min(3,400, 1,000) + +- name: Zero when the whole credit could be refunded. + period: 2025 + input: + ctc: 1_000 + ctc_refundable_maximum: 1_700 + ctc_phase_in: 5_000 + output: + ctc_non_refundable_minimum: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_tax_liability_after_preceding_credits.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_tax_liability_after_preceding_credits.yaml new file mode 100644 index 00000000000..2304ab2ae24 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_tax_liability_after_preceding_credits.yaml @@ -0,0 +1,52 @@ +- name: Subtracts the Credit Limit Worksheet A line 2 credits, but not the residential clean energy credit. + period: 2025 + input: + income_tax_before_credits: 5_000 + foreign_tax_credit: 100 # Schedule 3, line 1 + cdcc: 200 # Schedule 3, line 2 + non_refundable_american_opportunity_credit: 300 # Schedule 3, line 3 + savers_credit: 400 # Schedule 3, line 4 + energy_efficient_home_improvement_credit: 500 # Schedule 3, line 5b + elderly_disabled_credit: 600 # Schedule 3, line 6d + new_clean_vehicle_credit: 700 # Schedule 3, line 6f + used_clean_vehicle_credit: 800 # Schedule 3, line 6m + residential_clean_energy_credit: 900 # Schedule 3, line 5a: not subtracted. + output: + # 5,000 - (100 + 200 + 300 + 400 + 500 + 600 + 700 + 800) = 1,400 + ctc_tax_liability_after_preceding_credits: 1_400 + +- name: Does not fall below zero. + period: 2025 + input: + income_tax_before_credits: 1_000 + cdcc: 2_000 + output: + ctc_tax_liability_after_preceding_credits: 0 + +- name: The Child and Dependent Care Credit does not precede the CTC in 2021, when it was refundable. + period: 2021 + input: + income_tax_before_credits: 3_000 + cdcc: 1_000 + elderly_disabled_credit: 500 + output: + ctc_tax_liability_after_preceding_credits: 2_500 + +- name: The Child and Dependent Care Credit precedes the CTC in 2020. + period: 2020 + input: + income_tax_before_credits: 3_000 + cdcc: 500 + savers_credit: 200 + residential_clean_energy_credit: 1_000 + output: + ctc_tax_liability_after_preceding_credits: 2_300 + +- name: A reform that empties the list of preceding credits leaves income tax before credits. + period: 2025 + input: + gov.irs.credits.ctc_tax_liability_limit.preceding_credits: [] + income_tax_before_credits: 3_000 + cdcc: 500 + output: + ctc_tax_liability_after_preceding_credits: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.yaml new file mode 100644 index 00000000000..22309321d1f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.yaml @@ -0,0 +1,58 @@ +# Form 5695, Residential Clean Energy Credit Limit Worksheet (2025 instructions): +# https://www.irs.gov/pub/irs-pdf/i5695.pdf#page=5 + +- name: With Credit Limit Worksheet B, only the CTC that cannot be refunded precedes the credit. + period: 2025 + input: + income_tax_before_credits: 2_943 + ctc: 4_400 + ctc_credit_limit_worksheet_b_applies: true + ctc_non_refundable_minimum: 1_000 # Credit Limit Worksheet B, line 14 + output: + residential_clean_energy_credit_credit_limit: 1_943 + +- name: Without Credit Limit Worksheet B, the non-refundable CTC precedes the credit (Form 1040, line 19). + period: 2025 + input: + income_tax_before_credits: 2_943 + ctc: 500 + ctc_credit_limit_worksheet_b_applies: false + output: + residential_clean_energy_credit_credit_limit: 2_443 + +- name: Without Credit Limit Worksheet B, a CTC above the liability leaves nothing. + period: 2025 + input: + income_tax_before_credits: 2_943 + ctc: 4_000 + ctc_credit_limit_worksheet_b_applies: false + output: + residential_clean_energy_credit_credit_limit: 0 + +- name: Every other non-refundable credit precedes the credit. + period: 2025 + input: + income_tax_before_credits: 5_000 + foreign_tax_credit: 100 + cdcc: 200 + non_refundable_american_opportunity_credit: 300 + savers_credit: 400 + energy_efficient_home_improvement_credit: 500 + elderly_disabled_credit: 600 + new_clean_vehicle_credit: 700 + used_clean_vehicle_credit: 800 + ctc: 0 + output: + # 5,000 - (100 + 200 + 300 + 400 + 500 + 600 + 700 + 800) + residential_clean_energy_credit_credit_limit: 1_400 + +- name: In 2021, only the CTC that is not refunded precedes the credit, and the refundable CDCC does not. + period: 2021 + input: + income_tax_before_credits: 3_000 + cdcc: 1_000 + ctc: 6_500 + refundable_ctc: 6_000 + output: + # 3,000 - (6,500 - 6,000) + residential_clean_energy_credit_credit_limit: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml index 4ccce0a8e92..c3e17edd41b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/deductions/al_federal_income_tax_deduction.yaml @@ -172,8 +172,9 @@ # (Pub. 972 p.7) uses the CDCC (Form 2441) BEFORE the CTC when computing the # CTC's tax-liability limit, and Schedule 8812 then routes the unused CTC to # the ACTC. The recompute branch pins the CDCC out of the refundable list but - # must ALSO add it to the non-refundable list read by ctc_limiting_tax_liability - # so the recomputed CDCC reduces that limit. This is the one case that makes the + # must ALSO add it to the list of credits preceding the CTC + # (gov.irs.credits.ctc_tax_liability_limit.preceding_credits) so the + # recomputed CDCC reduces that limit. This is the one case that makes the # ordering bind: line_6 floors at 0, so the extra ACTC (not offset by line_6) # flows straight into Part II and cancels the NIIT. # diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.yaml index c410b67d76f..79de1385189 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.yaml @@ -3,7 +3,7 @@ input: state_code: CO co_federal_ctc_maximum: 1_000 - ctc_limiting_tax_liability: 999 + ctc_tax_liability_after_preceding_credits: 999 output: co_non_refundable_ctc: 999 @@ -12,6 +12,34 @@ input: state_code: CO co_federal_ctc_maximum: 998 - ctc_limiting_tax_liability: 1_000 + ctc_tax_liability_after_preceding_credits: 1_000 output: co_non_refundable_ctc: 998 + +- name: The Colorado replica does not subtract the residential clean energy credit. + # 2022 DR 0104CN lines 4 - 6 reproduce Schedule 8812 Credit Limit Worksheet A, + # lines 1 - 3, without Credit Limit Worksheet B. + period: 2022 + input: + state_code: CO + co_federal_ctc_maximum: 2_000 + ctc_tax_liability_after_preceding_credits: 1_500 + ctc_credit_limit_worksheet_b_applies: true + residential_clean_energy_credit: 1_000 + output: + ctc_limiting_tax_liability: 500 + co_non_refundable_ctc: 1_500 + +- name: From 2023 the Colorado limit is Schedule 8812 line 13, after Credit Limit Worksheet B. + # 2023 DR 0104CN line 4: "If you completed Federal Schedule 8812, enter the + # amount from line 13 of that form." + period: 2023 + input: + state_code: CO + co_federal_ctc_maximum: 2_000 + ctc_tax_liability_after_preceding_credits: 1_500 + ctc_credit_limit_worksheet_b_applies: true + residential_clean_energy_credit: 1_000 + output: + ctc_limiting_tax_liability: 500 + co_non_refundable_ctc: 500 diff --git a/policyengine_us/tests/test_ctc_credit_limit_worksheets.py b/policyengine_us/tests/test_ctc_credit_limit_worksheets.py new file mode 100644 index 00000000000..5285ed1a2e2 --- /dev/null +++ b/policyengine_us/tests/test_ctc_credit_limit_worksheets.py @@ -0,0 +1,244 @@ +"""Property and differential tests for the CTC's tax-liability limit. + +The model follows Schedule 8812 Credit Limit Worksheets A and B and the +Form 5695 Residential Clean Energy Credit Limit Worksheet. These tests check +it against the statute applied directly, which never mentions the worksheets: + +- 26 U.S.C. 26(a) limits the aggregate of the subpart A credits to the tax. +- 26 U.S.C. 24(d)(1) refunds the lesser of the CTC (capped per qualifying + child by 24(h)(5): $1,400, indexed for inflation, or $1,700 for 2025) and + the amount by which the aggregate subpart A credits would increase if the + 26(a) limit rose by the earned income phase-in amount; the refunded amount + reduces the non-refundable CTC. +- 26 U.S.C. 25D(c) orders the residential clean energy credit after every + other subpart A credit. + +Households are married or single filers in Texas who take the standard +deduction, so the CTC limit's no-SALT liability equals actual liability. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2025 + +OUTPUTS = [ + "income_tax_before_credits", + "tax_unit_itemizes", + "ctc", + "ctc_refundable_maximum", + "ctc_phase_in", + "energy_efficient_home_improvement_credit_potential", + "energy_efficient_home_improvement_credit", + "residential_clean_energy_credit_potential", + "residential_clean_energy_credit_credit_limit", + "residential_clean_energy_credit", + "ctc_tax_liability_after_preceding_credits", + "ctc_credit_limit_worksheet_b_applies", + "ctc_non_refundable_minimum", + "ctc_limiting_tax_liability", + "refundable_ctc", + "non_refundable_ctc", + "income_tax_capped_non_refundable_credits", + "income_tax_before_refundable_credits", +] + + +def build_situation(households): + people, tax_units, units = {}, {}, {} + groups = {"households": {}, "spm_units": {}, "families": {}} + for i, h in enumerate(households): + head = f"head_{i}" + people[head] = { + "age": {YEAR: 40}, + "employment_income": {YEAR: h["wages"]}, + "taxable_interest_income": {YEAR: h["interest"]}, + } + members = [head] + units[f"marital_unit_{i}"] = {"members": [head]} + if h["married"]: + spouse = f"spouse_{i}" + people[spouse] = {"age": {YEAR: 40}} + members.append(spouse) + units[f"marital_unit_{i}"]["members"].append(spouse) + for j, age in enumerate(h["dependent_ages"]): + dependent = f"dependent_{i}_{j}" + people[dependent] = {"age": {YEAR: age}} + members.append(dependent) + units[f"marital_unit_{i}_{j}"] = {"members": [dependent]} + tax_units[f"tax_unit_{i}"] = { + "members": members, + "solar_electric_property_expenditures": {YEAR: h["solar"]}, + "energy_efficient_home_improvement_credit_potential": { + YEAR: h["home_improvement_credit"] + }, + } + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {YEAR: "TX"}, + } + groups["spm_units"][f"spm_unit_{i}"] = {"members": members} + groups["families"][f"family_{i}"] = {"members": members} + return { + "people": people, + "tax_units": tax_units, + "marital_units": units, + **groups, + } + + +def calculate(households): + simulation = Simulation(situation=build_situation(households)) + return { + variable: np.asarray(simulation.calculate(variable, YEAR)) + for variable in OUTPUTS + } + + +def statutory_reference(r): + """Apply 26 U.S.C. 24(d)(1), 25D(c) and 26(a) directly. + + The inputs this change does not touch come from the model: income tax + before credits, each credit before the 26(a) limit, the per-child refund + cap and the earned income (or Social Security tax) increase. + """ + liability = r["income_tax_before_credits"] + # The only other credit in these households is the energy efficient home + # improvement credit, which precedes both (Form 5695, line 31). + prior_potential = r["energy_efficient_home_improvement_credit_potential"] + prior = r["energy_efficient_home_improvement_credit"] + ctc = r["ctc"] + potential = r["residential_clean_energy_credit_potential"] + + def aggregate(limit): + # Subpart A credits allowed under a 26(a) limit, before 24(d). + return np.minimum(np.maximum(limit, 0), prior_potential + ctc + potential) + + increase = aggregate(liability + r["ctc_phase_in"]) - aggregate(liability) + refundable = np.minimum(np.minimum(r["ctc_refundable_maximum"], ctc), increase) + non_refundable_ctc = ctc - refundable + liability_after_prior = np.maximum(liability - prior, 0) + ctc_used = np.minimum(non_refundable_ctc, liability_after_prior) + residential_clean_energy = np.minimum(potential, liability_after_prior - ctc_used) + return { + "refundable_ctc": refundable, + "residential_clean_energy_credit": residential_clean_energy, + "income_tax_capped_non_refundable_credits": ( + prior + ctc_used + residential_clean_energy + ), + } + + +def assert_matches_statute(r): + assert not r["tax_unit_itemizes"].any() + expected = statutory_reference(r) + for variable, values in expected.items(): + np.testing.assert_allclose(r[variable], values, atol=0.01, err_msg=variable) + + +def assert_invariants(r): + tol = 0.01 + line_3 = r["ctc_tax_liability_after_preceding_credits"] + line_5 = r["ctc_limiting_tax_liability"] + rce = r["residential_clean_energy_credit"] + worksheet_b = r["ctc_credit_limit_worksheet_b_applies"] + # Worksheet A: 0 <= line 5 <= line 3 <= income tax before credits. + assert (line_5 >= 0).all() + assert (line_5 <= line_3 + tol).all() + assert (line_3 <= np.maximum(r["income_tax_before_credits"], 0) + tol).all() + # With Worksheet B, line 5 = line 3 - the residential clean energy credit; + # without it, line 5 = line 3. + np.testing.assert_allclose( + line_5, np.where(worksheet_b, line_3 - rce, line_3), atol=tol + ) + # The residential clean energy credit never exceeds its potential or limit. + assert (rce <= r["residential_clean_energy_credit_potential"] + tol).all() + assert (rce <= r["residential_clean_energy_credit_credit_limit"] + tol).all() + # Section 24(d)(1): the refund never exceeds the per-child cap, the credit + # or the phase-in amount. + cap = np.minimum( + np.minimum(r["ctc_refundable_maximum"], r["ctc"]), + np.maximum(r["ctc_phase_in"], 0), + ) + assert (r["refundable_ctc"] <= cap + tol).all() + # Section 26(a): non-refundable credits never exceed the tax. + assert ( + r["income_tax_capped_non_refundable_credits"] + <= r["income_tax_before_credits"] + tol + ).all() + + +GRID = [ + { + "married": married, + "dependent_ages": ages, + "wages": wages, + "interest": interest, + "solar": solar, + "home_improvement_credit": home_improvement_credit, + } + for married in (True, False) + for ages in ([], [5], [5, 8], [3, 6, 9], [17], [5, 17]) + for wages in (0, 12_000, 30_000, 60_000, 120_000) + for interest in (0, 40_000) + for solar in (0, 2_000, 10_000) + for home_improvement_credit in (0, 300) +] + + +def test_grid_matches_statute_and_invariants(): + r = calculate(GRID) + assert_matches_statute(r) + assert_invariants(r) + # The grid exercises every branch of Worksheet B. + assert r["ctc_credit_limit_worksheet_b_applies"].any() + assert (~r["ctc_credit_limit_worksheet_b_applies"] & (r["ctc"] > 0)).any() + limited = r["residential_clean_energy_credit"] < ( + r["residential_clean_energy_credit_potential"] - 1 + ) + assert (limited & r["ctc_credit_limit_worksheet_b_applies"]).any() + + +household_strategy = st.fixed_dictionaries( + { + "married": st.booleans(), + "dependent_ages": st.lists(st.integers(0, 18), max_size=4), + "wages": st.integers(0, 200_000), + "interest": st.integers(0, 100_000), + "solar": st.integers(0, 40_000), + "home_improvement_credit": st.integers(0, 1_200), + } +) + + +@settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(household_strategy, min_size=1, max_size=25)) +def test_random_households_match_statute_and_invariants(households): + r = calculate(households) + assert_matches_statute(r) + assert_invariants(r) + + +@settings( + max_examples=8, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + st.lists(household_strategy, min_size=1, max_size=15), + st.integers(1, 20_000), +) +def test_more_residential_clean_energy_spending_never_raises_tax(households, extra): + # Each household appears twice: with its spending and with more. + more = [{**h, "solar": h["solar"] + extra} for h in households] + r = calculate(households + more) + tax = r["income_tax_before_refundable_credits"] - r["refundable_ctc"] + n = len(households) + assert (tax[n:] <= tax[:n] + 0.01).all() diff --git a/policyengine_us/tests/test_ctc_itemizing_branch_cycle.py b/policyengine_us/tests/test_ctc_itemizing_branch_cycle.py index 9d797ce7b43..9ba59171672 100644 --- a/policyengine_us/tests/test_ctc_itemizing_branch_cycle.py +++ b/policyengine_us/tests/test_ctc_itemizing_branch_cycle.py @@ -3,7 +3,8 @@ Calling `refundable_ctc` on an `itemizing` reform branch used to form a dependency cycle - refundable_ctc -> ctc_limiting_tax_liability -> (no_salt branch) + refundable_ctc -> ctc_limiting_tax_liability + -> ctc_tax_liability_after_preceding_credits -> (no_salt branch) -> income_tax_before_credits -> taxable_income -> tax_unit_itemizes -> tax_liability_if_itemizing -> (itemizing branch) -> income_tax -> income_tax_before_refundable_credits @@ -15,7 +16,7 @@ consumers (e.g. policyengine.py's household-impact integration tests) on `policyengine-core >= 3.24`. -The fix in `ctc_limiting_tax_liability.py` propagates the parent's +The fix in `ctc_tax_liability_after_preceding_credits.py` propagates the parent's `tax_unit_itemizes` value to the no_salt child branch so the `tax_unit_itemizes` formula is never re-entered there. """ diff --git a/policyengine_us/tests/test_pinned_branch_credit_dependents.py b/policyengine_us/tests/test_pinned_branch_credit_dependents.py new file mode 100644 index 00000000000..2cec12e0506 --- /dev/null +++ b/policyengine_us/tests/test_pinned_branch_credit_dependents.py @@ -0,0 +1,56 @@ +"""Pinned-rule branches recompute credits that read the CTC. + +New York (pre-TCJA CTC) and Alabama (2020 IRC) recompute the federal CTC on a +branch under pinned rules. The residential clean energy credit's limit +subtracts the non-refundable CTC (26 U.S.C. 25D(c)), so the branch must +recompute it rather than inherit the parent's value; otherwise the result +depends on which variables were calculated first. +""" + +import pytest + +from policyengine_us import Simulation + +YEAR = 2021 + + +def situation(state_code): + members = ["head", "spouse", "child1", "child2"] + return { + "people": { + "head": { + "age": {YEAR: 40}, + "employment_income": {YEAR: 6_000}, + "taxable_interest_income": {YEAR: 40_000}, + }, + "spouse": {"age": {YEAR: 40}}, + "child1": {"age": {YEAR: 5}}, + "child2": {"age": {YEAR: 8}}, + }, + "tax_units": { + "tax_unit": { + "members": members, + "solar_electric_property_expenditures": {YEAR: 10_000}, + } + }, + "households": { + "household": {"members": members, "state_code": {YEAR: state_code}} + }, + } + + +@pytest.mark.parametrize( + "state_code, variable", + [("NY", "ny_ctc_pre_2024"), ("AL", "al_federal_income_tax_deduction")], +) +def test_pinned_branch_result_does_not_depend_on_calculation_order( + state_code, variable +): + results = [] + for first in (None, "residential_clean_energy_credit", "income_tax"): + simulation = Simulation(situation=situation(state_code)) + if first is not None: + simulation.calculate(first, YEAR) + results.append(float(simulation.calculate(variable, YEAR)[0])) + assert results[1] == pytest.approx(results[0]) + assert results[2] == pytest.approx(results[0]) diff --git a/policyengine_us/tools/pinned_tbs.py b/policyengine_us/tools/pinned_tbs.py index 0a32321b14c..ab0b7e445c1 100644 --- a/policyengine_us/tools/pinned_tbs.py +++ b/policyengine_us/tools/pinned_tbs.py @@ -27,6 +27,12 @@ # built before that reform would otherwise be reused after it. _PINNED_TBS_CACHE = {} +# Variables without "ctc", "cdcc" or "eitc" in their names that read those +# credits, so a branch that recomputes them under pinned rules must drop these +# too. The residential clean energy credit's limit subtracts the +# non-refundable CTC (26 U.S.C. 25D(c); Form 5695, line 14 worksheet). +CREDIT_DEPENDENT_VARIABLES = ("residential_clean_energy_credit",) + def _get_pinned_tbs(base_tbs, pin_name, pin_fn): entry = _PINNED_TBS_CACHE.get(pin_name) @@ -99,17 +105,24 @@ def _pin_2020_irc(tbs): pass # The 2020 CTC & ODC Worksheet (Pub. 972, p.7) uses the CDCC before the CTC # when computing the CTC's tax-liability limit, so the recomputed CDCC must - # reduce that limit inside `ctc_limiting_tax_liability`, which reads the - # non-refundable-credits list. The 2021 list omits `cdcc` (it was refundable - # under ARPA). Add `cdcc` to the current 2021 membership rather than pinning - # the whole 2020 list, which would drop `new_clean_vehicle_credit` (2021+). - try: - non_refundable_2021 = list(credits.non_refundable(start)) - if "cdcc" not in non_refundable_2021: - non_refundable_2021 = ["cdcc"] + non_refundable_2021 - credits.non_refundable.update(start=start, stop=stop, value=non_refundable_2021) - except Exception: - pass + # reduce that limit (`ctc_tax_liability_after_preceding_credits`) and the + # residential clean energy credit's limit, and count among the + # non-refundable credits. The 2021 lists omit `cdcc` (it was refundable + # under ARPA). Add `cdcc` to each current 2021 membership rather than + # pinning the whole 2020 lists, which would drop `new_clean_vehicle_credit` + # (2021+). + for credit_list in ( + credits.non_refundable, + credits.ctc_tax_liability_limit.preceding_credits, + credits.residential_clean_energy.preceding_credits, + ): + try: + members_2021 = list(credit_list(start)) + if "cdcc" not in members_2021: + members_2021 = ["cdcc"] + members_2021 + credit_list.update(start=start, stop=stop, value=members_2021) + except Exception: + pass def get_pre_arpa_eitc_tbs(base_tbs): diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py new file mode 100644 index 00000000000..07af7bbaf36 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * + + +class ctc_credit_limit_worksheet_b_applies(Variable): + value_type = bool + entity = TaxUnit + label = "Schedule 8812 Credit Limit Worksheet B applies" + documentation = ( + "Whether the filer completes Schedule 8812 Credit Limit Worksheet B, " + "which lets the residential clean energy credit (and the adoption, " + "mortgage interest and DC first-time homebuyer credits) displace the " + "part of the Child Tax Credit that can be refunded. The IRS also " + "requires the filer to claim one of those credits; that condition is " + "omitted because Worksheet B changes nothing when those credits are " + "zero." + ) + definition_period = YEAR + reference = ( + # 2025 Instructions for Schedule 8812, Credit Limit Worksheet A, line 3. + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=4", + "https://www.law.cornell.edu/uscode/text/26/24#d_1_B", + # No refundable CTC for filers excluding foreign earned income under + # section 911 (added as 24(d)(5), redesignated (d)(3) in 2018). + "https://www.law.cornell.edu/uscode/text/26/24#d_3", + ) + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.credits.ctc + if p.refundable.fully_refundable: + # In 2021, Worksheet B applied only to filers completing Part I-C, + # whose CTC was not fully refundable. + return np.zeros(tax_unit.count, dtype=bool) + has_qualifying_child = tax_unit("ctc_qualifying_children", period) > 0 + # Form 2555 filers skip Worksheet B from 2015, when the refundable CTC + # was first denied to them. + files_form_2555 = tax_unit("foreign_earned_income_exclusion", period) > 0 + excluded = ( + files_form_2555 & p.refundable.foreign_earned_income_exclusion_bar_applies + ) + return has_qualifying_child & ~excluded diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.py index 6a00eddc72c..85d6ccccbdb 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_limiting_tax_liability.py @@ -6,32 +6,36 @@ class ctc_limiting_tax_liability(Variable): entity = TaxUnit label = "CTC-limiting tax liability" unit = USD - documentation = "The tax liability used to determine the maximum amount of the non-refundable CTC. Excludes SALT from all calculations (this is an inaccuracy required to avoid circular dependencies)." + documentation = ( + "The tax liability used to determine the maximum amount of the " + "non-refundable CTC (Schedule 8812 Credit Limit Worksheet A, line 5): " + "income tax before credits less the credits that precede the CTC, " + "less the residential clean energy credit when Credit Limit Worksheet " + "B applies. Excludes SALT from income tax before credits (this is an " + "inaccuracy required to avoid circular dependencies)." + ) definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/26#a", + "https://www.law.cornell.edu/uscode/text/26/24#d_1_B", + # 2025 Instructions for Schedule 8812, Credit Limit Worksheets A and B. + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=4", + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=6", + ) def formula(tax_unit, period, parameters): - simulation = tax_unit.simulation - no_salt_branch = get_branch_for_period(simulation, "no_salt", period) - no_salt_branch.set_input("salt_deduction", period, np.zeros(tax_unit.count)) - # Propagate the parent's itemization determination so the - # no_salt branch doesn't re-enter - # `tax_unit_itemizes` -> `tax_liability_if_itemizing` -> - # `income_tax` -> `refundable_ctc`, which forms a cycle - # (issue #8059). The parent's value has already been computed - # by the time we get here: either set as input on the - # itemizing / not_itemizing branch, or computed and cached on - # the top-level sim before `refundable_ctc` was reached (the - # `income_tax_before_credits` branch of - # `income_tax_before_refundable_credits` runs first). - itemizes = tax_unit("tax_unit_itemizes", period) - no_salt_branch.set_input("tax_unit_itemizes", period, itemizes) - tax_liability_before_credits = no_salt_branch.calculate( - "income_tax_before_credits", period + liability_after_preceding_credits = tax_unit( + "ctc_tax_liability_after_preceding_credits", period + ) # Line 3 + # Section 25D(c) orders the residential clean energy credit after the + # CTC, but section 24(d)(1)(B) refunds the CTC by the increase in all + # subpart A credits. Worksheet B therefore lets that credit use the + # liability first, moving the displaced CTC into the refundable part. + p = parameters(period).gov.irs.credits.ctc_tax_liability_limit + # add() returns None for an empty list, which a reform may set. + subsequent_credits = ( + add(tax_unit, period, p.subsequent_credits) if p.subsequent_credits else 0 ) - non_refundable_credits = parameters(period).gov.irs.credits.non_refundable - non_refundable_credits_ex_ctc = [ - x for x in non_refundable_credits if x != "non_refundable_ctc" - ] - total_credits = add(tax_unit, period, non_refundable_credits_ex_ctc) - - return max_(0, tax_liability_before_credits - total_credits) + worksheet_b_applies = tax_unit("ctc_credit_limit_worksheet_b_applies", period) + worksheet_b_amount = where(worksheet_b_applies, subsequent_credits, 0) # Line 4 + return max_(0, liability_after_preceding_credits - worksheet_b_amount) # Line 5 diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_non_refundable_minimum.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_non_refundable_minimum.py new file mode 100644 index 00000000000..46810fd965e --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_non_refundable_minimum.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class ctc_non_refundable_minimum(Variable): + value_type = float + entity = TaxUnit + label = "CTC that cannot be refunded" + unit = USD + documentation = ( + "The part of the Child Tax Credit (including the credit for other " + "dependents) that exceeds the most that could be refunded, whatever " + "the tax liability (Schedule 8812 Credit Limit Worksheet B, line 14)." + ) + definition_period = YEAR + reference = ( + # 2025 Instructions for Schedule 8812, Credit Limit Worksheet B (lines + # 1-9 on page 5, lines 10-15 on page 6). + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=5", + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=6", + "https://www.law.cornell.edu/uscode/text/26/24#d_1", + ) + + def formula(tax_unit, period, parameters): + ctc = tax_unit("ctc", period) # Line 1 + refundable_maximum = tax_unit("ctc_refundable_maximum", period) # Line 2 + phase_in = tax_unit("ctc_phase_in", period) # Line 12 + most_refundable = min_(refundable_maximum, phase_in) # Line 13 + return max_(0, ctc - most_refundable) # Line 14 diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_tax_liability_after_preceding_credits.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_tax_liability_after_preceding_credits.py new file mode 100644 index 00000000000..d950fd27e22 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_tax_liability_after_preceding_credits.py @@ -0,0 +1,46 @@ +from policyengine_us.model_api import * + + +class ctc_tax_liability_after_preceding_credits(Variable): + value_type = float + entity = TaxUnit + label = "Tax liability after credits preceding the CTC" + unit = USD + documentation = ( + "Income tax before credits less the non-refundable credits that " + "precede the Child Tax Credit (Schedule 8812 Credit Limit Worksheet " + "A, line 3). Excludes SALT from income tax before credits (this is an " + "inaccuracy required to avoid circular dependencies)." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/26#a", + # 2025 Instructions for Schedule 8812, Credit Limit Worksheet A. + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=4", + ) + + def formula(tax_unit, period, parameters): + simulation = tax_unit.simulation + no_salt_branch = get_branch_for_period(simulation, "no_salt", period) + no_salt_branch.set_input("salt_deduction", period, np.zeros(tax_unit.count)) + # Propagate the parent's itemization determination so the + # no_salt branch doesn't re-enter + # `tax_unit_itemizes` -> `tax_liability_if_itemizing` -> + # `income_tax` -> `refundable_ctc`, which forms a cycle + # (issue #8059). The parent's value has already been computed + # by the time we get here: either set as input on the + # itemizing / not_itemizing branch, or computed and cached on + # the top-level sim before `refundable_ctc` was reached (the + # `income_tax_before_credits` branch of + # `income_tax_before_refundable_credits` runs first). + itemizes = tax_unit("tax_unit_itemizes", period) + no_salt_branch.set_input("tax_unit_itemizes", period, itemizes) + tax_liability_before_credits = no_salt_branch.calculate( + "income_tax_before_credits", period + ) + p = parameters(period).gov.irs.credits.ctc_tax_liability_limit + # add() returns None for an empty list, which a reform may set. + preceding_credits = ( + add(tax_unit, period, p.preceding_credits) if p.preceding_credits else 0 + ) + return max_(0, tax_liability_before_credits - preceding_credits) diff --git a/policyengine_us/variables/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.py b/policyengine_us/variables/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.py index 87ce2750b71..b16a5df70b1 100644 --- a/policyengine_us/variables/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.py +++ b/policyengine_us/variables/gov/irs/credits/residential_clean_energy/residential_clean_energy_credit_credit_limit.py @@ -6,12 +6,42 @@ class residential_clean_energy_credit_credit_limit(Variable): entity = TaxUnit label = "Residential clean energy credit credit limit" definition_period = YEAR - documentation = "Residential clean energy tax credit" + documentation = ( + "Income tax before credits less every other non-refundable credit, " + "including the non-refundable Child Tax Credit (Form 5695 Residential " + "Clean Energy Credit Limit Worksheet)." + ) unit = USD - reference = "https://www.law.cornell.edu/uscode/text/26/25D" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/25D#c", + # 2025 Instructions for Form 5695, Residential Clean Energy Credit + # Limit Worksheet. + "https://www.irs.gov/pub/irs-pdf/i5695.pdf#page=5", + ) def formula(tax_unit, period, parameters): income_tax_before_credits = tax_unit("income_tax_before_credits", period) p = parameters(period).gov.irs.credits.residential_clean_energy preceding_credits = add(tax_unit, period, p.preceding_credits) - return max_(income_tax_before_credits - preceding_credits, 0) + liability_after_preceding_credits = max_( + income_tax_before_credits - preceding_credits, 0 + ) + # The Child Tax Credit and credit for other dependents also precede + # this credit (Form 1040, line 19). When Schedule 8812 Credit Limit + # Worksheet B applies, only the part of the CTC that cannot be refunded + # counts (Worksheet B, line 14): this credit uses the liability first + # and the CTC it displaces is refunded instead. + ctc = tax_unit("ctc", period) + if parameters(period).gov.irs.credits.ctc.refundable.fully_refundable: + # The refundable CTC does not depend on tax liability in these + # years, so it can be read here without a circular dependency. + ctc_not_refunded = ctc - tax_unit("refundable_ctc", period) + else: + ctc_not_refunded = ctc + non_refundable_ctc = min_(ctc_not_refunded, liability_after_preceding_credits) + preceding_ctc = where( + tax_unit("ctc_credit_limit_worksheet_b_applies", period), + tax_unit("ctc_non_refundable_minimum", period), + non_refundable_ctc, + ) + return max_(liability_after_preceding_credits - preceding_ctc, 0) diff --git a/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py b/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py index 46a0b93b1c8..7d18b9873f2 100644 --- a/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py +++ b/policyengine_us/variables/gov/states/al/tax/income/deductions/federal_income_tax/al_federal_income_tax_deduction.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * -from policyengine_us.tools.pinned_tbs import get_2020_irc_tbs +from policyengine_us.tools.pinned_tbs import ( + CREDIT_DEPENDENT_VARIABLES, + get_2020_irc_tbs, +) class al_federal_income_tax_deduction(Variable): @@ -55,7 +58,10 @@ def formula(tax_unit, period, parameters): branch = get_branch_for_period(simulation, "al_2020_irc", period) branch.tax_benefit_system = get_2020_irc_tbs(simulation.tax_benefit_system) for variable in branch.tax_benefit_system.variables: - if any(key in variable for key in ("ctc", "cdcc", "eitc")): + if any( + key in variable + for key in ("ctc", "cdcc", "eitc", *CREDIT_DEPENDENT_VARIABLES) + ): branch.delete_arrays(variable) recomputed_ctc = branch.tax_unit("ctc", period) recomputed_refundable_ctc = branch.tax_unit("refundable_ctc", period) diff --git a/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.py b/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.py index 65bdb659695..ef731be88a2 100644 --- a/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.py +++ b/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_non_refundable_ctc.py @@ -17,13 +17,23 @@ class co_non_refundable_ctc(Variable): "https://tax.colorado.gov/sites/tax/files/documents/DR_0104CN_2022.pdf#page=1", # Colorado Individual Income Tax Filing Guide - Instructions for Select Credits from the DR 0104CR - Line 1 Child Tax Credit "https://tax.colorado.gov/sites/tax/files/documents/DR_104_Book_2022.pdf#page=16", + # 2023 Colorado Child Tax Credit, line 4 instructions + "https://tax.colorado.gov/sites/tax/files/documents/DR0104CN_2023.pdf#page=2", ) defined_for = StateCode.CO def formula(tax_unit, period, parameters): # follow 2022 DR 0104CN form and its instructions (in Book cited above): maximum = tax_unit("co_federal_ctc_maximum", period) # Line 3 - limiting_tax_liability = tax_unit( - "ctc_limiting_tax_liability", period - ) # Line 4 - 6 - return min_(maximum, limiting_tax_liability) # Line 7 + p = parameters(period).gov.states.co.tax.income.credits.ctc + if p.federal_credit_limitation_after_worksheet_b: + # 2023 line 4: Schedule 8812 line 13 (Credit Limit Worksheet A, + # line 5), after Credit Limit Worksheet B. + limiting_tax_liability = tax_unit("ctc_limiting_tax_liability", period) + else: + # 2022 lines 4 - 6: Form 1040 line 18 less listed credits, as on + # Credit Limit Worksheet A lines 1 - 3, with no Worksheet B. + limiting_tax_liability = tax_unit( + "ctc_tax_liability_after_preceding_credits", period + ) + return min_(maximum, limiting_tax_liability) # Line 7 (2022), line 5 (2023) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py index c331a3336b8..24c4fd4914c 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * -from policyengine_us.tools.pinned_tbs import get_pre_tcja_ctc_tbs +from policyengine_us.tools.pinned_tbs import ( + CREDIT_DEPENDENT_VARIABLES, + get_pre_tcja_ctc_tbs, +) class ny_ctc_pre_2024(Variable): @@ -36,7 +39,9 @@ def formula(tax_unit, period, parameters): branch_parameters = pre_tcja_ctc.tax_benefit_system.parameters # Delete all arrays from pre-TCJA CTC branch. for variable in pre_tcja_ctc.tax_benefit_system.variables: - if "ctc" in variable: + if "ctc" in variable or any( + name in variable for name in CREDIT_DEPENDENT_VARIABLES + ): pre_tcja_ctc.delete_arrays(variable) # Calculate pre-TCJA CTC. maximum_ctc = pre_tcja_ctc.calculate("ctc_child_individual_maximum", period) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py index c7c3b398e65..6506de0a399 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/credits/ctc/ny_ctc_pre_2024_eligible.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * -from policyengine_us.tools.pinned_tbs import get_pre_tcja_ctc_tbs +from policyengine_us.tools.pinned_tbs import ( + CREDIT_DEPENDENT_VARIABLES, + get_pre_tcja_ctc_tbs, +) class ny_ctc_pre_2024_eligible(Variable): @@ -34,7 +37,9 @@ def formula(tax_unit, period, parameters): ) # Delete all arrays from pre-TCJA CTC branch. for variable in pre_tcja_ctc.tax_benefit_system.variables: - if "ctc" in variable: + if "ctc" in variable or any( + name in variable for name in CREDIT_DEPENDENT_VARIABLES + ): pre_tcja_ctc.delete_arrays(variable) qualifies_for_federal_ctc = pre_tcja_ctc.person( "ctc_qualifying_child", period diff --git a/pyproject.toml b/pyproject.toml index 6c9277c6e0a..bfd691367ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "setuptools>=80.9.0", "build>=1.2.2.post1", "towncrier>=24.8.0", + "hypothesis>=6.100.0", ] [tool.hatch.build.targets.wheel] diff --git a/uv.lock b/uv.lock index e9abc858340..3b54a463ea2 100644 --- a/uv.lock +++ b/uv.lock @@ -766,6 +766,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1746,6 +1834,7 @@ dev = [ { name = "build" }, { name = "coverage" }, { name = "furo" }, + { name = "hypothesis" }, { name = "jupyter-book" }, { name = "ruff" }, { name = "setuptools" }, @@ -1757,6 +1846,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2.post1" }, { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.9.2" }, { name = "furo", marker = "extra == 'dev'", specifier = ">=2024.8.6" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100.0" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=1.0.4.post1" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "pandas", specifier = ">=3.0" },