diff --git a/changelog.d/co-early-year-guards.fixed.md b/changelog.d/co-early-year-guards.fixed.md new file mode 100644 index 00000000000..e26645c7c00 --- /dev/null +++ b/changelog.d/co-early-year-guards.fixed.md @@ -0,0 +1 @@ +Stop Colorado CCAP from reading parameters before 2015, which made every 2015 Colorado income tax calculation raise ParameterNotFoundError. Measure the Colorado sales tax refund's age test as of December 31 of the preceding year, per C.R.S. 39-22-2003(1)(a)(II). diff --git a/policyengine_us/parameters/gov/states/co/tax/income/credits/sales_tax_refund/age_threshold.yaml b/policyengine_us/parameters/gov/states/co/tax/income/credits/sales_tax_refund/age_threshold.yaml index de5407b2f2a..2cc7cb915c3 100644 --- a/policyengine_us/parameters/gov/states/co/tax/income/credits/sales_tax_refund/age_threshold.yaml +++ b/policyengine_us/parameters/gov/states/co/tax/income/credits/sales_tax_refund/age_threshold.yaml @@ -1,5 +1,6 @@ description: >- - Colorado limits its sales tax refund to filers this age or older. The refund is + Colorado limits its sales tax refund to filers this age or older as of December 31 of + the year before the tax year. The refund is attributed to the modeled eligibility tax year, even when the return and payment occur later. This is the residual sales tax refund mechanism, separate from Colorado Cash Back; in_effect controls whether the refund is available. diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_eligible.yaml index f3a235d6053..28007833960 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_child_eligible.yaml @@ -41,3 +41,14 @@ output: co_ccap_child_eligible: 2022-12: false + +- name: Child eligibility in January 2015, before the first October read. + period: 2015 + input: + is_disabled: false + is_tax_unit_dependent: true + age: 12 + state_code: CO + output: + co_ccap_child_eligible: + 2015-01: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_smi.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_smi.yaml index 6e858218787..0841f38395f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_smi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/co_ccap_smi.yaml @@ -69,3 +69,17 @@ output: co_ccap_smi: 2022-01: 1.35 * 106_120 / 12 + +# Before October 2015, the October before each month falls before the date +# parameters are backdated to, so the parameters are read at 2015-01-01. +# HHS's FY 2015 SMI for Colorado, effective 2014-10-01, still applies there. +- name: Household size 4, State CO, January and October 2015 + period: 2015 + input: + spm_unit_size: 4 + state_code_str: CO + output: + co_ccap_smi: + 2015-01: 85_182 / 12 + 2015-09: 85_182 / 12 + 2015-10: 85_915 / 12 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml index eab6ece1bb6..4db517693fa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml @@ -37,3 +37,16 @@ spm_unit_fpg: [12, 12] output: co_ccap_fpg_eligible: [false, true] + +# Before October 2015, the parameters are read at 2015-01-01, the date they +# are backdated to, rather than the October before, where none exist. +- name: In January 2015, Adams County limit is 235% of FPL. + period: 2015-01 + input: + state_code_str: [CO, CO] + co_ccap_countable_income: [28.1, 28.3] + county_str: [ADAMS_COUNTY_CO, ADAMS_COUNTY_CO] + spm_unit_fpg: [144, 144] + output: + # 144 / 12 x 2.35 = 28.20. + co_ccap_fpg_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/co_income_tax_before_non_refundable_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/co_income_tax_before_non_refundable_credits.yaml index c011646e435..e984ededfe1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/co_income_tax_before_non_refundable_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/co_income_tax_before_non_refundable_credits.yaml @@ -55,3 +55,20 @@ co_taxable_income: 0 output: co_income_tax_before_non_refundable_credits: 0 + +# Under itemization branching, the federal tax computed for itemizing reaches +# the child and dependent care credit, child care expenses and Colorado CCAP. +# For January to September 2015, CCAP once read its parameters at +# 2014-10-01, before the date to which the model backdates parameters, and +# raised ParameterNotFoundError. This case covers that itemization path and +# sets the flag so it doesn't depend on the package default. The Colorado +# CDCC reaches CCAP without branching too; co_cdcc.yaml covers that path. +- name: 2015 single filer with no income, under itemization branching + period: 2015 + absolute_error_margin: 0 + input: + gov.simulation.branch_to_determine_itemization: true + state_code: CO + filing_status: SINGLE + output: + co_income_tax_before_non_refundable_credits: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/cdcc/co_cdcc.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/cdcc/co_cdcc.yaml index be550a08624..4c9eea9f8b7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/cdcc/co_cdcc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/cdcc/co_cdcc.yaml @@ -173,3 +173,16 @@ state_code: CO output: co_cdcc: 0 + +# Without itemization branching, co_cdcc reaches CCAP through the federal CDCC +# and child care expenses. For January to September 2015, CCAP once read its +# parameters at 2014-10-01 and raised ParameterNotFoundError. +- name: 2015 single filer with no income, without itemization branching + period: 2015 + absolute_error_margin: 0 + input: + gov.simulation.branch_to_determine_itemization: false + state_code: CO + filing_status: SINGLE + output: + co_cdcc: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_eligible.yaml index 86aaad16059..928d4d7cfad 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_eligible.yaml @@ -18,7 +18,7 @@ output: co_sales_tax_refund_eligible: false -- name: Age eligible +- name: Turns 18 during the year, not age eligible period: 2022 input: people: @@ -35,6 +35,26 @@ household: members: [head] state_code: CO + output: + co_sales_tax_refund_eligible: false + +- name: Age eligible + period: 2022 + input: + people: + head: + age: 19 + is_tax_unit_head: true + irs_employment_income: 0 + tax_units: + tax_unit: + members: [head] + co_income_tax_before_non_refundable_credits: 0 + filing_status: SINGLE + households: + household: + members: [head] + state_code: CO output: co_sales_tax_refund_eligible: true @@ -87,11 +107,11 @@ input: people: head: - age: 18 + age: 19 is_tax_unit_head: true irs_employment_income: 0 spouse: - age: 18 + age: 19 is_tax_unit_spouse: true irs_employment_income: 0 tax_units: diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.yaml index 5d2324ed768..ce0049fb4f6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.yaml @@ -128,9 +128,10 @@ output: co_sales_tax_refund_person_eligible: [true, true] -# Age boundary: a filer exactly at the age threshold (18) qualifies via -# C.R.S. 39-22-2003(1)(a)(II). -- name: Head exactly age 18 -> eligible +# Age boundary: C.R.S. 39-22-2003(1)(a)(II) requires age 18 as of December 31 +# of the preceding taxable year. Born in 2004, this filer reaches 18 in 2022, the +# taxable year, and was 17 on December 31, 2021. +- name: Head turning 18 during the year -> ineligible period: 2022 input: people: @@ -146,6 +147,65 @@ household: members: [head] state_code: CO + output: + co_sales_tax_refund_person_eligible: false + +# Born in 2003, this filer was 18 on December 31, 2021. +- name: Head 18 at the end of the preceding year -> eligible + period: 2022 + input: + people: + head: + age: 19 + is_tax_unit_head: true + irs_employment_income: 0 + tax_units: + tax_unit: + members: [head] + co_income_tax_before_non_refundable_credits: 0 + households: + household: + members: [head] + state_code: CO + output: + co_sales_tax_refund_person_eligible: true + +# The 2015 Form 104 line 46 test: "born before 1997". +- name: 2015 head born in 1997 -> ineligible + period: 2015 + input: + people: + head: + age: 18 + is_tax_unit_head: true + irs_employment_income: 0 + tax_units: + tax_unit: + members: [head] + co_income_tax_before_non_refundable_credits: 0 + households: + household: + members: [head] + state_code: CO + output: + co_sales_tax_refund_person_eligible: false + +- name: 2015 head born in 1996 -> eligible + period: 2015 + input: + people: + head: + age: 19 + is_tax_unit_head: true + irs_employment_income: 0 + tax_units: + tax_unit: + members: [head] + co_income_tax_before_non_refundable_credits: 0 + households: + household: + members: [head] + state_code: CO output: co_sales_tax_refund_person_eligible: true diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py index 61bd44815bc..e6382e0de6e 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_add_on_parent_fee.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_add_on_parent_fee(Variable): @@ -13,11 +16,7 @@ class co_ccap_add_on_parent_fee(Variable): definition_period = MONTH def formula(spm_unit, period, parameters): - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap # Calculate base parent fee and add on parent fee. gross_income = spm_unit("co_ccap_countable_income", period) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py index 9393506cf4a..a8e2a3163c5 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_base_parent_fee.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_base_parent_fee(Variable): @@ -13,11 +16,7 @@ class co_ccap_base_parent_fee(Variable): definition_period = MONTH def formula(spm_unit, period, parameters): - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap # Calculate base parent fee and add on parent fee. gross_income = spm_unit("co_ccap_countable_income", period) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_eligible.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_eligible.py index 298f7fcfb4b..32b1810ad0c 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_eligible.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_child_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_child_eligible(Variable): @@ -14,11 +17,7 @@ class co_ccap_child_eligible(Variable): defined_for = StateCode.CO def formula(person, period, parameters): - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap # Section 3.105.1(F) (now 3.111(F)) extends the under-19 limit to # children under court supervision; the required documentation of care diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_parameter_instant.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parameter_instant.py new file mode 100644 index 00000000000..42c2f3cc959 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parameter_instant.py @@ -0,0 +1,22 @@ +from policyengine_us.tools.parameters import FIRST_MODELED_YEAR + + +def co_ccap_parameter_instant(period) -> str: + """The instant at which Colorado CCAP reads its parameters for a month. + + CCAP sets its limits each October, so a month from January to September + uses the limits that took effect the previous October 1. Before October + of the first modeled year, that October falls before the date to which + CountryTaxBenefitSystem backdates parameters, where the CCAP parameters + have no value. Those months read the parameters at the start of the + first modeled year instead. There the CCAP parameters hold their + backdated values, and `gov.hhs.smi` still holds HHS's values for the + fiscal year that began the previous October. The helper backdates + nothing further. + """ + year = period.start.year + if period.start.month >= 10: + return f"{year}-10-01" + if year - 1 < FIRST_MODELED_YEAR: + return f"{FIRST_MODELED_YEAR}-01-01" + return f"{year - 1}-10-01" diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py index 8983609a245..1aea57f8d1e 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_parent_fee.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_parent_fee(Variable): @@ -13,11 +16,7 @@ class co_ccap_parent_fee(Variable): definition_period = MONTH def formula(spm_unit, period, parameters): - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap eligible_children = spm_unit("co_ccap_eligible_children", period) base_parent_fee = spm_unit("co_ccap_base_parent_fee", period) diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py index 7f919461bd6..201151bc735 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) from policyengine_us.variables.gov.hhs.hhs_smi import smi @@ -13,10 +16,5 @@ class co_ccap_smi(Variable): def formula(spm_unit, period, parameters): size = spm_unit("spm_unit_size", period.this_year) state_code = spm_unit.household("state_code_str", period.this_year) - year = period.start.year - month = period.start.month - if month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) return smi(size, state_code, instant_str, parameters) / MONTHS_IN_YEAR diff --git a/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py b/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py index f75c0ea4b1d..7aca7415357 100644 --- a/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py +++ b/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_fpg_eligible(Variable): @@ -16,11 +19,7 @@ def formula(spm_unit, period, parameters): spm_unit("co_ccap_countable_income", period), 2, ) - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap # Calculate monthly fpg limit, only for counties in Colorado. county = household("county_str", period.this_year) diff --git a/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_smi_eligible.py b/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_smi_eligible.py index ceb92ea8656..6c3cbea2d7e 100644 --- a/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_smi_eligible.py +++ b/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_smi_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_smi_eligible(Variable): @@ -14,11 +17,7 @@ def formula(spm_unit, period, parameters): spm_unit("co_ccap_countable_income", period), 2, ) - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap # Calculate monthly smi limit smi = spm_unit("co_ccap_smi", period) diff --git a/policyengine_us/variables/gov/states/co/ccap/re_determination/co_ccap_re_determination_income_eligible.py b/policyengine_us/variables/gov/states/co/ccap/re_determination/co_ccap_re_determination_income_eligible.py index c2260252a1e..5af7a245b96 100644 --- a/policyengine_us/variables/gov/states/co/ccap/re_determination/co_ccap_re_determination_income_eligible.py +++ b/policyengine_us/variables/gov/states/co/ccap/re_determination/co_ccap_re_determination_income_eligible.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ccap.co_ccap_parameter_instant import ( + co_ccap_parameter_instant, +) class co_ccap_re_determination_income_eligible(Variable): @@ -14,11 +17,7 @@ def formula(spm_unit, period, parameters): spm_unit("co_ccap_countable_income", period), 2, ) - year = period.start.year - if period.start.month >= 10: - instant_str = f"{year}-10-01" - else: - instant_str = f"{year - 1}-10-01" + instant_str = co_ccap_parameter_instant(period) p = parameters(instant_str).gov.states.co.ccap # Calculate monthly smi limit smi = spm_unit("co_ccap_smi", period) diff --git a/policyengine_us/variables/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.py b/policyengine_us/variables/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.py index 45ec16fb307..fffdcd1cd91 100644 --- a/policyengine_us/variables/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.py +++ b/policyengine_us/variables/gov/states/co/tax/income/credits/sales_tax_refund/co_sales_tax_refund_person_eligible.py @@ -15,8 +15,10 @@ class co_sales_tax_refund_person_eligible(Variable): reference = ( # C.R.S. 39-22-2003(1)(a) defines a "qualified individual" via a # disjunctive test: (I) has CO income tax liability or files to claim a - # refund of withheld wages, OR (II) is at least 18 years of age. + # refund of withheld wages, OR (II) is at least 18 years of age as of + # December 31 of the preceding taxable year. "https://leg.colorado.gov/sites/default/files/images/olls/crs2023-title-39.pdf", + "https://colorado.public.law/statutes/crs_39-22-2003", "https://tax.colorado.gov/sites/tax/files/documents/DR_104_Book_2022.pdf#page=23", ) defined_for = StateCode.CO @@ -27,9 +29,13 @@ def formula(person, period, parameters): is_filer = person("is_tax_unit_head", period) | person( "is_tax_unit_spouse", period ) - # The statute measures age as of December 31 of the year preceding the - # taxable year; the model approximates this with current-year age. - age_eligible = person("age", period) >= p.age_threshold + # C.R.S. 39-22-2003(1)(a)(II) measures age "as of December 31 of the + # taxable year preceding such taxable year", so a filer who turns 18 + # during the year doesn't qualify on age. The 2015 Form 104 states the + # same test as "born before 1997". + age_eligible = ( + person("birth_year", period) < period.start.year - p.age_threshold + ) # Filers that had Colorado tax withheld are also eligible; use employment # income as a proxy. employment_income_eligible = person("irs_employment_income", period) > 0