Your own dataset
Your own dataset
The engine reads the register through one contract, Cbox\Tax\Contracts\RegisterData.
The default binding is the file store: tax:data:sync compiles a release into
shard files on local disk and Register\Reader\RegisterDataset reads a record at a
time from them. Most applications never touch this.
Bind your own implementation when the register already lives somewhere your application can read directly, for example a database the release is loaded into, and you want a release to be live without compiling a store on every machine.
use Cbox\Tax\Contracts\RegisterData;
$this->app->singleton(RegisterData::class, fn ($app) => new DatabaseDataset(/* … */));
Everything that reads the register follows the binding: the rate source, nexus,
delivery, rounding, sourcing, marketplace rules, purchaser exemptions, the boundary
resolver, the geocoder, the catalogue audit and TaxRegister.
The file store stays bound under its own class name, RegisterDataset, and the
tax:data:* commands keep managing it. When another dataset is bound they print one
line saying so, because what they install, verify or prune is then not what a
calculation reads.
What an implementation must do
- One release per instance.
version()is resolved once and never moves for the life of the object, so two lines of one order are priced by the same release. Another release is another instance, fromat($release). - Records in the register's own shape and order. Rates, jurisdictions and rules are the published records, unfiltered, in publication order. Where a category has more than one candidate rate the resolver takes the first, so order is part of the answer.
- Nothing found is an empty answer. An unknown code has no rates and no
jurisdiction record. Whether a place is covered at all is
carries(). - No release is a refusal.
requireVersion()throwsDatasetNotInstalled; nothing is priced from a guess.
Extend DocumentDataset
A release is a handful of small sections (the manifest, the vocabulary, the rules,
the coverage map) beside two large sets of rows (rates and jurisdictions) and the
boundary files. Register\Reader\DocumentDataset answers every question that can be
derived from the small sections, exactly as the file store does, and leaves you to
say where things are:
| You implement | What it is |
|---|---|
version(), storeRoot(), at($release) |
Which release this instance reads, a locator for refusals, and the same register at another release |
section($name) |
One small section, whole: manifest, meta, rules, standard-rates, coverage, us-local, facts |
ratesFor($code), jurisdiction($code) |
The rows of one jurisdiction |
shards(), codesIn($shard) |
What the release holds, for enumeration: a shard is a regime (eu) or a US state (us/TX) |
boundaryPath($file) |
Where one compiled boundary file is on local disk |
use Cbox\Tax\Register\Reader\DocumentDataset;
class DatabaseDataset extends DocumentDataset
{
public function version(): ?string { /* the release this instance was built for */ }
public function ratesFor(string $jurisdiction): array
{
// Every published rate record of the jurisdiction, in publication order.
}
protected function section(string $name): array
{
// 'manifest' => ['version' => …, 'schemaVersion' => …, 'contentHash' => …, 'regions' => […], 'states' => null]
// 'meta' => ['regimes' => […], 'categories' => […], 'mappings' => […], 'sources' => […]]
// …an empty array for a section the release does not carry.
}
// …
}
Sections are read once per instance and kept. A release is immutable, so anything read for it can also be cached by release for as long as you like.
Boundaries stay files
The ZIP, ZIP+4, street and polygon layers are read a record at a time through an
index, and that is a file format whatever your rows are kept in. boundaryPath()
returns where the compiled file for a state is on local disk (TX.zip,
TX.zip.head.json, KS.street, CA.geo, CA.geo.boxes.json, NE.overlay.json),
whether or not it exists; a state with no file is a state this register does not
place an address in.
If you hold the published boundary artifacts rather than a compiled store,
Register\Compile\BoundaryCompiler turns one artifact into those files. It is the
step tax:data:sync runs on what it downloads:
$compiler = new BoundaryCompiler;
$compiler->postal($zipArtifact, $directory, 'TX'); // TX.zip, TX.zip.idx, TX.zip.head.json
$compiler->street($streetArtifact, $directory, 'KS'); // KS.street, KS.street.idx, KS.street.sets.json
$compiler->geometry($directory, 'CA'); // from CA.geo.json already in $directory
Prove it
Two tools ship in the Testing namespace.
DatasetConformance is the test every implementation must pass. It checks the
rules above without knowing what any rate is:
use Cbox\Tax\Testing\DatasetConformance;
uses(DatasetConformance::class);
it('is a dataset the engine can read', function (): void {
$this->assertDatasetConforms(new DatabaseDataset(/* … */));
});
DatasetComparison proves two datasets return the same answers for a release. It
walks every section, every shard, every jurisdiction's rates and record, the rules
and standard rates of each, the vocabulary, every country's candidate codes, every US
state's local claims and every boundary file:
use Cbox\Tax\Testing\DatasetComparison;
$comparison = DatasetComparison::between($fileStore, $databaseDataset);
expect($comparison->identical())->toBeTrue($comparison->summary());
Lists are compared in order and maps key by key; scalars strictly, so "25" and
25 differ. Every difference is counted, the first 200 are kept in full
(differences()), and patterns() groups all of them by where they are with codes
and indexes removed, for example rates[*][*].provenance.snapshot, which is the
shape a cause has. compared() says how much was walked.
The manifest's compiledAt, files and streets describe a store rather than a
release and are not compared. A street index only one side was built with is listed
under skipped().
The conformance check proves a dataset behaves; it does not prove its data is right. Run the comparison against a file store compiled from the same release for that.
Limits
RegisterData,DocumentDatasetand the store classes follow the register's format and change with it. An application that only asks the register questions should useContracts\TaxRegister, which does not.- The contract has no write side. Loading a release into your own storage, and deciding which release is live, are yours.