Skip to content

Gate AZ Families Tax Rebate on after-credit tax liability per SB 1734#9099

Open
DTrim99 wants to merge 2 commits into
PolicyEngine:mainfrom
DTrim99:fix/az-rebate-liability-gate
Open

Gate AZ Families Tax Rebate on after-credit tax liability per SB 1734#9099
DTrim99 wants to merge 2 commits into
PolicyEngine:mainfrom
DTrim99:fix/az-rebate-liability-gate

Conversation

@DTrim99

@DTrim99 DTrim99 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9098

Surfaced by PolicyEngine/policyengine-taxsim#1102 (AZ 2021 HoH, $19,824.22 wages, one dependent).

Changes

SB 1734 (Laws 2023, ch. 147, §3) requires "a tax liability of at least $1" on the 2021 return for the Arizona Families Tax Rebate, and §3.N.4 defines the term as taxable income × rate "minus the sum of nonrefundable and refundable income tax credits claimed" under title 43, chapter 10, article 5. The ADOR FAQ says the same: credits reducing liability to zero in 2019–2021 → ineligible.

az_families_tax_rebate previously gated on az_income_tax_before_non_refundable_credits >= 1 (tax before credits). It now gates on az_income_tax_before_refundable_credits minus the other refundable credits (excise and property tax credits; the rebate itself is a session-law payment, not an article 5 credit, so its exclusion is both non-circular and statutory).

The statute's 2020/2019 fallback tests are unmodelable from a single-year record; like TAXSIM, the 2021 liability proxies all three years (noted in a comment).

Validation

Against the #1102 record (Form 140: tax 27, dependent credit 100, family income tax credit 80 → balance 0, excise credit 50 → $50 refund; TAXSIM refuses the rebate):

Before After TAXSIM / statute
az_families_tax_rebate 250 0 0 (liability < $1)
az_income_tax −300 −50 −50

New tests: the #1102 record (credits wipe liability → no rebate) and a boundary case (after-credit liability exactly $1 → rebate kept). All 10 tests in the file pass locally, including the 8 pre-existing ones.

🤖 Generated with Claude Code

SB 1734 (Laws 2023, ch. 147, sec. 3.N.4) defines the $1 tax liability
eligibility test as tax minus the sum of nonrefundable and refundable
income tax credits claimed under title 43, chapter 10, article 5. The
gate previously used tax before credits, paying the rebate to filers
whose credits wiped their liability.

Fixes PolicyEngine#9098

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (6a8a897) to head (c65bf4d).
⚠️ Report is 47 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9099   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         1           
  Lines           16        33   +17     
=========================================
+ Hits            16        33   +17     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DTrim99
DTrim99 marked this pull request as ready for review July 20, 2026 19:49
@DTrim99
DTrim99 requested a review from hua7450 July 20, 2026 19:50

@hua7450 hua7450 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Program Review — PR #9099

Gate AZ Families Tax Rebate on after-credit tax liability per SB 1734

