tax — module #17, the first module with zero v1 precedent
3 tables, 47 columns — schema-locked 2026-07-07. tax calculates & durably decomposes tax owed — closing a live, already-logged gap: pos's own 19-to-9 audit found sale_line.tax_amount_cents/tax_rate are a flat aggregate with no per-jurisdiction breakdown. v1 never had a Tax module at all — v1's architecture assumed tax was 100% outsourced to Stripe Tax (confirmed via find docs/old -iname "*tax*" returning zero results; Payments' MODULE_INDEX row: "tax → Stripe Tax"; Admin's: "tax config (Stripe Tax)"). This is disclosed honestly rather than fabricating a v1 diff — Design-Phase Integrity Blocks 1–3 report 0 tables → 2 tables, not a migration. Depends on platform, identity (actor), multi_loc (site), shared (currency), crm (customer, customer_tax_certificate — the exemption honored), pos (sale_line — the finalized decomposition target), orders (order_line — the estimate target).
PROJECT_DECISIONS entry: #31.
Global rules for this schema:
- This module NEVER computes a tax rate. Stripe Tax (external) does;
taxstores the RESULT, durably, per jurisdiction. No local rate/jurisdiction master tables — deliberately thin, avoiding duplication of what the vendor (Stripe Tax) and Admin (nexus/registration config) already own. - Uniform tenant-scoping — both tables
tenant_id NOT NULLFK →platform.tenant, RLS with<table>_tenant_isolationpolicy. - Asymmetric mutability, by design.
tax_calculation(the header) hasupdated_at/deleted_at(soft-delete, mutable in the ordinary sense).tax_calculation_jurisdiction(the per-jurisdiction breakdown) has neither — append-only, "tax records are legal records" (v1's own framing, now enforced by the table shape, not just asserted). automation_sourcedefaults'system'— the FIRST justified deviation in the entire codebase from the universal'human'default (confirmed live: every other module'sautomation_sourcecolumn defaults'human', zero exceptions before this). A tax calculation is a deterministic, externally-computed result, never a human action recorded after the fact.source_refis polymorphic —tax_calculation.source_refpoints ATpos.sale_line.idororders.order_line.id(link-don't-require-reciprocal, matchingbilling.ar_charge's own established pattern intopos.sale/orders.order_header) — no reciprocal column needed on either locked table, no reopen.supersedes_calculation_idis scoped to same-source_refcorrections only — DB-enforced, not just documented. A miscalculated tax for the SAME line gets a new row that supersedes the old one (mirrorspricing.price_rule's supersede-don't-edit pattern).trg_tax_calculation_validate_supersession(aBEFORE INSERT OR UPDATE OF supersedes_calculation_idtrigger, mirroringpricing.trg_price_rule_validate_supersessionexactly) rejects any attempt to setsupersedes_calculation_idto a row with a differenttenant_id/source_module/source_type/source_ref— live-tested (test F3): a cross-source_refsupersede attempt is rejected with a raised exception. It does NOT and cannot bridge an orders-time estimate to its later pos-time final — those coexist as separate rows, reconciled only at report time via the header-levelorder_header.fulfilled_sale_idjoin (resolved decision 1, option b — see PROJECT_DECISIONS #31 for the adversarial finding that caught an earlier over-claim here, and the follow-up independent verification that caught the scoping wasn't yet DB-enforced, closed by this trigger).
Cross-Phase Foreign Keys (tax)
| Column | Target | Notes |
|---|---|---|
*.tenant_id (both) |
platform.tenant |
NOT NULL |
tax_calculation.site_id |
multi_loc.site |
NOT NULL |
tax_calculation.customer_id |
crm.customer |
nullable, denormalized |
tax_calculation.applied_exemption_certificate_id |
crm.customer_tax_certificate |
nullable — the exemption honored, if any |
tax_calculation.currency_code |
shared.currency.iso_code |
char(3), NOT NULL, no default |
tax_calculation.source_ref |
pos.sale_line.id or orders.order_line.id |
polymorphic (plain uuid, no single FK — validity enforced by the source-pair CHECK); NULL only when source_module='manual' |
tax_calculation.supersedes_calculation_id |
tax.tax_calculation.id (self) |
nullable, same-source_ref corrections only |
tax_calculation.created_by_actor_id, .reviewed_by_actor_id |
identity.actor |
nullable |
tax_calculation_jurisdiction.tax_calculation_id |
tax.tax_calculation.id |
NOT NULL |
tax_calculation.provider_ref |
— (deferred, Payments) | plain nullable text, no FK — Stripe Tax's own calc ID, Payments not built |
tax_calculation.entity_id |
platform.legal_entity |
nullable — Remediation Phase 4, Item 15; NULL = tenant's primary entity |
tax_calculation_jurisdiction.jurisdiction_level_id |
tax.jurisdiction_level_catalog |
nullable — Remediation Phase 4, Item 17c; additive-interim, not yet synced with the jurisdiction_level CHECK-enum |
tax.tax_calculation (31 cols, FULL)
Header: one row per calculation event. Full autonomy pack — the nexus/rate-anomaly-flagging review seam, never a rate-computing one.
Tenant-scoped. RLS enabled —
tax_calculation_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
site_id |
UUID | NOT NULL | — | FK → multi_loc.site |
source_module |
text | NOT NULL | — | CHECK IN (pos,orders,manual) |
source_type |
text | NOT NULL | — | CHECK IN (sale_line,order_line,adjustment,sale_refund_line); valid (module,type) pairs enforced |
source_ref |
UUID | nullable | — | → pos.sale_line/orders.order_line/pos.sale_refund_line; NOT NULL unless manual (CHECK) |
customer_id |
UUID | nullable | — | FK → crm.customer |
applied_exemption_certificate_id |
UUID | nullable | — | FK → crm.customer_tax_certificate |
is_exempt |
boolean | NOT NULL | false |
|
provider |
text | NOT NULL | — | CHECK IN (stripe_tax,manual,exempt) |
provider_ref |
text | nullable | — | DEFERRED — Stripe Tax's own calc ID, Payments not built |
tax_treatment |
text | NOT NULL | — | CHECK IN (inclusive,exclusive) — mirrors Pricing's field |
taxable_amount_cents |
bigint | NOT NULL | — | CHECK sign-aware: original ⇒ >= 0, reversal ⇒ <= 0 (Remediation Phase 3) |
total_tax_amount_cents |
bigint | NOT NULL | — | CHECK sign-aware, same pair as above; must equal SUM(jurisdiction rows) (documented, live-tested) |
currency_code |
char(3) | NOT NULL | (none) | FK → shared.currency.iso_code |
calculated_at |
timestamptz | NOT NULL | — | |
is_estimate |
boolean | NOT NULL | false |
true = orders-time estimate; coexists with, never auto-superseded by, a pos-time final |
supersedes_calculation_id |
UUID | nullable | — | Self-FK, same-source_ref corrections only |
calculation_type |
text | NOT NULL | 'original' |
CHECK IN (original,reversal) — Remediation Phase 3 |
reversed_calculation_id |
UUID | nullable | — | Self-FK → tax.tax_calculation.id; NOT NULL iff calculation_type='reversal' (CHECK) — Remediation Phase 3 |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor |
automation_source |
text | NOT NULL | 'system' |
CHECK IN (human,agent,system,seed) — the codebase's first non-human default |
review_status |
text | NOT NULL | 'not_required' |
CHECK IN (not_required,pending,approved,rejected) |
review_reason |
text | nullable | — | |
reviewed_by_actor_id |
UUID | nullable | — | FK → identity.actor; cannot equal created_by_actor_id |
reviewed_at |
timestamptz | nullable | — | |
decision_provenance |
jsonb | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
entity_id |
UUID | nullable | — | FK → platform.legal_entity; NULL = tenant's primary entity — Remediation Phase 4, Item 15 |
CHECK constraints (14): chk_tax_calculation_source_module; chk_tax_calculation_source_type (widened, Remediation Phase 3, to add sale_refund_line); chk_tax_calculation_source_pair (valid module/type combos; widened, Remediation Phase 3, to add (pos, sale_refund_line)); chk_tax_calculation_manual_no_source_ref (manual⇒NULL source_ref, pos/orders⇒NOT NULL — the idempotency-adjacent guard, mirrors billing.ar_charge's identical pattern); chk_tax_calculation_provider; chk_tax_calculation_tax_treatment; chk_tax_calculation_taxable_amount_signed (Remediation Phase 3, replaces the old _nonneg CHECK — sign-aware by calculation_type); chk_tax_calculation_total_tax_amount_signed (same replacement, Remediation Phase 3); chk_tax_calculation_automation_source; chk_tax_calculation_review_status; chk_tax_calculation_reviewer_not_creator; chk_tax_calculation_exempt_requires_evidence (Remediation Phase 1); chk_tax_calculation_calculation_type (Remediation Phase 3); chk_tax_calculation_reversal_requires_target (Remediation Phase 3).
Indexes: PK; tenant_id; (tenant_id, source_module, source_type, source_ref) — lookup by source; (tenant_id, customer_id) WHERE not null; review_status='pending' partial (the anomaly-flag queue).
Triggers (3): set_updated_at (standard); trg_tax_calculation_validate_supersession (BEFORE INSERT OR UPDATE OF supersedes_calculation_id, calls tax.validate_tax_calculation_supersession()) — DB-enforces the same-source_ref supersession scoping described above. A CHECK cannot express this (it requires comparing against a DIFFERENT row); mirrors pricing.trg_price_rule_validate_supersession exactly. trg_tax_calculation_validate_reversal (BEFORE INSERT OR UPDATE OF reversed_calculation_id, calls tax.validate_tax_calculation_reversal()) — Remediation Phase 3, see below.
Remediation Phase 1 (2026-07-08)
A cross-module senior-architect review found tax_calculation could record is_exempt = true with no evidence backing the exemption — a fail-open gap (a calculation could simply assert tax-exempt status with nothing on record to justify it). Closed by a new CHECK constraint:
chk_tax_calculation_exempt_requires_evidence—is_exempt = truenow requires eitherapplied_exemption_certificate_id IS NOT NULLorprovider = 'exempt'(live-verified shape). No column or table count change (28/9/37 unchanged).
Full cross-module record: PROJECT_DECISIONS #37.
Remediation Phase 3 (2026-07-08)
Phase 3, Item 9 closed a live gap: a refund's tax was previously nowhere represented at all — tax_calculation only ever recorded the original sale's tax, so remitting SUM(total_tax_amount_cents) over-reported every time a refund happened (the tax on returned goods was never backed out). tax_calculation gained 2 columns:
calculation_type(text, NOT NULL DEFAULT'original') — CHECK IN (original,reversal).reversed_calculation_id(uuid, nullable, self-FK →tax_calculation.id) — the same self-FK-workaround pattern already used bysupersedes_calculation_id.chk_tax_calculation_reversal_requires_targetenforces(calculation_type = 'reversal') = (reversed_calculation_id IS NOT NULL).
chk_tax_calculation_source_type and chk_tax_calculation_source_pair were widened to admit sale_refund_line (paired with source_module='pos') — a refund line is now a valid tax-calculation source in its own right, alongside sale_line/order_line/adjustment.
The old taxable_amount_cents/total_tax_amount_cents >= 0 CHECKs were replaced with sign-aware pairs, chk_tax_calculation_taxable_amount_signed / chk_tax_calculation_total_tax_amount_signed: original rows stay >= 0 (byte-identical old behavior, zero risk to the 54 live rows, all calculation_type='original' by DEFAULT), reversal rows must be <= 0. This lets SUM(original + reversal) net to exactly zero for remittance reporting with no per-row branching — the entire point of this item.
A new trigger, trg_tax_calculation_validate_reversal (BEFORE INSERT OR UPDATE OF reversed_calculation_id), same-tenant-validates any reversal link, mirroring the pre-existing trg_tax_calculation_validate_supersession — but deliberately does not also require matching source_module/source_type/source_ref the way supersession does. A reversal's source_ref (the refund line) is architecturally always different from what it reverses (the original sale line) — unlike a same-line supersession correction, where a mismatch is a bug.
Full cross-module record: PROJECT_DECISIONS #39. Migration: packages/db/migrations/20260708220000_phase3_item9_refund_tax_reversal.sql.
Remediation Phase 4 (2026-07-08)
Phase 4, Item 15 (Legal Entity) added a new nullable FK column to tax_calculation:
entity_id(uuid, nullable, FK →platform.legal_entity.id) — one of 10 header tables independently derived (documented with reasoning, since no pre-existing list of "10 tenant-identity-assuming tables" was found in this session's context) as plausibly differing per legal entity within one tenant:platform.contract,platform.billing_account,admin.compliance_document,tax.tax_calculation,billing.ar_account,billing.vendor_payable,purchasing.vendor_invoice,purchasing.purchase_order,orders.order_header,pos.sale. NULL means "the tenant's primary entity" — today's implicit, unchanged behavior; no backfill was required (confirmed additive-safe against 65 livetax_calculationrows at build time).entity_idwas added only to this HEADER table, never to any line-item child —taxhas none, but the rule (disclosed explicitly for the first time in this phase, per Lens B's finding) is that a line item's entity is always inherited via its header's FK, never needs its own column.
Phase 4, Item 17c (Enum→Catalog, additive interim step) added a new global catalog table plus a new nullable FK column on tax_calculation_jurisdiction, and — unlike the other three Item 17 sub-items — also included a REAL widen of the pre-existing CHECK-enum itself:
tax.jurisdiction_level_catalog(new table, 6 cols, 6 seeded rows) — see below.tax_calculation_jurisdiction.jurisdiction_level_id(uuid, nullable, FK →tax.jurisdiction_level_catalog.id) — additive-interim: independently, not-yet-synced with the pre-existingjurisdiction_levelCHECK-enum column on the same table (the interim gap is disclosed durably in the Drizzle TypeScript source itself, not just a migration-file SQL comment, per Lens B). Note:tax_calculation_jurisdictionis itself append-only (no UPDATE/DELETE grant, plus its own append-only trigger) — meaningjurisdiction_level_idcan structurally never be backfilled onto any pre-migration row, only used by future inserts. This is a permanent gap for historical rows, not a temporary sync lag.chk_tax_calculation_jurisdiction_levelwas REAL-widened (dropped and re-added) to add'country'to its allowed list (state,county,city,district,special→+ 'country') — independently confirmed by Lens A to be a genuine widen against the original20260707080000_tax_module.sqlmigration, not a no-op. This closes the VAT/GST gap: country-level tax jurisdictions were not representable before this change (US-only sales/use-tax stacking — state/county/city/district/special — had no concept of a country-level jurisdiction line).
Full cross-module record: PROJECT_DECISIONS #40. Migrations: packages/db/migrations/20260709010000_phase4_item15_legal_entity.sql (entity_id ALTER); packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql (part c — jurisdiction_level_catalog + jurisdiction_level_id + CHECK widen).
tax.jurisdiction_level_catalog (6 cols, global reference, Remediation Phase 4)
New table, Item 17c. The catalog counterpart (interim, additive) to the pre-existing jurisdiction_level CHECK-enum on tax_calculation_jurisdiction — seeded with the same values plus 'country', the one the enum itself was simultaneously widened to admit.
Global reference data — zero
tenant_id, zero RLS (confirmed live), matching the established precedent ofidentity.permission/admin.setting_definition. NOTE (cross-cutting, pre-existing, not introduced by this table): like every global-reference table in this codebase,authenticatedhas full INSERT/SELECT/UPDATE/DELETE with no RLS restricting who mutates it — a systemic grant-model gap logged to OPEN_ITEMS, not fixed here.
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
code |
text | NOT NULL | — | Unique; e.g. country, state, county, city, district, special |
name |
text | NOT NULL | — | Display name, e.g. "Country", "State" |
is_active |
boolean | NOT NULL | true |
|
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Seeded rows (6): country/Country, state/State, county/County, city/City, district/District, special/Special — live-verified against the documented list; matches jurisdiction_level's pre-existing enum values plus the new country addition.
Constraints: jurisdiction_level_catalog_code_unique (unique index on code).
Indexes: PK; code (unique).
Full cross-module record: PROJECT_DECISIONS #40. Migration: packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql.
tax.tax_calculation_jurisdiction (10 cols, ZERO, append-only)
One row per (calculation, jurisdiction). Restores v1's own pre-pivot granularity — the exact decomposition pos.sale_line's collapse made unrecoverable.
Tenant-scoped, append-only. RLS enabled —
tax_calculation_jurisdiction_tenant_isolation. Noupdated_at/deleted_atat all — confirmed live, a structural guarantee, not a convention.
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
tax_calculation_id |
UUID | NOT NULL | — | FK → tax.tax_calculation |
jurisdiction_level |
text | NOT NULL | — | CHECK IN (state,county,city,district,special,country) — REAL-widened, Remediation Phase 4, to add country |
jurisdiction_name |
text | NOT NULL | — | e.g. "California", "Los Angeles County" |
jurisdiction_code |
text | nullable | — | Stripe Tax's own jurisdiction identifier, if provided |
rate |
numeric | NOT NULL | — | CHECK >= 0 |
tax_amount_cents |
bigint | NOT NULL | — | CHECK >= 0 |
created_at |
timestamptz | NOT NULL | now() |
|
jurisdiction_level_id |
UUID | nullable | — | FK → tax.jurisdiction_level_catalog; additive-interim, Remediation Phase 4, Item 17c — structurally can never be backfilled onto pre-migration rows (table is append-only) |
CHECK constraints (2): chk_tax_calculation_jurisdiction_level (REAL-widened, Remediation Phase 4, to add country); chk_tax_calculation_jurisdiction_rate_nonneg. (chk_tax_calculation_jurisdiction_amount_nonneg was DROPPED and replaced by a trigger, Remediation Phase 3 addendum — see below.)
Indexes: PK; tenant_id; tax_calculation_id.
Remediation Phase 2 (2026-07-08)
tax_calculation_jurisdiction.id's DEFAULT was changed from gen_random_uuid() to platform.uuid_generate_v7() (Remediation Phase 2, Item 6). This table was already one of Remediation Phase 1's 9 append-only-enforced (REVOKE+trigger) tables, and is now also UUIDv7-keyed. UUIDv7 is time-ordered, keeping future time-range partitioning possible on this append-only ledger without a PK rewrite — something impossible once data lands on a random UUIDv4 PK. PK-generation-strategy change only — no column or table count change (37 cols unchanged).
Full cross-module record: PROJECT_DECISIONS #38.
Remediation Phase 3 addendum (2026-07-08)
Independent post-build verification of Phase 3 Item 9 found that total_tax_amount_cents's sign convention (see the tax_calculation Phase 3 note above) had no matching enforcement on this child table: chk_tax_calculation_jurisdiction_amount_nonneg still required every jurisdiction row's tax_amount_cents >= 0, even for rows belonging to a reversal calculation — an unenforced service-layer contract, briefly disclosed as such before this same-day addendum closed it.
The old CHECK was DROPPED and replaced with a real trigger, trg_tax_calculation_jurisdiction_validate_sign (BEFORE INSERT only — this table is INSERT-only/append-only; authenticated has no UPDATE/DELETE grant, and the pre-existing trg_tax_calculation_jurisdiction_append_only trigger already rejects both, so BEFORE INSERT alone suffices, no UPDATE race). It looks up the parent tax_calculation.calculation_type and enforces the same sign rule for real: original ⇒ >= 0, reversal ⇒ <= 0. A CHECK can't express this because it requires reading a different table's row; the trigger can.
No column or table count change from this addendum (still 9 cols). Migration: packages/db/migrations/20260708270000_phase3_item9_addendum_jurisdiction_sign_trigger.sql. Full cross-module record: PROJECT_DECISIONS #39.
Remediation Phase 4 (2026-07-08)
Item 17c (Enum→Catalog, additive interim step) added a new nullable FK column, jurisdiction_level_id (uuid → tax.jurisdiction_level_catalog.id) — see the new jurisdiction_level_catalog table above. This is deliberately independent of, and not yet synced with, the pre-existing jurisdiction_level CHECK-enum column on this same table; that interim gap is disclosed durably in the Drizzle TypeScript source itself (tax/calculation.ts), not just a migration-file SQL comment.
chk_tax_calculation_jurisdiction_level itself was REAL-widened (dropped and re-added) to add 'country' to its allowed list — independently confirmed by adversarial verification to be a genuine widen against the original 20260707080000_tax_module.sql migration, not a no-op. This closes the VAT/GST gap: country-level tax jurisdictions (as opposed to the US-only state/county/city/district/special stack) were not representable in this table before.
Permanent, not temporary, historical gap: because this table is append-only (no UPDATE/DELETE grant, plus its own trg_tax_calculation_jurisdiction_append_only trigger), jurisdiction_level_id can never be backfilled onto any row that existed before this migration — only future inserts can populate it. 9→10 cols.
Full cross-module record: PROJECT_DECISIONS #40. Migration: packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql.
The seams
The source seam. tax_calculation.source_ref → pos.sale_line.id / orders.order_line.id — no reciprocal column on either locked table (link-don't-require-reciprocal). Live-tested: resolves via JOIN to a real pos.sale_line.
The decomposition itself. total_tax_amount_cents = SUM(tax_calculation_jurisdiction.tax_amount_cents) for that calculation — live-tested against a real 3-jurisdiction stacked-tax fixture (state 6.00% + county 0.25% + district 1.00% = $0.66 on a $9.00 taxable amount, decomposed across 3 rows summing to the header total exactly).
The exemption seam. tax_calculation.applied_exemption_certificate_id → crm.customer_tax_certificate — live-tested, honors the certificate's own 2026-07-07 fail-closed verification requirement (an exemption is only meaningfully "applied" when the underlying certificate is actually verified — enforced at customer_tax_certificate's own layer, not duplicated here).
The orders↔pos non-bridge (by design). An is_estimate=true row (from orders) and an is_estimate=false row (from pos) for the same logical transaction are separate, un-superseded rows — supersedes_calculation_id only links same-source_ref corrections. Reconciliation happens at report time via the existing header-level orders.order_header.fulfilled_sale_id → pos.sale join, not a line-level tax seam. Live-tested: an is_estimate=true row has supersedes_calculation_id IS NULL by default and stays that way.
The billing seam (forward). billing.ar_charge.tax_calculation_id → tax.tax_calculation.id — see PROJECT_DECISIONS #32 / schema_docs/billing.md.
The refund-reversal seam (Remediation Phase 3). tax_calculation.source_ref → pos.sale_refund_line.id (paired with source_module='pos', source_type='sale_refund_line') is the new source path for a calculation_type='reversal' row; reversed_calculation_id points back at the original (calculation_type='original') row it reverses. pos.sale_refund_line itself gained tax_amount_cents/tax_rate (mirroring pos.sale_line's own columns) and pos.sale_refund gained tax_refunded_amount_cents — all plain positive magnitudes; the negative-for-remittance sign convention lives ONLY in tax.tax_calculation/tax_calculation_jurisdiction, never on the POS side. These pos-side columns are owned and documented by pos's own schema docs, not this module — noted here only because the seam crosses the boundary.
The legal-entity seam (Remediation Phase 4, Item 15). tax_calculation.entity_id → platform.legal_entity.id — nullable, one of 10 header tables across the codebase that can now optionally attribute a row to a specific legal entity when a tenant has more than one. NULL means "the tenant's primary entity" (today's implicit, unchanged behavior). No reciprocal column; platform.legal_entity doesn't reference back.
The jurisdiction-catalog seam (Remediation Phase 4, Item 17c). tax_calculation_jurisdiction.jurisdiction_level_id → tax.jurisdiction_level_catalog.id — nullable, additive-interim alongside the pre-existing jurisdiction_level CHECK-enum column on the same table (not yet kept in sync — a future cutover must reconcile the two). The catalog itself is global reference data (no tenant_id, no RLS), seeded with the same 5 levels the enum already had plus the new country level, which the enum's own CHECK (chk_tax_calculation_jurisdiction_level) was simultaneously, genuinely widened to admit — closing the VAT/GST gap country-level jurisdictions previously had no way to be represented.
tax — Design Patterns Summary
Column-count reconciliation
| Table | Cols |
|---|---|
tax_calculation |
31 |
tax_calculation_jurisdiction |
10 |
jurisdiction_level_catalog |
6 |
| Total | 47 |
Verified live: 3 tables (2→3, Remediation Phase 4 — new jurisdiction_level_catalog), 47 columns (39→47: +8 net — tax_calculation +1 (entity_id, Item 15), tax_calculation_jurisdiction +1 (jurisdiction_level_id, Item 17c), jurisdiction_level_catalog +6 new table, Remediation Phase 4), 14 CHECK + 12 FK constraints on tax_calculation (11→12 FK, Remediation Phase 4: +entity_id → platform.legal_entity; CHECK count unchanged at 14, no CHECK added or dropped on this table this phase) plus 2 CHECK on tax_calculation_jurisdiction (unchanged at 2, but chk_tax_calculation_jurisdiction_level REAL-widened in place to add country, Remediation Phase 4) plus 1 unique constraint on jurisdiction_level_catalog (jurisdiction_level_catalog_code_unique), RLS on the 2 tenant-scoped tables, zero RLS on the new global catalog (by design, matching identity.permission/admin.setting_definition); 3 triggers total, unchanged this phase: tax_calculation has set_updated_at + trg_tax_calculation_validate_supersession + trg_tax_calculation_validate_reversal; tax_calculation_jurisdiction has trg_tax_calculation_jurisdiction_append_only (Remediation Phase 1) + trg_tax_calculation_jurisdiction_validate_sign (Remediation Phase 3 addendum) — the jurisdiction child still has no updated_at column at all; jurisdiction_level_catalog has none (plain global reference table).
Service layer
No TaxService yet — schema-only this pass.
JSONB columns
tax_calculation.decision_provenance (project-wide reason/evidence/confidence/memory_refs convention).
Open items carried forward (see OPEN_ITEMS.md)
provider_ref deferred (Payments not built); no TaxService yet; nexus/rate-anomaly detection logic not built (cross-reference Admin's nexus-config scope, don't duplicate); the orders↔pos reconciliation is a deliberate design (option b), not an open item requiring a fix. Remediation Phase 4 additions: the Item 17 sync-constraint gap — jurisdiction_level_id is not yet kept in sync with the jurisdiction_level CHECK-enum, and a future cutover must decide how to reconcile them; jurisdiction_level_id can never be backfilled onto pre-migration tax_calculation_jurisdiction rows (permanent gap, table is append-only); the cross-cutting catalog-table grant-model gap (jurisdiction_level_catalog, like every global-reference table in this codebase, has no RLS restricting mutation by any tenant-scoped session) — logged for whoever owns the grant model, not fixed here.