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; tax stores 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 NULL FK → platform.tenant, RLS with <table>_tenant_isolation policy.
  • Asymmetric mutability, by design. tax_calculation (the header) has updated_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_source defaults 'system' — the FIRST justified deviation in the entire codebase from the universal 'human' default (confirmed live: every other module's automation_source column defaults 'human', zero exceptions before this). A tax calculation is a deterministic, externally-computed result, never a human action recorded after the fact.
  • source_ref is polymorphictax_calculation.source_ref points AT pos.sale_line.id or orders.order_line.id (link-don't-require-reciprocal, matching billing.ar_charge's own established pattern into pos.sale/orders.order_header) — no reciprocal column needed on either locked table, no reopen.
  • supersedes_calculation_id is scoped to same-source_ref corrections only — DB-enforced, not just documented. A miscalculated tax for the SAME line gets a new row that supersedes the old one (mirrors pricing.price_rule's supersede-don't-edit pattern). trg_tax_calculation_validate_supersession (a BEFORE INSERT OR UPDATE OF supersedes_calculation_id trigger, mirroring pricing.trg_price_rule_validate_supersession exactly) rejects any attempt to set supersedes_calculation_id to a row with a different tenant_id/source_module/source_type/source_ref — live-tested (test F3): a cross-source_ref supersede 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-level order_header.fulfilled_sale_id join (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 via platform.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_evidenceis_exempt = true now requires either applied_exemption_certificate_id IS NOT NULL or provider = '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 by supersedes_calculation_id. chk_tax_calculation_reversal_requires_target enforces (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 live tax_calculation rows at build time). entity_id was added only to this HEADER table, never to any line-item child — tax has 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-existing jurisdiction_level CHECK-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_jurisdiction is itself append-only (no UPDATE/DELETE grant, plus its own append-only trigger) — meaning jurisdiction_level_id can 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_level was 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 original 20260707080000_tax_module.sql migration, 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 of identity.permission/admin.setting_definition. NOTE (cross-cutting, pre-existing, not introduced by this table): like every global-reference table in this codebase, authenticated has 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. No updated_at/deleted_at at 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 rowssupersedes_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.

Last modified: Jul 8, 2026, 2:56 PM PT
On this page
Esc