Skip to content

Build out local income taxes: registry + Yonkers, Wilmington, KY Louisville/Lexington#8751

Open
DTrim99 wants to merge 3 commits into
PolicyEngine:mainfrom
DTrim99:local-tax-registry-cleanup
Open

Build out local income taxes: registry + Yonkers, Wilmington, KY Louisville/Lexington#8751
DTrim99 wants to merge 3 commits into
PolicyEngine:mainfrom
DTrim99:local-tax-registry-cleanup

Conversation

@DTrim99

@DTrim99 DTrim99 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

What this does

Builds out local income tax capability in two parts.

1. Registry cleanup (metadata)

programs.yaml previously listed only 3 local entries despite many local taxes being modeled. Registers the modeled-but-unregistered taxes so they surface on the coverage page / /us/metadata: Philadelphia wage tax; Kansas City & St. Louis earnings taxes; Indiana county LIT; Maryland county income tax; Multnomah Preschool for All; and the 4 Colorado occupational privilege taxes. (Investigated cleanup items that needed no action: Aurora CO isn't modeled; the Mamdani NYC proposal is correctly reform-only; MD/IN/Multnomah already have tests.)

2. New local taxes on existing geography (residence-only sourcing)

Tax Rate Placement Rolls into
NY Yonkers resident surcharge 16.75% of NY State tax in_yonkers input flag levy aggregate
NY Yonkers nonresident earnings 0.5% of Yonkers-source wages input levy aggregate
DE Wilmington earned income tax 1.25% in_wilmington input flag SALT + levy
KY Louisville/Jefferson Co. occupational 1.45% county_str (consolidated city-county) occupational aggregate
KY Lexington/Fayette Co. occupational 2.25% county_str (consolidated city-county) occupational aggregate

Each: parameters with references, a variable, programs.yaml entry, YAML tests, changelog.

Implementation notes

  • Wiring: local_income_tax is the SALT-deductible measure; local_income_tax_before_refundable_credits is the levy measure that rolls into household tax (Philadelphia is in both). Wilmington is in both. Yonkers is in the levy measure only, deliberately excluded from the SALT measure — its surcharge reads NY State tax, which depends on the federal SALT deduction, so including it in local_income_tax creates a circular dependency. Documented in the variable; minor SALT understatement for the Yonkers portion.
  • Residence-only sourcing: KY occupational taxes are workplace-based; modeled as residence-based per the agreed scope. School-district occupational add-ons (JCPS 0.75%, FCPS 0.5%, both wage-capped) are noted as not-yet-modeled.
  • Rates are from published municipal/state sources (cited in each parameter); confirm against primary ordinances if precision matters.

Verification

  • New YAML tests: 10/10 pass (KY×4, Yonkers×3, Wilmington×3).
  • Regression: existing local employee-tax aggregation tests pass (11/11), including the NYC pre-credit test that initially surfaced the Yonkers cycle.
  • Integration confirmed: Yonkers (+$451 surcharge on $60k NY single) and Wilmington (+$750 = 1.25%×$60k) flow into household_tax; no circular dependency.
  • ruff format + ruff check clean.

Deferred (roadmap)

Larger Tier-1 gaps needing sub-county geography + workplace sourcing — Ohio municipal + school-district, Michigan cities, PA Act 32 EIT — are out of scope here.

🤖 Generated with Claude Code

DTrim99 and others added 2 commits June 25, 2026 17:14
programs.yaml previously listed only 3 local entries (NYC income tax, SF
WFTC, Montgomery County EITC) despite many local taxes being fully modeled.
Add registry entries for the modeled-but-unregistered local taxes so they
appear on the model coverage page / /us/metadata:

- Philadelphia wage tax
- Kansas City and St. Louis earnings taxes
- Indiana county income tax (LIT)
- Maryland county income tax
- Multnomah County Preschool for All tax
- Denver / Glendale / Greenwood Village / Sheridan occupational privilege taxes

No calculation logic changes; registry metadata only. Each entry references
an existing household-facing variable (verified) and matches the existing
entry schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New local income/occupational taxes that fit existing geography
(residence-only sourcing):

- NY Yonkers: resident surcharge (16.75% of NY State tax) + nonresident
  earnings tax (0.5%), gated by an in_yonkers input flag
- DE Wilmington: 1.25% earned income tax (residents on all earnings;
  nonresidents on Wilmington-source earnings), gated by in_wilmington
- KY Louisville/Jefferson County (1.45%) and Lexington/Fayette County
  (2.25%) occupational license fees, assigned via county_str (consolidated
  city-counties). School-district occupational taxes (JCPS/FCPS) not yet
  modeled.

Wiring: Wilmington -> local_income_tax (SALT) + local_income_tax_before_
refundable_credits (levy). KY -> local_occupational_tax. Yonkers ->
local_income_tax_before_refundable_credits only (excluded from the SALT
measure to avoid a circular dependency, since its surcharge reads NY State
tax which depends on the federal SALT deduction). Registered all in
programs.yaml; added YAML tests and a changelog fragment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DTrim99 DTrim99 changed the title Register existing local taxes in the program coverage registry Build out local income taxes: registry + Yonkers, Wilmington, KY Louisville/Lexington Jun 26, 2026
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (194d129) to head (ff7af1e).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #8751   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           11        11           
  Lines          171       121   -50     
=========================================
- Hits           171       121   -50     
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 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Program Review — PR #8751 (Build out local income taxes)

Scope & method

  • Mixed PR: programs.yaml registry (14 entries — 10 existing taxes + 4 new) plus 4 new local taxes (KY Jefferson/Louisville & Fayette/Lexington occupational; NY Yonkers; DE Wilmington).
  • No PDFs (rates are web-sourced), so the value-audit was done via web verification of each rate against official sources, alongside regulatory/reference/code/test validators.
  • CI: all checks passing.

🔴 Critical (Must Fix)

  1. Louisville/Jefferson rate is the nonresident rate, but the tax is modeled residence-based. parameters/gov/local/ky/jefferson/tax/income/occupational/rate.yaml uses 1.45%, which per the official Louisville Metro Form W-1 (TY2025) is the nonresident rate. A Louisville resident's actual occupational rate is 2.20% (1.25% Metro govt + 0.20% TARC + 0.75% JCPS). Since the PR assigns this tax by county of residence (county_str == JEFFERSON_COUNTY_KY), residents are under-taxed. Also the param comment "Louisville Metro occupational license fee" / "Metro only" is wrong: the Metro government portion alone is 1.25%, and 1.45% already includes the 0.20% TARC transit portion. Fix: use 2.20% for residence-based modeling (JCPS included), or relabel as the nonresident rate and correct the TARC note.
  2. Fayette reference URL is a dead link (404). parameters/gov/local/ky/fayette/tax/income/occupational/rate.yaml and variables/.../ky_fayette_occupational_tax.py both cite lexingtonky.gov/departments/finance/division-revenue, which 404s — not traceable. The value (2.25%) is correct; the working source is https://www.lexingtonky.gov/departments/revenue (states "2.25% occupational license fee") and the rate-schedule page. Update both files.

🟡 Should Address

  1. No end-to-end household-tax rollup test. Every test asserts only the leaf variable it sets; nothing asserts household_tax, local_income_tax_before_refundable_credits, or local_occupational_tax. A typo/removal in an adds list would go uncaught. Add at least one integration case per aggregate.
  2. No SALT-asymmetry regression test. The PR deliberately adds Wilmington to local_income_tax (SALT-deductible) but excludes Yonkers (to avoid a circular dependency). That asymmetry is exactly what needs a guard test (Wilmington appears in the federal SALT base; Yonkers does not) — none exists.
  3. Yonkers & Wilmington add the nonresident-earnings term without gating on ~resident. A resident with *_nonresident_earnings set would be charged BOTH the resident tax/surcharge AND the nonresident tax. Harmless given normal inputs, but defensively gate the nonresident term to non-residents.
  4. Reference specificity. Yonkers (tax.ny.gov/...nyc_yonkers_residents.htm), Wilmington, and Jefferson references are landing pages that don't print the rate. Cite the value-bearing sources: Yonkers Form Y-203-I (0.5%) and IT-360.1 instructions (16.75%); a Louisville rate schedule/ordinance; the Wilmington ordinance or a page that shows 1.25%.
  5. Test gaps: Fayette has no out-of-county/zero case (Jefferson does); self-employment inclusion untested (Jefferson/Fayette/Wilmington); SE-loss guard only tested for Fayette; multiple-earner aggregation untested.
  6. Code style: documentation field used instead of reference on the 4 input variables; the 4-line earnings block (max_(employment + self_employment, 0)) is duplicated across 3 files — factor it / use add(); defined_for inconsistent (KY sets StateCode.KY; Yonkers/Wilmington set none).

🟢 Suggestions

  • resident_surcharge_rate.yaml: 0.167500.1675 (trailing zero).
  • Verify the param comment's FCPS/JCPS wage-cap claim — the FCPS page did not confirm a cap; Louisville Metro has no wage cap.
  • Document the residence-only modeling limitation (KY occupational and the Yonkers/Wilmington nonresident pieces are workplace-based in law).

Value Audit (rates)

Jurisdiction Repo Official Verdict
Louisville/Jefferson KY 1.45% 2.20% resident / 1.45% nonresident MISMATCH (residence-modeled)
Lexington/Fayette KY 2.25% 2.25%
Yonkers resident surcharge 16.75% 16.75% of net NY tax
Yonkers nonresident 0.5% 0.5%
Wilmington DE 1.25% 1.25%

Validation Summary

Check Result
Regulatory / rate accuracy 1 critical (Louisville rate)
Reference quality 1 critical (Fayette 404) + landing-page specificity
Code patterns / aggregation ✅ wiring correct; 3 should (style/DRY/defined_for)
Test coverage 0 untested vars; missing rollup + SALT-asymmetry + edge cases
CI ✅ passing

Review Severity: REQUEST_CHANGES

Two must-fix items: the Louisville residence rate (2.20% vs 1.45%) and the Fayette 404 reference. Aggregation wiring and the SALT/cycle workaround are sound. Other rates verified correct.

Next Steps

To auto-fix: /fix-pr 8751


🤖 Generated with Claude Code

…ting, tests

- CRITICAL: Louisville/Jefferson occupational rate 1.45% -> 2.20% (the tax is
  assigned by residence; 2.20% = 1.25% Metro + 0.20% TARC + 0.75% school board.
  1.45% was the nonresident rate.) JCPS 0.75% wage cap not modeled (noted).
- CRITICAL: fix Fayette dead reference URL (.../finance/division-revenue ->
  .../departments/revenue) in both the param and the variable.
- Gate the Yonkers/Wilmington nonresident-earnings term on non-residence so a
  resident is never charged both the resident and nonresident amounts.
- Add defined_for (NY/DE); drop disallowed documentation field on input vars;
  point Yonkers refs to Form IT-360.1-I / Y-203-I; update programs.yaml note.
- Tests: Jefferson now 1,100 (50k*2.2%); add end-to-end household-tax rollup
  and SALT-asymmetry (Wilmington in local_income_tax, Yonkers out) tests, plus
  Fayette zero-case, self-employment, and multi-earner cases. 24 pass.

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

DTrim99 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes Applied (commit ff7af1eb3d)

Addresses the review.

Critical

  • Louisville/Jefferson rate 1.45% → 2.20%. The tax is assigned by residence, and 1.45% is the nonresident rate; the resident rate is 2.20% (1.25% Metro + 0.20% TARC + 0.75% school board), confirmed against the official Louisville Metro W-1KJC 2025 instructions. The JCPS 0.75% wage cap isn't modeled (flat 2.20% slightly overstates earners above that cap) — noted in the param.
  • Fayette dead reference URL fixed in both the parameter and the variable (.../finance/division-revenue.../departments/revenue).

Should

  • Gated the nonresident-earnings term on non-residence for Yonkers and Wilmington, so a resident is never charged both the resident tax/surcharge and the nonresident tax.
  • Added defined_for (NY/DE); dropped the disallowed documentation field on the input variables; pointed Yonkers references to Form IT-360.1-I / Y-203-I; updated the programs.yaml Jefferson note.
  • Tests: Jefferson now 1,100 (50k × 2.2%); added an end-to-end household-tax rollup test (guards the adds wiring) and a SALT-asymmetry test (Wilmington in local_income_tax, Yonkers excluded but in the levy aggregate), plus Fayette zero-case, self-employment, and multi-earner cases.

Verification

  • 24/24 local-tax tests pass (py 3.11), including the new rollup and SALT-asymmetry tests; ruff clean.

Not changed (suggestions)

  • The duplicated per-person earnings block was left inline — Yonkers/Wilmington use different earnings inputs and no trivial shared helper preserves the required per-person max_ floor.

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.

1 participant