Skip to content

Releases: cboxdk/tax-resolver

v1.0.0 — the API is settled

Choose a tag to compare

@sylvesterdamgaard sylvesterdamgaard released this 16 Sep 17:17

No code changed from v0.2.0. What changed is the commitment: Authority, JurisdictionAssignment, BoundaryData, Geometry and Resolver are stable surface now, and breaking any of them needs a v2.

That is worth committing to because the shape has been used rather than merely published. cboxdk/laravel-tax resolves US addresses through this package, and the register's own conformance deck — addresses in, expected authority set out, cut from the artifacts a release actually ships — passes through it.

Two readers of one format drifting apart is the failure this package exists to prevent, and it is the failure v0.1.0 shipped: it read a formatVersion 3 artifact as a v2 one, dropped every authority, and answered "no local authority levies here" for every address in all twenty-four Streamlined states. Its own suite stayed green the whole time, because every fixture had been hand-built to match the parser rather than the publisher. A deck that both sides run is the only thing that proves they agree.

What you are committing to as a consumer

  • An authority is level AND code. A county and a special district can file under the same number and levy separately, so matching on the bare code merges two bodies that each want their own share.
  • null is not []. Null means no layer answered — fall back to the state rate. An empty list means a row answered "no local authority levies here", which is an answer. Use resolved() rather than checking emptiness.
  • An unknown formatVersion throws. A version this code cannot read has to stop: the alternative is a confident answer that is wrong in the direction nobody audits.

Upgrading from v0.1.0

$assignment->authorities is list<Authority>|null, not list<string>|null. Read $authority->key() ('county:209') or join on {stateFips, $authority->level, $authority->code}.

v0.2.0 — read formatVersion 3, and refuse what it cannot read

Choose a tag to compare

@sylvesterdamgaard sylvesterdamgaard released this 16 Sep 10:52

Breaking. JurisdictionAssignment::$authorities is now list<Authority>|null, was list<string>|null.

The bug this fixes

An authority set in the boundary format is [{level, code, type?}]. BoundaryData::sets() kept an entry only when it was a bare string — the v2 shape — so against a v3 artifact every entry was dropped and every set came out empty.

An empty set is not nothing in this format. It means a row covers this address and no local authority levies here. So v0.1.0 reported no local tax, with no error, for every address in all twenty-four Streamlined states. Against the real Kansas artifact that is 1029 sets, every one of them wrong.

The suite was green throughout, because every fixture had been hand-built to match the parser's assumption rather than what the register publishes. tests/Fixtures/ks.v3.json is now a genuine slice of boundaries/KS from register release 2026.09.15-202, so that cannot happen the same way twice.

Changed

  • BoundaryData::resolveZip(), resolveStreet() and Geometry::authoritiesAt() return Authority objects.
  • BoundaryData::fromArtifacts() and Geometry::fromFeatureCollection() throw UnsupportedFormatVersion for a version they do not implement, instead of reading what they can. A version this code cannot read has to stop: the alternative is a confident answer that is wrong in the direction nobody audits.

Added

  • Authority — level, code, optional type (the state's own district-type code: 79 and 26 in North Carolina, 63 in Minnesota, 02 in Vermont), and jurisdiction, filled only by the geometry layer, which publishes the register's own code.
  • Authority::key() and is(). Identity is level and code together, never the bare number — a county and a special district can file under the same number and levy separately, so matching on 209 alone merges two bodies that each want their own share.
  • UnsupportedFormatVersion.

Upgrading

Anywhere you read $assignment->authorities as strings, read $authority->key() ('county:209') or join on {stateFips, $authority->level, $authority->code}. Use $assignment->resolved() rather than checking for an empty array — null and empty mean opposite things.