TaxRateCollector — Project Bible
Single source of truth for what TaxRateCollector IS, is NOT, and the rules that keep it coherent. README.md says how to build/run; this says how to think about the system.
1. The one sentence
TaxRateCollector is a Blazor Server application that maintains a provably accurate, evidence-backed master table of US sales and excise tax rates for every jurisdiction (Country → State → County → City), binding every rate to the hashed, timestamped government source document it came from.
2. The product promise
- Evidence on every row. Each authoritative rate is bound to a
SourceDocumentholding the raw.govartifact, its SHA-256 content hash, and aFetchedAttimestamp. A rate is independently verifiable without a live network call. See Law 1. - Every US jurisdiction. A self-referential Country → State → County → City hierarchy (
Jurisdiction) seeded from US Census Bureau gazetteers, with ZIP→jurisdiction lookup over ~33,000 ZCTAs. - SSUTA-aligned product taxonomy. Products are classified against the Streamlined Sales & Use Tax Agreement Appendix C taxonomy (
TaxCategory), with documented per-state overrides for non-member states. - Continuous re-scraping with change detection. A background host re-checks each jurisdiction's official source on a cadence; the
DiffEnginewrites aChangeLogEntrythe moment a rate moves. See Law 4. - Pay only for the states you bill in. Subscribers pick states (and categories) at a per-unit price; PayPal handles checkout.
- Plugs into a billing stack. Blazor Server UI plus CSV / XLSX / SQL / HTML exports.
3. What it is NOT
- NOT a live tax-calc API for third parties. There is currently no public REST/GraphQL endpoint — consumers use the UI and file exports. A REST/GraphQL endpoint is a backlog item (TRC-US-G1), not a shipped feature.
- NOT a "best guess" rate oracle. A rate without attached, hash-verified evidence is not considered validated for export. "Our API said so" is explicitly rejected as a compliance answer. See Law 1.
- NOT a SQLite app. The system runs on SQL Server (LocalDB in dev, SQL Server / Azure SQL in prod) via EF Core 10. There is no SQLite path in the application (the unit-test project uses the SQLite in-memory provider only as a relational test double). See Law 8.
- NOT a hard-delete system. Rate history is retired (
IsCurrent=false), never destroyed; jurisdictions are deactivated (IsActive=false), not deleted. See Law 2 and [HOUSE-LAW-2]. - NOT vendor-locked to one LLM. AI rate extraction routes through MindAttic.Legion, not a hard-coded vendor SDK. See Law 3.
- NOT a flat-percentage tax engine. It models excise structures (per-unit, per-volume, per-proof-gallon, per-weight, percentage-of-wholesale), brackets, caps, compound tax-on-tax, ABV gating, and origin/destination sourcing. See §4.2.
4. Architecture canon
+------------------------------+
| TaxRateCollector.Blazor | Blazor Server UI (InteractiveServer),
| (front door: interactive) | pages, exports, DI composition root
+---------------+--------------+
|
+------------------------------+------------------------------+
| | |
+--v---------------+ +----------v-----------+ +--------------v-------+
| TaxRateCollector | | TaxRateCollector | | TaxRateCollector |
| .Worker | | .Infrastructure | | .UnitTests |
| (front door: | | EF Core 10, scrapers,| | NUnit 4, InMemory + |
| background) | | seeders, services | | LocalDB integration |
+--------+---------+ +----------+-----------+ +----------------------+
| |
| +----------v-----------+
+------------>| TaxRateCollector.Core| Entities, Enums, Interfaces, Options
| (NOUNS + contracts) |
+----------------------+
|
+-------v--------+
| SQL Server | LocalDB (dev) / Azure SQL (prod)
+----------------+
The Blazor host and the Worker are two front doors over one engine (HOUSE-LAW-6): both register the same Core + Infrastructure graph and read the same connection string. The Worker exists so unattended re-scraping does not compete with interactive traffic.
4.1 Projects
| Project | Responsibility |
|---|---|
TaxRateCollector.Core |
Domain entities, enums, interfaces, options. No EF, no I/O. |
TaxRateCollector.Infrastructure |
EF Core AppDbContext, migrations, seeders, importers, scrapers, services. The timestamped migrations in Migrations/ are the authoritative schema history (dotnet ef migrations list). |
TaxRateCollector.Blazor |
Blazor Server UI, pages, exports, DI composition root, startup migrate→seed. |
TaxRateCollector.Worker |
Background host (MonthlySchedulerService, ScrapeJobWorker) for unattended re-scrapes. |
TaxRateCollector.UnitTests |
NUnit 4 — unit + LocalDB integration (Category=Integration). |
4.2 Domain model (NOUNS)
| Entity | Role |
|---|---|
Jurisdiction |
Self-referential hierarchy node (ParentId); JurisdictionType Country/State/County/City; FipsCode, StateCode, SourceUrl, IsActive, IsHomeRuleAdministered. |
TaxRate |
One rate law: Rate + RateBasis, TaxType, brackets (Min/MaxTaxableAmount), FlatCapPerUnit, IsCompound, IsIncludedInPrice, ABV gating, IsCurrent, SourceConfidence. |
TaxCategory |
SST product/service taxonomy node (Goods/Services tree). |
SourceDocument |
Evidence artifact: raw content, SHA-256 ContentHash, SourceUrl, FetchedAt, SourceType. |
ExciseTaxRate |
Excise/specific taxes (alcohol, tobacco, cannabis, fuel, hotel). |
StateTaxProfile |
Per-state metadata: general rate, SST membership, LocalTaxAuthorityType, revenue-agency name + URL. Authoritative home: StateTaxProfileSeeder. |
ScrapeRun |
Metadata for a scrape/import batch (status, counts, pause/resume). |
ZipCodeRecord |
ZIP → (State, County, City) jurisdiction junction. |
ChangeLogEntry |
A detected rate change (old → new), written by the diff engine. |
Subscriber / SubscribedState / SubscribedCategory / BillingRecord |
Subscription + billing. SubscribedCategory gates per-category access; a subscriber needs both SubscribedState and SubscribedCategory for a state+category combination. |
ZipCodeDistrict |
Junction mapping a ZIP to one or more overlapping special taxing district jurisdictions (e.g. RTA, MPEA in Chicago). One ZIP → many ZipCodeDistrict rows. |
JurisdictionData (ICanonEntity) |
JSON-first canon projection of a jurisdiction's rate + provenance (string GUID id, type, aliases). |
4.3 Key services (VERBS)
| Service / Interface | Responsibility |
|---|---|
ITaxCalculator → TaxCalculator |
Walks the jurisdiction chain, sums rate lines, applies basis/brackets/caps/compound/sourcing. |
IScrapeOrchestrator → ScrapeOrchestrator |
Loops active jurisdictions, dispatches to the right IScrapeStrategy. |
IScrapeStrategy |
One plug-in per source format: CaliforniaCsvScraper, IllinoisTableScraper, TexasExcelScraper. |
IStateBulkScraper |
Per-state bulk excise/sales scrapers (Wisconsin, Illinois, Minnesota, Iowa, Indiana, Michigan, the Dakotas, Ohio, Montana, Idaho, Oregon). |
IDiffEngine → DiffEngine |
Compares new scrape to current IsCurrent rows; emits ChangeLogEntry rows. |
IRecursiveRateScraper → RecursiveRateScraper |
Walks a jurisdiction hierarchy (State → County → City → District), discovers and AI-extracts rate laws with evidence via IRateLawExtractor, persists new TaxRate rows flagged NeedsReview=true. |
IRateLawExtractor (defined in IRecursiveRateScraper.cs) → ClaudeRateLawExtractor |
AI extraction of structured rate laws from raw source content, via MindAttic.Legion LegionClient (Law 3); StubRateLawExtractor is the keyless fallback. |
IEvidenceFileStore → EvidenceFileStore |
Hashes + stores uploaded/captured evidence under %APPDATA%\MindAttic\TaxRateCollector\evidence\. |
ICensusJurisdictionImportService / IZipImportService / ISstTaxonomyImportService |
Setup-pipeline importers (Census gazetteers, ZCTA crosswalks, SSUTA PDF). |
SettingsService |
Reads/writes %APPDATA%\MindAttic\TaxRateCollector\settings.json; resolves the Anthropic key from the shared MindAttic credential store (Law 5, HOUSE-LAW-3). |
ScrapeWorkerService / ScrapeJobCoordinator |
Hosted background re-scrape execution + coordination. |
IPayPalService → PayPalService |
Subscription checkout + webhook billing. |
5. The Laws
This project inherits the MindAttic House Rules at
../MindAttic.HouseRules.md(HOUSE-LAW-1…HOUSE-LAW-9) by reference. They are not restated here. Relevant inherited laws: [HOUSE-LAW-1] whole-number versioning · [HOUSE-LAW-2] soft-disable never hard-delete · [HOUSE-LAW-3] credentials via MindAttic.Vault · [HOUSE-LAW-4] provider-agnostic LLMs via MindAttic.Legion · [HOUSE-LAW-6] one engine, many front doors · [HOUSE-LAW-8] done is verified · [HOUSE-LAW-9] psst only on request.The laws below are TaxRateCollector-specific and govern correctness of tax data.
TRC-LAW-1 — No rate without evidence
Every authoritative TaxRate must be traceable to a SourceDocument whose ContentHash = SHA256(RawContent) and whose FetchedAt + SourceUrl make it independently verifiable offline. A rate lacking hash-verified evidence is not valid for export. "The API said so" is not a compliance answer.
TRC-LAW-2 — Rates are retired, never overwritten
A rate change sets the prior row IsCurrent=false and inserts a new IsCurrent=true row; there is exactly one current row per (JurisdictionId, TaxCategoryId[, ProductCategory]). Jurisdictions deactivate (IsActive=false), never delete. (Specialises [HOUSE-LAW-2].) (verified by TaxRate_OnlyOneCurrentRatePerJurisdiction.)
TRC-LAW-3 — LLM extraction routes through MindAttic.Legion
AI rate-law extraction calls go through MindAttic.Legion (LegionClient); no code path embeds a vendor SDK directly, and a missing key degrades gracefully to StubRateLawExtractor (no throw). (Specialises [HOUSE-LAW-4].)
TRC-LAW-4 — Every rate change is logged
Whenever the DiffEngine detects a changed, removed, or structurally-altered rate versus the current run, it writes a ChangeLogEntry. Unchanged rates produce no entry. (verified by RateChanged_CreatesRateChangedEntry, UnchangedRate_CreatesNoEntry, StructuralChange_RateBasisChanges_CreatesStructuralChangeEntry.)
TRC-LAW-5 — Secrets live in the MindAttic credential store, never in code
API keys (Anthropic, USPS, PayPal) resolve through SettingsService from %APPDATA%\MindAttic\... (the shared MindAttic credential store) or IConfiguration — never hard-coded or committed. (Specialises [HOUSE-LAW-3].) (URL/settings defaults verified by AppSettingsTests.)
TRC-LAW-6 — Taxes are cumulative up the chain
A point-of-sale rate is the sum of every applicable tier (State + County + City + districts), filtered by category taxability and sourcing rule. ZIP codes carry no rate themselves — they are a lookup index into the jurisdiction hierarchy. A row whose source publishes a combined point-of-sale rate (state + local in one figure: the CA/IL/TX statewide strategies) is stored with IsCombinedRate; it stands in for itself and its ancestors' sales-tax percentages, so the chain never adds the state/county rates on top of it again (tiers below it and non-sales-tax rows still stack). (verified by CombinedRate_City_SumsStatePlusCountyPlusCity, CombinedRate_StateOnly_ReturnsSingleRate.)
TRC-LAW-7 — Idempotent seeders and importers
Startup seeders and Setup-pipeline importers check before inserting and may be re-run without duplicating data. (verified by SeedAsync_IsIdempotent, Hierarchy_SeederDoesNotSeedIfCountryExists.)
TRC-LAW-8 — SQL Server is the only datastore
The system targets SQL Server (LocalDB dev / Azure SQL prod) via EF Core 10 (UseSqlServer, AppDbContextFactory). The hierarchy's filtered indexes and the sqlpackage .bacpac export on the Setup page assume SQL Server. There is no SQLite path in the application.
6. Verified state
Build: dotnet build TaxRateCollector.slnx -c Debug → Build succeeded, 0 errors (verified 2026-10-03).
Tests: dotnet test TaxRateCollector.UnitTests --filter "Category!=Integration" → 748 passed / 0 failed / 748 total (verified 2026-10-03). Integration tests (Category=Integration) were not run here — they require SQL Server LocalDB with migrations applied — so anything depending solely on them stays 🟡.
Proven working (✅, each cited in USER_STORIES):
- Cumulative rate calculation across tiers and all excise bases/brackets/caps/compound/sourcing —
TaxCalculatorTests(both suites),HierarchyTests. - Evidence hashing + integrity (SHA-256, tamper detection, MIME mapping, base64 PDF round-trip) —
EvidenceValidationTests. - Change detection (new/changed/removed/structural) —
DiffEngineTests. - SST taxonomy structure + idempotent category/profile seeding —
SstTaxonomyStructureTests,TaxCategorySeederTests. - Scrape strategies CA/IL/TX parse + skip invalid rows —
StrategyScraperTests. - Billing math (per-state pricing, tax-on-subscription, precision) —
BillingCalculationTests. - Settings defaults + URL validity —
AppSettingsTests. - Evidence-store type detection and HTML→zip bundling —
EvidenceFileStoreTests. - Alert acknowledge-all flow —
AlertServiceTests.
7. Active frontier
- RFC: docs/rfc/0001-sst-bulk-scraper.md — one shared scraper for the 24 SSUTA member states instead of 24 near-identical classes.
- Epics / backlog: see docs/USER_STORIES.md — coverage expansion to all ~3,144 counties / ~10,000+ cities (Epic B), evidence capture + Wayback fallback (Epic C), shared SST scraper (Epic E), public rate API (Epic G).
8. Quality bar
A feature is done ([HOUSE-LAW-8]) only when:
dotnet build TaxRateCollector.slnxis clean (0 errors).- New behaviour has NUnit coverage and the suite is green (
Category!=Integrationat minimum;Category=Integrationfor DB-shaped work). - Any rate-producing path attaches hash-verified evidence (TRC-LAW-1) and logs changes (TRC-LAW-4).
- Secrets resolve through the credential store, not literals (TRC-LAW-5).
- Docs updated: a
✅story names its verifying test; unproven work stays🟡/⬜.
9. Glossary
- Jurisdiction — a node in the Country → State → County → City hierarchy.
- SSUTA / SST — Streamlined Sales & Use Tax Agreement; its Appendix C is the canonical product taxonomy; 24 member states apply it uniformly.
- Evidence / provenance — the raw government artifact (
SourceDocument) plus its SHA-256 hash, source URL, and fetch timestamp that proves a rate. - RateBasis — how a rate is applied: Percentage, FlatPerUnit, FlatPerVolume, FlatPerWeight, FlatPerProofGallon, PercentageOfWholesale.
- IsCurrent — flags the single live rate row per jurisdiction+category; earlier rows are retained with
IsCurrent=false. - IsIncludedInPrice — true when a tax is remitted upstream and already embedded in the retailer's cost (must not be re-added to the customer invoice).
- IsCompound — true when a rate applies to (price + other taxes), i.e. tax-on-tax.
- Home rule — a local jurisdiction that administers/collects its own sales tax independently of the state (CO, AL, LA).
- Sourcing (origin/destination/modified) — which location's rate applies for an intrastate sale.
- ZCTA — ZIP Code Tabulation Area; the Census geography that backs ZIP → jurisdiction lookup.
- Front door — an executable entry point (Blazor host, Worker) over the shared Core+Infrastructure engine ([HOUSE-LAW-6]).