From c60a45619055babc8da7c74955a88a349bac72d0 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:08:08 -0400 Subject: [PATCH 1/3] Add NJ budget housing reforms Implements StayNJ and ANCHOR changes from Gov. Sherrill's budget proposal. Closes #7762 Co-Authored-By: Claude Opus 4.6 From 98d7865a3517a0700b69ecf12954775bc62b9812 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:41:09 -0400 Subject: [PATCH 2/3] Add NJ StayNJ and ANCHOR budget housing reforms Implement separately toggleable contributed reforms for NJ's FY2026 budget proposals: - ANCHOR: expanded property tax relief with higher benefits for seniors - StayNJ: new senior property tax credit with $250K income limit Includes parameters, reform modules, and YAML test cases (12 tests). Co-Authored-By: Claude Opus 4.6 --- changelog.d/7763.added.md | 1 + .../homeowner/senior/amount/lower_income.yaml | 15 ++ .../homeowner/senior/amount/upper_income.yaml | 15 ++ .../contrib/states/nj/anchor/in_effect.yaml | 14 ++ .../contrib/states/nj/stay_nj/in_effect.yaml | 14 ++ .../states/nj/stay_nj/income_limit.yaml | 15 ++ .../states/nj/stay_nj/max_benefit.yaml | 15 ++ policyengine_us/reforms/reforms.py | 5 + policyengine_us/reforms/states/nj/__init__.py | 2 + .../reforms/states/nj/anchor/__init__.py | 4 + .../states/nj/anchor/nj_anchor_reform.py | 107 +++++++++++ .../reforms/states/nj/stay_nj/__init__.py | 4 + .../states/nj/stay_nj/nj_stay_nj_reform.py | 102 ++++++++++ .../nj/anchor/nj_anchor_budget_reform.yaml | 171 +++++++++++++++++ .../nj/stay_nj/nj_stay_nj_budget_reform.yaml | 178 ++++++++++++++++++ 15 files changed, 662 insertions(+) create mode 100644 changelog.d/7763.added.md create mode 100644 policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/lower_income.yaml create mode 100644 policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/upper_income.yaml create mode 100644 policyengine_us/parameters/gov/contrib/states/nj/anchor/in_effect.yaml create mode 100644 policyengine_us/parameters/gov/contrib/states/nj/stay_nj/in_effect.yaml create mode 100644 policyengine_us/parameters/gov/contrib/states/nj/stay_nj/income_limit.yaml create mode 100644 policyengine_us/parameters/gov/contrib/states/nj/stay_nj/max_benefit.yaml create mode 100644 policyengine_us/reforms/states/nj/__init__.py create mode 100644 policyengine_us/reforms/states/nj/anchor/__init__.py create mode 100644 policyengine_us/reforms/states/nj/anchor/nj_anchor_reform.py create mode 100644 policyengine_us/reforms/states/nj/stay_nj/__init__.py create mode 100644 policyengine_us/reforms/states/nj/stay_nj/nj_stay_nj_reform.py create mode 100644 policyengine_us/tests/policy/contrib/states/nj/anchor/nj_anchor_budget_reform.yaml create mode 100644 policyengine_us/tests/policy/contrib/states/nj/stay_nj/nj_stay_nj_budget_reform.yaml diff --git a/changelog.d/7763.added.md b/changelog.d/7763.added.md new file mode 100644 index 00000000000..f1ade512119 --- /dev/null +++ b/changelog.d/7763.added.md @@ -0,0 +1 @@ +Added NJ StayNJ and ANCHOR budget housing reforms as separately toggleable contributed reforms. diff --git a/policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/lower_income.yaml b/policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/lower_income.yaml new file mode 100644 index 00000000000..ec145e1e1a5 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/lower_income.yaml @@ -0,0 +1,15 @@ +description: >- + New Jersey reduces the ANCHOR senior homeowner lower-income benefit to this amount + after the $250 bonus expires following FY2026. +values: + 2027-01-01: 1_500 + +metadata: + unit: currency-USD + period: year + label: NJ ANCHOR senior homeowner lower-income amount (post-bonus) + reference: + - title: P.L. 2023, c.75, Section 15 - ANCHOR Senior Bonus (sunset FY2026) + href: https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.PDF#page=11 + - title: ANCHOR Program - How Benefits Are Calculated + href: https://www.nj.gov/treasury/taxation/anchor/calculated.shtml diff --git a/policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/upper_income.yaml b/policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/upper_income.yaml new file mode 100644 index 00000000000..064355e4531 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/nj/anchor/homeowner/senior/amount/upper_income.yaml @@ -0,0 +1,15 @@ +description: >- + New Jersey reduces the ANCHOR senior homeowner upper-income benefit to this amount + after the $250 bonus expires following FY2026. +values: + 2027-01-01: 1_000 + +metadata: + unit: currency-USD + period: year + label: NJ ANCHOR senior homeowner upper-income amount (post-bonus) + reference: + - title: P.L. 2023, c.75, Section 15 - ANCHOR Senior Bonus (sunset FY2026) + href: https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.PDF#page=11 + - title: ANCHOR Program - How Benefits Are Calculated + href: https://www.nj.gov/treasury/taxation/anchor/calculated.shtml diff --git a/policyengine_us/parameters/gov/contrib/states/nj/anchor/in_effect.yaml b/policyengine_us/parameters/gov/contrib/states/nj/anchor/in_effect.yaml new file mode 100644 index 00000000000..cf768138fdb --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/nj/anchor/in_effect.yaml @@ -0,0 +1,14 @@ +description: >- + New Jersey ANCHOR senior homeowner bonus expiry reform applies if this is true. +values: + 0000-01-01: false + +metadata: + unit: bool + period: year + label: NJ ANCHOR senior bonus expiry in effect + reference: + - title: P.L. 2023, c.75, Section 15 - ANCHOR Senior Bonus (sunset FY2026) + href: https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.PDF#page=11 + - title: NJ FY2027 Budget in Brief - ANCHOR Senior Renter Bonus Extension + href: https://www.nj.gov/treasury/omb/publications/27bib/BIB.pdf#page=18 diff --git a/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/in_effect.yaml b/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/in_effect.yaml new file mode 100644 index 00000000000..4e9933f33b6 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/in_effect.yaml @@ -0,0 +1,14 @@ +description: >- + New Jersey StayNJ budget reform applies if this is true. +values: + 0000-01-01: false + +metadata: + unit: bool + period: year + label: NJ StayNJ budget reform in effect + reference: + - title: NJ FY2027 Budget in Brief - Property Tax Relief + href: https://www.nj.gov/treasury/omb/publications/27bib/BIB.pdf#page=17 + - title: Governor Sherrill Presents Fiscal Year 2027 Budget + href: https://www.nj.gov/governor/news/2026/20260310b.shtml diff --git a/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/income_limit.yaml b/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/income_limit.yaml new file mode 100644 index 00000000000..ce623d66204 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/income_limit.yaml @@ -0,0 +1,15 @@ +description: >- + New Jersey reduces the StayNJ income threshold to this amount under the + Governor's FY2027 budget proposal. +values: + 2027-01-01: 250_000 + +metadata: + unit: currency-USD + period: year + label: NJ StayNJ budget reform income limit + reference: + - title: NJ FY2027 Budget in Brief - Property Tax Relief + href: https://www.nj.gov/treasury/omb/publications/27bib/BIB.pdf#page=17 + - title: P.L. 2023, c.75 - Stay NJ Act + href: https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.PDF#page=1 diff --git a/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/max_benefit.yaml b/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/max_benefit.yaml new file mode 100644 index 00000000000..a42fe65d566 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/states/nj/stay_nj/max_benefit.yaml @@ -0,0 +1,15 @@ +description: >- + New Jersey caps the StayNJ maximum benefit at this amount under the + Governor's FY2027 budget proposal. +values: + 2027-01-01: 4_000 + +metadata: + unit: currency-USD + period: year + label: NJ StayNJ budget reform max benefit + reference: + - title: NJ FY2027 Budget in Brief - Property Tax Relief + href: https://www.nj.gov/treasury/omb/publications/27bib/BIB.pdf#page=17 + - title: P.L. 2023, c.75 - Stay NJ Act + href: https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.PDF#page=1 diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 3070e9395e9..0e7450889f7 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -177,6 +177,9 @@ from .congress.watca import ( create_watca_reform, ) +from .states.wa.sb6346 import ( + create_wa_sb6346_reform, +) from .states.ga.sb520 import ( @@ -346,6 +349,7 @@ def create_structural_reforms_from_parameters(parameters, period): ct_hb5009 = create_ct_hb5009_reform(parameters, period) ga_sb520 = create_ga_sb520_reform(parameters, period) watca = create_watca_reform(parameters, period) + wa_sb6346 = create_wa_sb6346_reform(parameters, period) reforms = [ afa_reform, @@ -428,6 +432,7 @@ def create_structural_reforms_from_parameters(parameters, period): ct_tax_rebate_2026, ga_sb520, watca, + wa_sb6346, ] reforms = tuple(filter(lambda x: x is not None, reforms)) diff --git a/policyengine_us/reforms/states/nj/__init__.py b/policyengine_us/reforms/states/nj/__init__.py new file mode 100644 index 00000000000..5a45d51f079 --- /dev/null +++ b/policyengine_us/reforms/states/nj/__init__.py @@ -0,0 +1,2 @@ +from .stay_nj import create_nj_stay_nj_reform, nj_stay_nj_budget_reform +from .anchor import create_nj_anchor_reform, nj_anchor_budget_reform diff --git a/policyengine_us/reforms/states/nj/anchor/__init__.py b/policyengine_us/reforms/states/nj/anchor/__init__.py new file mode 100644 index 00000000000..55d3f807527 --- /dev/null +++ b/policyengine_us/reforms/states/nj/anchor/__init__.py @@ -0,0 +1,4 @@ +from .nj_anchor_reform import ( + create_nj_anchor_reform, + nj_anchor_budget_reform, +) diff --git a/policyengine_us/reforms/states/nj/anchor/nj_anchor_reform.py b/policyengine_us/reforms/states/nj/anchor/nj_anchor_reform.py new file mode 100644 index 00000000000..b902ad78c32 --- /dev/null +++ b/policyengine_us/reforms/states/nj/anchor/nj_anchor_reform.py @@ -0,0 +1,107 @@ +from policyengine_us.model_api import * +from policyengine_core.periods import period as period_ + + +def create_nj_anchor() -> Reform: + class nj_anchor(Variable): + value_type = float + entity = TaxUnit + label = "New Jersey ANCHOR benefit" + unit = USD + definition_period = YEAR + reference = ( + "https://www.nj.gov/treasury/taxation/anchor/", + "https://www.nj.gov/treasury/taxation/anchor/calculated.shtml", + ) + defined_for = "nj_anchor_eligible" + + def formula(tax_unit, period, parameters): + p_baseline = parameters(period).gov.states.nj.tax.income.credits.anchor + p_reform = parameters(period).gov.contrib.states.nj.anchor + + reform_active = p_reform.in_effect + + gross_income = add(tax_unit, period, ["nj_gross_income"]) + greater_age = tax_unit("greater_age_head_spouse", period) + is_senior = greater_age >= p_baseline.age_threshold + + pays_property_taxes = add(tax_unit, period, ["real_estate_taxes"]) > 0 + pays_rent = tax_unit("rents", period) + is_homeowner = pays_property_taxes & ~pays_rent + is_renter = pays_rent & ~pays_property_taxes + + lower_income = gross_income <= p_baseline.homeowner.income_limit.lower + + # Senior homeowner amounts: reform overrides when active + baseline_senior_lower = p_baseline.homeowner.senior.amount.lower_income + baseline_senior_upper = p_baseline.homeowner.senior.amount.upper_income + reform_senior_lower = p_reform.homeowner.senior.amount.lower_income + reform_senior_upper = p_reform.homeowner.senior.amount.upper_income + + senior_lower = where( + reform_active, + reform_senior_lower, + baseline_senior_lower, + ) + senior_upper = where( + reform_active, + reform_senior_upper, + baseline_senior_upper, + ) + + homeowner_senior_amount = where(lower_income, senior_lower, senior_upper) + + # Non-senior homeowner amounts: unchanged from baseline + homeowner_non_senior_amount = where( + lower_income, + p_baseline.homeowner.non_senior.amount.lower_income, + p_baseline.homeowner.non_senior.amount.upper_income, + ) + homeowner_amount = where( + is_senior, + homeowner_senior_amount, + homeowner_non_senior_amount, + ) + + # Renter amounts: unchanged from baseline + renter_amount = where( + is_senior, + p_baseline.renter.senior.amount, + p_baseline.renter.non_senior.amount, + ) + + return where( + is_homeowner, + homeowner_amount, + where(is_renter, renter_amount, 0), + ) + + class reform(Reform): + def apply(self): + self.update_variable(nj_anchor) + + return reform + + +def create_nj_anchor_reform(parameters, period, bypass: bool = False): + if bypass: + return create_nj_anchor() + + p = parameters.gov.contrib.states.nj.anchor + + reform_active = False + current_period = period_(period) + + for _ in range(5): + if p(current_period).in_effect: + reform_active = True + break + current_period = current_period.offset(1, "year") + + if reform_active: + return create_nj_anchor() + else: + return None + + +nj_anchor_budget_reform = create_nj_anchor_reform(None, None, bypass=True) diff --git a/policyengine_us/reforms/states/nj/stay_nj/__init__.py b/policyengine_us/reforms/states/nj/stay_nj/__init__.py new file mode 100644 index 00000000000..dcfc82f1c51 --- /dev/null +++ b/policyengine_us/reforms/states/nj/stay_nj/__init__.py @@ -0,0 +1,4 @@ +from .nj_stay_nj_reform import ( + create_nj_stay_nj_reform, + nj_stay_nj_budget_reform, +) diff --git a/policyengine_us/reforms/states/nj/stay_nj/nj_stay_nj_reform.py b/policyengine_us/reforms/states/nj/stay_nj/nj_stay_nj_reform.py new file mode 100644 index 00000000000..4b52deabde0 --- /dev/null +++ b/policyengine_us/reforms/states/nj/stay_nj/nj_stay_nj_reform.py @@ -0,0 +1,102 @@ +from policyengine_us.model_api import * +from policyengine_core.periods import period as period_ + + +def create_nj_stay_nj() -> Reform: + class nj_staynj_eligible(Variable): + value_type = bool + entity = TaxUnit + label = "New Jersey Stay NJ Property Tax Credit program eligibility" + definition_period = YEAR + reference = ( + "https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.HTM", + "https://www.nj.gov/treasury/taxation/staynj/index.shtml", + ) + defined_for = StateCode.NJ + + def formula(tax_unit, period, parameters): + p_baseline = parameters(period).gov.states.nj.tax.income.credits.staynj + p_reform = parameters(period).gov.contrib.states.nj.stay_nj + + reform_active = p_reform.in_effect + + greater_age = tax_unit("greater_age_head_spouse", period) + age_eligible = greater_age >= p_baseline.age_threshold + + gross_income = add(tax_unit, period, ["nj_gross_income"]) + baseline_income_eligible = gross_income < p_baseline.income_limit + reform_income_eligible = gross_income < p_reform.income_limit + income_eligible = where( + reform_active, + reform_income_eligible, + baseline_income_eligible, + ) + + pays_property_taxes = add(tax_unit, period, ["real_estate_taxes"]) > 0 + pays_rent = tax_unit("rents", period) + is_homeowner = pays_property_taxes & ~pays_rent + + return age_eligible & income_eligible & is_homeowner + + class nj_staynj(Variable): + value_type = float + entity = TaxUnit + label = "New Jersey Stay NJ Property Tax Credit" + unit = USD + definition_period = YEAR + reference = ( + "https://pub.njleg.state.nj.us/Bills/2022/PL23/75_.HTM", + "https://www.nj.gov/treasury/taxation/staynj/index.shtml", + ) + defined_for = "nj_staynj_eligible" + + def formula(tax_unit, period, parameters): + p_baseline = parameters(period).gov.states.nj.tax.income.credits.staynj + p_reform = parameters(period).gov.contrib.states.nj.stay_nj + + reform_active = p_reform.in_effect + + property_taxes = add(tax_unit, period, ["real_estate_taxes"]) + + max_benefit = where( + reform_active, + p_reform.max_benefit, + p_baseline.max_benefit, + ) + target_benefit = min_(property_taxes * p_baseline.rate, max_benefit) + + anchor_benefit = tax_unit("nj_anchor", period) + senior_freeze = tax_unit("nj_senior_freeze", period) + + return max_(target_benefit - anchor_benefit - senior_freeze, 0) + + class reform(Reform): + def apply(self): + self.update_variable(nj_staynj_eligible) + self.update_variable(nj_staynj) + + return reform + + +def create_nj_stay_nj_reform(parameters, period, bypass: bool = False): + if bypass: + return create_nj_stay_nj() + + p = parameters.gov.contrib.states.nj.stay_nj + + reform_active = False + current_period = period_(period) + + for _ in range(5): + if p(current_period).in_effect: + reform_active = True + break + current_period = current_period.offset(1, "year") + + if reform_active: + return create_nj_stay_nj() + else: + return None + + +nj_stay_nj_budget_reform = create_nj_stay_nj_reform(None, None, bypass=True) diff --git a/policyengine_us/tests/policy/contrib/states/nj/anchor/nj_anchor_budget_reform.yaml b/policyengine_us/tests/policy/contrib/states/nj/anchor/nj_anchor_budget_reform.yaml new file mode 100644 index 00000000000..95397cc131a --- /dev/null +++ b/policyengine_us/tests/policy/contrib/states/nj/anchor/nj_anchor_budget_reform.yaml @@ -0,0 +1,171 @@ +# ANCHOR Senior Homeowner Bonus Expiry Reform Tests +# Reform removes $250 senior homeowner bonus that was added for FY2024-FY2026 +# Senior homeowner amounts revert: $1,750 -> $1,500 (lower), $1,250 -> $1,000 (upper) +# All other amounts (non-senior homeowner, renter) are unchanged + +- name: Case 1, senior homeowner lower income gets $1,500 not $1,750. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.anchor.nj_anchor_reform.nj_anchor_budget_reform + input: + gov.contrib.states.nj.anchor.in_effect: true + people: + person1: + age: 70 + employment_income: 100_000 + real_estate_taxes: 5_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Senior homeowner, income $100K <= $150K (lower tier) + # Reform amount: $1,500 (baseline with bonus would be $1,750) + nj_anchor: 1_500 + +- name: Case 2, senior homeowner upper income gets $1,000 not $1,250. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.anchor.nj_anchor_reform.nj_anchor_budget_reform + input: + gov.contrib.states.nj.anchor.in_effect: true + people: + person1: + age: 70 + employment_income: 200_000 + real_estate_taxes: 5_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITHOUT_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Senior homeowner, income $200K > $150K but <= $250K (upper tier) + # Reform amount: $1,000 (baseline with bonus would be $1,250) + nj_anchor: 1_000 + +- name: Case 3, senior renter unchanged at $700. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.anchor.nj_anchor_reform.nj_anchor_budget_reform + input: + gov.contrib.states.nj.anchor.in_effect: true + people: + person1: + age: 70 + employment_income: 100_000 + rent: 18_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: RENTER + households: + household: + members: [person1] + state_code: NJ + output: + # Senior renter, income $100K <= $150K + # Reform does NOT affect renter amounts - still $700 + nj_anchor: 700 + +- name: Case 4, non-senior homeowner lower income unchanged at $1,500. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.anchor.nj_anchor_reform.nj_anchor_budget_reform + input: + gov.contrib.states.nj.anchor.in_effect: true + people: + person1: + age: 45 + employment_income: 100_000 + real_estate_taxes: 5_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Non-senior homeowner, income $100K <= $150K (lower tier) + # Reform does NOT affect non-senior amounts - still $1,500 + nj_anchor: 1_500 + +- name: Case 5, non-senior renter unchanged at $450. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.anchor.nj_anchor_reform.nj_anchor_budget_reform + input: + gov.contrib.states.nj.anchor.in_effect: true + people: + person1: + age: 45 + employment_income: 100_000 + rent: 18_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: RENTER + households: + household: + members: [person1] + state_code: NJ + output: + # Non-senior renter, income $100K <= $150K + # Reform does NOT affect renter amounts - still $450 + nj_anchor: 450 + +- name: Case 6, senior homeowner over income limit is ineligible. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.anchor.nj_anchor_reform.nj_anchor_budget_reform + input: + gov.contrib.states.nj.anchor.in_effect: true + people: + person1: + age: 70 + employment_income: 260_000 + real_estate_taxes: 5_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Income $260K > $250K homeowner upper limit - ineligible + nj_anchor: 0 diff --git a/policyengine_us/tests/policy/contrib/states/nj/stay_nj/nj_stay_nj_budget_reform.yaml b/policyengine_us/tests/policy/contrib/states/nj/stay_nj/nj_stay_nj_budget_reform.yaml new file mode 100644 index 00000000000..aca9f5ec64c --- /dev/null +++ b/policyengine_us/tests/policy/contrib/states/nj/stay_nj/nj_stay_nj_budget_reform.yaml @@ -0,0 +1,178 @@ +# StayNJ Budget Reform Tests +# Reform reduces income limit from $500K to $250K and max benefit from $6,500 to $4,000 +# Formula: StayNJ = max(min(property_taxes * 50%, max_benefit) - ANCHOR - Senior_Freeze, 0) +# Eligibility: age 65+, homeowner, income < $250K (reform limit) + +- name: Case 1, eligible senior homeowner under new $250K income limit. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.stay_nj.nj_stay_nj_reform.nj_stay_nj_budget_reform + input: + gov.contrib.states.nj.stay_nj.in_effect: true + people: + person1: + age: 70 + employment_income: 200_000 + real_estate_taxes: 10_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Income $200K < $250K reform limit - eligible + # min($10,000 * 50%, $4,000) = min($5,000, $4,000) = $4,000 + # ANCHOR = $1,250 (senior homeowner, income $150K-$250K, post-bonus: $1,000) + # But ANCHOR reform is NOT active here, so baseline ANCHOR = $1,250 + # max($4,000 - $1,250 - $0, 0) = $2,750 + nj_staynj: 2_750 + +- name: Case 2, ineligible under new $250K limit but would be eligible under baseline $500K. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.stay_nj.nj_stay_nj_reform.nj_stay_nj_budget_reform + input: + gov.contrib.states.nj.stay_nj.in_effect: true + people: + person1: + age: 70 + employment_income: 300_000 + real_estate_taxes: 15_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Income $300K >= $250K reform limit - ineligible + # (Would be eligible under baseline $500K limit) + nj_staynj_eligible: false + nj_staynj: 0 + +- name: Case 3, max benefit capped at $4,000 with high property taxes. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.stay_nj.nj_stay_nj_reform.nj_stay_nj_budget_reform + input: + gov.contrib.states.nj.stay_nj.in_effect: true + people: + person1: + age: 68 + employment_income: 100_000 + real_estate_taxes: 20_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # min($20,000 * 50%, $4,000) = min($10,000, $4,000) = $4,000 (capped at reform max) + # Baseline max would be $6,500 - reform caps at $4,000 + # ANCHOR = $1,750 (senior homeowner, income <= $150K) + # max($4,000 - $1,750 - $0, 0) = $2,250 + nj_staynj: 2_250 + +- name: Case 4, senior renter ineligible for StayNJ. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.stay_nj.nj_stay_nj_reform.nj_stay_nj_budget_reform + input: + gov.contrib.states.nj.stay_nj.in_effect: true + people: + person1: + age: 70 + employment_income: 100_000 + rent: 18_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: RENTER + households: + household: + members: [person1] + state_code: NJ + output: + # Renter - not homeowner - ineligible for StayNJ + nj_staynj_eligible: false + nj_staynj: 0 + +- name: Case 5, under-65 homeowner ineligible for StayNJ. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.stay_nj.nj_stay_nj_reform.nj_stay_nj_budget_reform + input: + gov.contrib.states.nj.stay_nj.in_effect: true + people: + person1: + age: 60 + employment_income: 100_000 + real_estate_taxes: 10_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Age 60 < 65 - ineligible for StayNJ + nj_staynj_eligible: false + nj_staynj: 0 + +- name: Case 6, income exactly at $250K reform threshold is ineligible. + period: 2027 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.states.nj.stay_nj.nj_stay_nj_reform.nj_stay_nj_budget_reform + input: + gov.contrib.states.nj.stay_nj.in_effect: true + people: + person1: + age: 70 + employment_income: 250_000 + real_estate_taxes: 10_000 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + spm_unit_tenure_type: OWNER_WITH_MORTGAGE + households: + household: + members: [person1] + state_code: NJ + output: + # Income $250K NOT < $250K (strict less than) - ineligible under reform + nj_staynj_eligible: false + nj_staynj: 0 From 54b9fb7021e21204c42cd41fea388bde61e5ce74 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:00:43 -0400 Subject: [PATCH 3/3] Fix reforms.py: remove stale WA SB6346 import, add NJ reform registrations The previous commit accidentally included WA SB6346 import from another branch, causing ModuleNotFoundError in CI. Removed it and properly registered the NJ StayNJ and ANCHOR reforms. Co-Authored-By: Claude Opus 4.6 --- policyengine_us/reforms/reforms.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 0e7450889f7..ac8e92d78ee 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -177,8 +177,11 @@ from .congress.watca import ( create_watca_reform, ) -from .states.wa.sb6346 import ( - create_wa_sb6346_reform, +from .states.nj.stay_nj import ( + create_nj_stay_nj_reform, +) +from .states.nj.anchor import ( + create_nj_anchor_reform, ) @@ -349,7 +352,8 @@ def create_structural_reforms_from_parameters(parameters, period): ct_hb5009 = create_ct_hb5009_reform(parameters, period) ga_sb520 = create_ga_sb520_reform(parameters, period) watca = create_watca_reform(parameters, period) - wa_sb6346 = create_wa_sb6346_reform(parameters, period) + nj_stay_nj = create_nj_stay_nj_reform(parameters, period) + nj_anchor = create_nj_anchor_reform(parameters, period) reforms = [ afa_reform, @@ -432,7 +436,8 @@ def create_structural_reforms_from_parameters(parameters, period): ct_tax_rebate_2026, ga_sb520, watca, - wa_sb6346, + nj_stay_nj, + nj_anchor, ] reforms = tuple(filter(lambda x: x is not None, reforms))