Source Documents

  • PDF (primary): Laws 2023, ch. 147 (SB 1734) — 8 pages, offset 0. §3 = Arizona Families Tax Rebate (#page=3–6); §3.A.1 liability test, §3.C one-per-return, §3.D/E amounts + max-3 (#page=4); §3.I gross-income subtraction (#page=5); §3.N.4 "tax liability" definition (#page=6) — the exact PR citation.
  • Secondary: ADOR FAQ (azdor.gov/individuals/arizona-families-tax-rebate; corroborates the credits-net liability gate); 211 Arizona flyer (reproduces eligibility, no MFJ $1,500).
  • Cross-check: SB 1734 Senate Engrossed (SB1734S.pdf) — identical §3.N.4 wording at enactment.
  • Year: 2021 return / 2023 one-time rebate.
  • Scope: PR's actual changes only — az_families_tax_rebate.py (liability gate), its test YAML (2 new cases), and changelog.d/fix-az-rebate-liability-gate.fixed.md. No parameter files changed.

Branch Status

⚠ PR branch is 18 commit(s) behind main (1 ahead). Consider rebasing before merging. Review was scoped to the PR's actual changes — staleness did not affect any finding.

Critical (Must Fix)

C1. The PR's distinctive logic — subtracting REFUNDABLE credits to deny the rebate — is not pinned by any test; a mutant survives the whole suite.
policyengine_us/tests/policy/baseline/gov/states/az/tax/income/credits/az_families_tax_rebate.yaml (2 new cases) against az_families_tax_rebate.py:31-40.
Both new tests DO fail under the old code, so the suite is pinned against a full regression to "tax before credits." But the specific new behavior — a positive after-nonrefundable liability driven below $1 by a refundable Article-5 credit (SB 1734 §3.N.4, #page=6) — is never outcome-deciding toward denial:

  • New test (a) (taxsim#1102 record): the nonrefundable dependent + family-income credits already floor az_income_tax_before_refundable_credits at $0; the $50 refundable excise credit is incidental (the gate is decided before the refundable subtraction runs).
  • New test (b): before-refundable $51, excise $50 → liability exactly $1 → rebate KEPT. Catches over-subtraction, not under-subtraction (delete the refundable subtraction and $51 ≥ 1 still yields $250, test still passes).

Verified mutant: replacing the liability block with has_tax_liability = tax_unit("az_income_tax_before_refundable_credits", period) >= 1 (i.e. dropping the refundable subtraction and the [c for c in refundable_credits if c != "az_families_tax_rebate"] construction entirely) passes all 10 tests. The PR's central change is therefore unverified.

Fix (one test closes both this and the $0.99 boundary gap): override az_income_tax_before_refundable_credits: 50.99, az_increased_excise_tax_credit: 50, az_property_tax_credit: 0, one dependent under 17 → liability $0.99 → expect az_families_tax_rebate: 0. (A 50/50 → 0 flip plus a separate 0.99 boundary case also works. A variant with az_property_tax_credit > 0 would additionally give the property-tax credit — currently always 0 in tests — a deciding role.) The scenario is organically reachable (HOH, 17+ dependent, AGI ~$20k–$25k, under the $25,000 excise-credit AGI limit). This subsumes the separately-noted "$0.99 vs $1.00 positive sub-$1 boundary" gap (a tax_liability > 0 mutant also passes all 10 tests today).

Should Address

  1. Code comment truncates the §3.N.4 definition — the two statutory "plus" additions are undocumented.
    az_families_tax_rebate.py:20-27 (PR-authored comment). §3.N.4 (#page=6) defines tax liability as taxable income × §43-1011 rate, plus "any amount of recaptured income tax credits and the taxpayer's Arizona small business tax liability, if any," minus the sum of nonrefundable and refundable Article-5 credits claimed. The comment quotes only the "minus" clause, reading as a complete quotation. Neither addition exists in the AZ model (no recapture/small_business/sbi variables under variables|parameters/gov/states/az), so the formula cannot include them — but omitting the additions understates statutory liability and could deny the rebate to a filer whose liability arose only from credit recapture or the Form 140-SBI election (a very small population; PE keeps small-business income inside regular taxable income, so the regular-bracket tax already approximates the combined liability). Extend the comment, e.g.: "§3.N.4 also adds recaptured credits and Arizona small-business (Form 140-SBI) tax liability; neither is modeled — recapture is not modeled at all, and SBI income remains in regular taxable income, which approximates the combined liability." (Flagged independently by both the regulatory and reference validators — same finding.)

  2. Hard-coded $1 statutory liability threshold should be parameterized.
    az_families_tax_rebate.py:40 (has_tax_liability = tax_liability >= 1). The $1 is a statutory dollar threshold (§3.A.1 "a tax liability of at least one dollar", #page=4), not basic math, so under the zero-hard-coding principle it belongs in a parameter (e.g. parameters/gov/states/az/tax/income/rebate/min_tax_liability.yaml, unit: currency-USD, 2021-01-01: 1). Mitigating: the literal predates this PR, the value is correct, 1 sits in the "acceptable literals" carve-out, and the operator >= correctly encodes "at least $1." Not a blocker — but the PR rewrites this exact gate, putting parameterization squarely in scope.

  3. No downstream/integration assertion — az_income_tax is never checked for 2021.
    No test asserts az_income_tax or az_refundable_credits in the rebate's base year (AZ integration.yaml cases are 2024–2026). So the rebate's membership in the 2021 refundable-credits list — i.e. that $250 → $0 actually flows through az_refundable_credits into az_income_tax — is unverified, and new test (a)'s premise ("tax 26.53, credits reduce to zero") lives only in a YAML comment. Cheap fix: add az_income_tax_before_refundable_credits: 0, az_increased_excise_tax_credit: 50, az_income_tax: -50 outputs to the (fully organic) new test (a), making it self-checking and pinning the downstream flow. (No partner contract tests touch AZ, so no partner-facing gap.)

Pre-existing (out of scope for this PR)

  1. The "claimed a dependent tax credit" prong is not enforced for high-income filers, and the header comment is imprecise about it.
    §3.A (#page=4) requires having "claimed a dependent tax credit under section 43-1073.01" on the 2021 return; the ADOR FAQ is explicit that filers whose income exceeded the dependent-credit threshold are ineligible even with qualifying dependents. The formula keys only on is_tax_unit_dependent presence, so a high-AGI filer with children and ample liability receives the rebate contrary to statute (the AZ dependent credit phases out entirely at federal AGI ≥ $220k single/HOH/MFS, ≥ $420k joint). A one-line az_dependent_tax_credit_potential > 0 gate would fix it — use the _potential variable, not the liability-capped az_dependent_tax_credit (a credit reduced to $0 by the cap was still "claimed"). The header comment (az_families_tax_rebate.py:16-17) also glosses this as "having claimed dependents." Not touched by this PR, but it is the other clause of the same §3.A sentence the PR corrects. (Consolidates regulatory S2, PDF audit c-2, and reference suggestion 3.)

  2. max_dependents JOINT = 6 conflicts with §3.C/§3.E for a genuine 2021 joint return.
    §3.C: "One rebate … for each full-year resident tax return for taxable year 2021"; §3.E: "a maximum of three dependents for a qualifying taxpayer" (#page=4). A 2021 MFJ return with six dependents is one return → max 3 → $750, but parameters/gov/states/az/tax/income/rebate/max_dependents.yaml sets JOINT: 6 (and a pre-existing test enshrines $1,500). The 6/$1,500 figure comes from the ADOR FAQ, which describes two taxpayers who each earned a $750 rebate on separate 2021 returns and married afterward — under period-2021 simulation such couples are separate tax units, so the 6-dependent allowance over-pays genuine 2021 MFJ returns. The value 6 is not in the session-law text (§3.E, #page=4). Unchanged by this PR; flag for a follow-up issue. (Consolidates regulatory S3 and PDF audit d-1.)

Suggestions

  1. Add the PDF with a section pin as a second reference. az_families_tax_rebate.py:10. The bare-string reference is the whole 4-section chapter law (.htm, no section anchors); the precise cite lives only in the new comment. Adding "https://www.azleg.gov/legtext/56leg/1R/laws/0147.pdf#page=6" (= §3.N.4) makes the metadata self-sufficient.
  2. Deprecated documentation field. az_families_tax_rebate.py:9. Fold the ADOR FAQ URL into the reference tuple; variables use bare-string/tuple references and documentation is deprecated. (Pre-existing; flagged by both code and reference validators.)
  3. String-literal self-exclusion is slightly fragile but acceptable. az_families_tax_rebate.py:37 ([c for c in refundable_credits if c != "az_families_tax_rebate"]). Required to break the az_families_tax_rebate → az_refundable_credits → az_families_tax_rebate cycle; the credit list itself is parameter-driven, and a stale string after a rename would fail loudly (circular-dependency error), not silently. Has codebase precedent. No action required.
  4. Note that the single-year proxy is strictly conservative. §3.A.1–3 (#page=4) allow qualifying on 2020 then 2019 liability when 2021 is under $1; a one-year record cannot evaluate the fallbacks. The comment already documents the proxy — optionally add that it can only deny, never over-grant, which helps anyone comparing model output to ADOR's paid-rebate totals.

Pre-existing (out of scope for this PR)

  1. Variable computes a phantom rebate for 2022+ (model-scope gap). az_families_tax_rebate.py. The one-time nature is enforced only by credits/refundable.yaml (rebate listed in the 2021 block, removed at 2022-01-01), so az_income_tax is unaffected outside 2021. But rebate/amount.yaml, age_threshold.yaml, and max_dependents.yaml extend forward from 2021-01-01 with no expiry, so a direct query of az_families_tax_rebate at 2022+ returns nonzero (visible in API/household-app contexts), and a pre-2021 query hits an undefined age_threshold. A period: 2022 → 0 test would currently FAIL. An in_effect guard or end-dated amounts is the standard fix. (Consolidates regulatory G3, code suggestion 2, test G4, and PDF audit c-5.)
  2. Age-17 boundary of the $250/$100 split untested. age_threshold is 17 and the formula uses age >= 17 → $100, but the only "older" dependent tested is 19, so an age > 17 mutant (a 17-year-old paid $250) survives. Add a dependent aged exactly 17 → $100.
  3. Young-first cap prioritization never squeezes out an older dependent. Cap-overflow cases use all-young dependents, so the min_(older_count, remaining_slots) clamp never binds. Add e.g. single filer, 3 young + 1 older → $750 (older gets zero slots).
  4. SEPARATE and SURVIVING_SPOUSE filing statuses have zero coverage. max_dependents.yaml has rows for all five statuses, but tests cover only SINGLE, JOINT, HEAD_OF_HOUSEHOLD. Add at least one case per row (MFS also has special SB 1734 treatment worth documenting).

PDF Audit Summary

Category Count Detail
Confirmed correct 12 $1 test + >= operator (§3.A.1); liability = tax − nonrefundable & refundable Article-5 credits (§3.N.4); rebate self-excluded; $250 under-17 / $100 age-17+; age boundary 17; max-3 (S/HOH/MFS/surviving spouse); young-first ordering; dependent-claim proxy; AZ residency proxy; MFJ = 6/$1,500 (ADOR FAQ); both new tests re-derived by hand. All 8 pre-existing tests also re-derived correct.
Mismatches 0 No value or operator disagreement between repo and statute/FAQ.
Rejected 0
Unmodeled (statute → repo) 5 2020/2019 fallback years; dependent-credit hard gate; recaptured credits + SBI tax liability; full-year-resident/return-filed check; one-time expiry — all pre-existing modeling simplifications, not regressions.
In repo, not literal in statute 2 JOINT = 6 (from ADOR FAQ, legitimate administrative interpretation); per-filing-status cap breakdown (all non-joint values = 3, consistent with the flat statutory 3). Presentational, not value issues.

Validation Summary

Validator Result
Regulatory Accuracy 0 critical, 3 should (1 PR-scoped, 2 pre-existing), 3 suggestions. The new gate correctly implements §3.N.4 to the extent the model's tax chain allows and fixes a real statutory error in the old gate (which subtracted no credits).
Reference Quality 0 critical, 1 should, 3 suggestions. Reference URL resolves (HTTP 200) and contains §3.N.4 verbatim; ADOR FAQ corroborates the credits-net gate; jurisdiction correct.
Code Patterns 0 critical, 1 should (hard-coded $1), 3 suggestions. Follows PolicyEngine standards; correct entities/periods; no dependency cycle; ruff format clean; changelog fragment correct.
Test Coverage 1 critical (distinctive refundable-subtraction logic untested — a mutant survives all 10 tests), 1 should, 4 suggestions. Both new tests fail under old code (regression-pinned), but the PR's core change is unverified.
PDF Value Audit 12 matches, 0 mismatches, 5 pre-existing unmodeled items, 2 FAQ-sourced repo values.
CI Status All checks passing (full suite, microsim, Household API Partners, changelog fragment).

Review Severity: REQUEST_CHANGES

The implementation is regulatorily accurate (0 PDF mismatches, references corroborate) and CI is green, but the PR's central behavioral change — denying the rebate when a refundable Article-5 credit drives after-credit liability below $1 — is not pinned by any test (a mutant that removes the refundable subtraction passes the entire suite). Add the one test in C1 (which also closes the $0.99 boundary gap); the Should items are quick, non-blocking-once-C1-is-fixed improvements, and the pre-existing items belong in a follow-up issue.

Next Steps

To auto-fix issues: /fix-pr 9099

…ndable-subtraction denial with a sub-$1 test, extend statute comment and references

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DTrim99

DTrim99 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes applied (commit c65bf4d)

Thanks @hua7450 for the review. Addressed the findings:

Critical

  • Pinned the refundable-subtraction denial logic: added a test where a refundable excise credit drives after-credit liability below $1 (az_income_tax_before_refundable_credits: 50.99, az_increased_excise_tax_credit: 50 → liability $0.99 → rebate $0). This fails if the refundable subtraction is removed, so the PR's central change is now outcome-deciding toward denial; it also closes the sub-$1 boundary gap.

Should-address

  • Parameterized the $1 minimum-liability gate (SB 1734 §3.A.1) into a new gov/states/az/tax/income/rebate/min_tax_liability.yaml parameter (with reference); the variable now reads p.min_tax_liability instead of the >= 1 literal.
  • Extended the §3.N.4 comment to note the statutory definition also adds recaptured credits and AZ small-business (Form 140-SBI) liability, neither of which is modeled.
  • Added a self-checking downstream assertion (az_income_tax: -50) to the taxsim Avoid double-counting CDCC when refundable #1102 case.

Suggestions

  • Added #page anchors to the azleg SB 1734 references and folded the ADOR FAQ URL from the deprecated documentation field into the reference tuple.

Verification

  • AZ income-tax suite passes locally (294/294).
  • Pattern validator: CLEAN (0 new issues).
  • make format: done.

@DTrim99
DTrim99 requested a review from hua7450 July 21, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AZ Families Tax Rebate gates on tax before credits; SB 1734 defines liability net of nonrefundable and refundable credits

2 participants