Gate AZ Families Tax Rebate on after-credit tax liability per SB 1734#9099
Gate AZ Families Tax Rebate on after-credit tax liability per SB 1734#9099DTrim99 wants to merge 2 commits into
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
hua7450
left a comment
There was a problem hiding this comment.
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), andchangelog.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_creditsat $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
-
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 (norecapture/small_business/sbivariables undervariables|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.) -
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,1sits 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. -
No downstream/integration assertion —
az_income_taxis never checked for 2021.
No test assertsaz_income_taxoraz_refundable_creditsin the rebate's base year (AZintegration.yamlcases are 2024–2026). So the rebate's membership in the 2021 refundable-credits list — i.e. that $250 → $0 actually flows throughaz_refundable_creditsintoaz_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: addaz_income_tax_before_refundable_credits: 0,az_increased_excise_tax_credit: 50,az_income_tax: -50outputs 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)
-
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 onis_tax_unit_dependentpresence, 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-lineaz_dependent_tax_credit_potential > 0gate would fix it — use the_potentialvariable, not the liability-cappedaz_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.) -
max_dependentsJOINT = 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, butparameters/gov/states/az/tax/income/rebate/max_dependents.yamlsets 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
- 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. - Deprecated
documentationfield.az_families_tax_rebate.py:9. Fold the ADOR FAQ URL into thereferencetuple; variables use bare-string/tuple references anddocumentationis deprecated. (Pre-existing; flagged by both code and reference validators.) - 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 theaz_families_tax_rebate → az_refundable_credits → az_families_tax_rebatecycle; 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. - 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)
- Variable computes a phantom rebate for 2022+ (model-scope gap).
az_families_tax_rebate.py. The one-time nature is enforced only bycredits/refundable.yaml(rebate listed in the 2021 block, removed at 2022-01-01), soaz_income_taxis unaffected outside 2021. Butrebate/amount.yaml,age_threshold.yaml, andmax_dependents.yamlextend forward from 2021-01-01 with no expiry, so a direct query ofaz_families_tax_rebateat 2022+ returns nonzero (visible in API/household-app contexts), and a pre-2021 query hits an undefinedage_threshold. Aperiod: 2022→ 0 test would currently FAIL. Anin_effectguard or end-dated amounts is the standard fix. (Consolidates regulatory G3, code suggestion 2, test G4, and PDF audit c-5.) - Age-17 boundary of the $250/$100 split untested.
age_thresholdis 17 and the formula usesage >= 17→ $100, but the only "older" dependent tested is 19, so anage > 17mutant (a 17-year-old paid $250) survives. Add a dependent aged exactly 17 → $100. - 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). - SEPARATE and SURVIVING_SPOUSE filing statuses have zero coverage.
max_dependents.yamlhas 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>
Fixes applied (commit c65bf4d)Thanks @hua7450 for the review. Addressed the findings: Critical
Should-address
Suggestions
Verification
|
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_rebatepreviously gated onaz_income_tax_before_non_refundable_credits >= 1(tax before credits). It now gates onaz_income_tax_before_refundable_creditsminus 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):
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