shared — Phase 3 (module #3)

8 tables, 75 columns — LOCKED 2026-07-05, reopened 2026-07-08 for Remediation Phase 4 (2 new tables, +16 cols; see below), reopened again 2026-07-18 for Phase 2 (Nursery Vertical Extraction) — 4 tables (climate_zone, plant, plant_common_name, plant_climate_zone, 61 cols total) moved OUT to a new nursery_ref schema via ALTER TABLE ... SET SCHEMA (a nursery-specific botanical taxonomy never belonged in this vertical-neutral core — see PROJECT_DECISIONS.md #70 and SCHEMA_CONVENTIONS.md §21); shared also had write access REVOKEd from the authenticated role in the same reopen (Part C of the same entry) — see the "Write access" note below. Global, non-tenant reference data: currency, country, administrative subdivisions, language, locale, unit of measure, exchange rates, and a payment-terms catalog. Global from day 1 — structural reference data fully seeded, not deferred to a later phase.

Baseline correction (pre-existing, not a Phase 4 change): the "108 columns" figure long stated here for the original 10-table baseline was itself stale. The true pre-Phase-4 baseline was 120 columns, not 108 — see the "Remediation Phase 4" section below for the full accounting of how the 108 figure drifted and was corrected. (That section's own final tally — 136 columns / 12 tables — was the pre-Phase-2 count; see the Phase 2 addendum at the end of that same section, and the header line above, for the current count.)

Depends on: identity — added 2026-07-06 by the cross-cutting autonomy-first backfill (see PROJECT_DECISIONS.md #19), via new created_by_actor_id FKs on plant, plant_common_name, and plant_climate_zone to identity.actor. This dependency no longer applies: all 3 of those tables moved to nursery_ref in Phase 2 (2026-07-18, see PROJECT_DECISIONS.md #70). shared's remaining 8 tables depend on nothing outside this schema.

Global rules for this schema:

  • NOT tenant-scoped — no tenant_id on any table; no RLS applied.
  • Readable by all authenticated usersapp_user and service_role both have SELECT.
  • Write access (corrected 2026-07-18, Phase 2 — see PROJECT_DECISIONS.md #70): authenticated has been REVOKEd INSERT/UPDATE/DELETE on every table in this schema (both the original 8 and, previously before their move, the 4 that are now in nursery_ref) — SELECT-only. ALTER DEFAULT PRIVILEGES was also applied so any future table added to this schema inherits the same read-only-by-default posture automatically, closing — in reverse — the exact platform.polymorphic_target_registry over-grant bug class Phase 1 Security Remediation found (PROJECT_DECISIONS.md #68). Live-reproduced via a genuine A/B, not just a post-hoc query: a scoped, rolled-back GRANT INSERT proved the pre-lockdown posture would have allowed the write; post-lockdown, INSERT INTO shared.currency as authenticated correctly rejects with 42501. Before this date, the statement below ("writable only via service_role...") was a documented application-level convention only, not yet DB-enforced — application code never wrote here in practice, but nothing at the database level actually prevented it from doing so.
  • Writable only via service_role and seed/AI-enrichment migrations — application code never INSERTs/UPDATEs shared.* rows at runtime, except the (now-relocated) AI→nursery_ref seam — see the "Inbound" note below.
  • No deleted_at anywhere — deactivation is via is_active = false, never soft delete. Reference rows have permanent identity (an ISO code is the same code forever, even when retired).
  • Natural-key PKs, with 2 deliberate UUID-surrogate exceptions (exchange_rate, payment_terms_catalog, both added Remediation Phase 4) — the original exception this schema carried, plant (UUID PK, see PROJECT_DECISIONS #17), moved to nursery_ref in Phase 2 (2026-07-18) and is no longer part of this schema's own accounting.

Superseded v1 note: shared previously existed in v1 (superseded) as a 7-table design (country, us_state, currency, unit_of_measure, usda_hardiness_zone, plant, plant_common_name) — see docs/old/schema/schema_modules/schema_shared.md. This v2 design reconciles with v1's country/currency/unit_of_measure/plant/plant_common_name column-level thinking, and deliberately expands us_stateadministrative_region (ISO 3166-2, global) and usda_hardiness_zoneclimate_zone (multi-system: USDA/RHS/AHS Heat/Australian/EU), plus adds language, locale, and plant_climate_zone as new tables. Note (2026-07-18): plant, plant_common_name, plant_climate_zone, and climate_zone — the v1-descended tables named in this paragraph — were subsequently moved out of shared entirely, to a new nursery_ref schema (Phase 2, PROJECT_DECISIONS.md #70). This v1-comparison paragraph is left as-is as a historical record of v1→v2 reconciliation, not a description of shared's current shape — see docs/database/schema_docs/nursery_ref.md for where these 4 tables live now.

Cross-Phase Foreign Keys (shared)

Column Target Notes
country.default_currency_code shared.currency Intra-schema, enforced at this module's migration.
country.default_locale_code shared.locale Circular with locale.country_code — both nullable. FK constraint added via ALTER TABLE after shared.locale exists (two-pass migration). See DESIGN_RATIONALE.
locale.language_code shared.language Intra-schema, enforced.
locale.country_code shared.country Nullable, intra-schema, enforced.
administrative_region.country_code shared.country Intra-schema, enforced.
administrative_region.parent_region_code shared.administrative_region Self-FK, nullable, app-enforced cycle prevention beyond the DB self-loop CHECK (same pattern as identity.role.parent_role_id, DR-12).
unit_of_measure.base_unit_code shared.unit_of_measure Self-FK, nullable.
exchange_rate.from_currency_code / to_currency_code shared.currency Intra-schema, enforced. Added 2026-07-08, Remediation Phase 4 Item 16 (PROJECT_DECISIONS.md #40).

Moved out 2026-07-18 (Phase 2): 4 rows previously listed here — plant_common_name.plant_id → shared.plant, plant_common_name.locale_code → shared.locale, plant_climate_zone.plant_id → shared.plant, and plant_climate_zone.min_zone_code/max_zone_code → shared.climate_zone — no longer belong in this table. Both the referencing and (for 3 of the 4) the referenced tables moved to nursery_ref together, so these are now purely intra-nursery_ref FKs (see nursery_ref.md). The one exception, plant_common_name.locale_code → shared.locale, is now a genuine cross-schema inbound FK (nursery_refshared) — see "Inbound" immediately below.

Inbound (not built here): later modules will reference shared tables — inventory.itemunit_of_measure; crm.customercountry/administrative_region; pricingcurrency. One seam is already locked in CROSS_MODULE_CONTRACTS.md: consumer_address.country_code → shared.country (enforced FK). New as of Phase 2 (2026-07-18): nursery_ref.plant_common_name.locale_code → shared.locale.code — a foundation-to-foundation cross-schema FK, permitted under SCHEMA_CONVENTIONS.md §21 since both nursery_ref and shared are non-tenant-scoped foundation-layer schemas. Two seams that used to point at shared.plant now point at nursery_ref.plant instead, retargeted in the same Phase 2 migration: consumer_interest.interest_ref → nursery_ref.plant.slug (loose text ref, app-validated) and AI → nursery_refAIService writes nursery_ref.plant/nursery_ref.plant_common_name rows with data_source='ai_generated', is_verified=false via service_role (see PROJECT_DECISIONS.md #70's "every consumer of a moved object retargeted" note).

Inbound, now real (added 2026-07-08, Remediation Phase 4): crm.customer.payment_terms_id, purchasing.vendor.payment_terms_id, purchasing.purchase_order.payment_terms_idshared.payment_terms_catalog.id (all nullable FKs). shared.exchange_rate has no direct FK consumer yet; instead it enables billing.ar_payment_application's new currency-agreement trigger (billing.validate_ar_payment_application_currency() — see schema_docs/billing.md), which cross-checks ar_payment/ar_charge/ar_account currency codes for agreement (does not join exchange_rate directly, but the table exists to support future cross-currency conversion once a real multi-currency flow is built).


shared.currency

ISO 4217 currency dictionary. Seeded at ~152 active currencies.

NOT tenant-scoped — reference data. No tenant_id, no deleted_at.

RLS: not applied — reference data.

Access control: readable by all authenticated users. Writable only via service_role and seed migration.

Column Type Nullable Default Constraints / Notes
iso_code char(3) NOT NULL PK — natural key, e.g. 'USD'
numeric_code char(3) NOT NULL UNIQUE — ISO 4217 numeric, e.g. '840'
name text NOT NULL
symbol text NOT NULL e.g. '$'
decimal_places smallint NOT NULL 2 CHECK >= 0
is_active boolean NOT NULL true
sort_order integer NOT NULL 0
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on iso_code; UNIQUE on numeric_code.


shared.language

ISO 639 language dictionary. Seeded at ~94 major world languages (ISO 639-1 codes; not the full ~184-code standard — see PROJECT_DECISIONS for scope rationale).

NOT tenant-scoped — reference data. No tenant_id, no deleted_at.

RLS: not applied — reference data.

Column Type Nullable Default Constraints / Notes
iso_639_1 char(2) NOT NULL PK — e.g. 'en'. Known limitation: languages without a 2-letter ISO 639-1 code (e.g. Hawaiian, which only has the 3-letter iso_639_3='haw') cannot be represented under this PK — see DESIGN_RATIONALE.
iso_639_3 char(3) nullable UNIQUE — superset code
name text NOT NULL English name
native_name text nullable
is_active boolean NOT NULL true
sort_order integer NOT NULL 0
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on iso_639_1; UNIQUE on iso_639_3 WHERE NOT NULL.


shared.country

ISO 3166-1 country dictionary. Seeded at 193 countries/territories.

NOT tenant-scoped — reference data. No tenant_id, no deleted_at.

RLS: not applied — reference data.

Circular reference: default_locale_codeshared.locale.code, and locale.country_codecountry.iso_alpha2. Both nullable. Seeded in two passes — see the migration and DESIGN_RATIONALE.

Column Type Nullable Default Constraints / Notes
iso_alpha2 char(2) NOT NULL PK — e.g. 'US'
iso_alpha3 char(3) NOT NULL UNIQUE
numeric_code char(3) NOT NULL UNIQUE
name text NOT NULL
official_name text nullable Not populated this pass
phone_code text nullable E.164, e.g. '+1'
default_currency_code char(3) nullable FK → shared.currency
default_locale_code text nullable FK → shared.locale (circular, see above)
is_active boolean NOT NULL true
sort_order integer NOT NULL 0
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on iso_alpha2; UNIQUE on iso_alpha3; UNIQUE on numeric_code; index on default_currency_code.


shared.locale

BCP 47 locale dictionary. Seeded at 36 locales (a practical set, not the full combinatorial space of country × language).

NOT tenant-scoped — reference data. No tenant_id, no deleted_at.

RLS: not applied — reference data.

Column Type Nullable Default Constraints / Notes
code text NOT NULL PK — e.g. 'en-US'
language_code char(2) NOT NULL FK → shared.language
country_code char(2) nullable FK → shared.country (circular, see country above)
name text NOT NULL e.g. 'English (United States)'
is_active boolean NOT NULL true
sort_order integer NOT NULL 0
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on code; index on language_code; index on country_code WHERE NOT NULL.


shared.administrative_region

ISO 3166-2 subdivision dictionary (replaces v1's us_state). Seeded at 186 rows — comprehensive for US (56: 50 states + DC + 5 territories), Canada (13), Mexico (32); a representative subset for UK (4, simplified to constituent countries), Australia (8), Germany (16), France (13), Italy (20), Spain (17), Japan (7 of 47). Scaled down from the originally-discussed ~3-4K global target — see PROJECT_DECISIONS for the accuracy-over-volume rationale.

NOT tenant-scoped — reference data. No tenant_id, no deleted_at.

RLS: not applied — reference data.

Column Type Nullable Default Constraints / Notes
iso_3166_2 text NOT NULL PK — full code, e.g. 'US-CA'
country_code char(2) NOT NULL FK → shared.country
code text NOT NULL region-only part, e.g. 'CA'
name text NOT NULL
region_type text NOT NULL CHECK IN (21 values: state,province,region,territory,department,municipality,district,governorate,oblast,emirate,county,parish,autonomous_republic,city,canton,prefecture,republic,capital_territory,special_administrative_region,administrative_area,country). Expanded from an initial 6-value proposal that failed the Section 4 audit (Item F) — the real ISO 3166-2 type diversity across countries required this larger list.
parent_region_code text nullable Self-FK → shared.administrative_region. DB self-loop guard only; deeper cycle prevention is app-enforced.
is_active boolean NOT NULL true
sort_order integer NOT NULL 0
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on iso_3166_2; UNIQUE on (country_code, code); index on country_code; index on parent_region_code WHERE NOT NULL.


shared.unit_of_measure

Global UoM vocabulary — the set of units that exist, not per-item conversion logic (that lives on inventory.item/package). Seeded at 31 rows: metric + imperial base units across 6 dimensions, plus nursery trade-size pot containers.

NOT tenant-scoped — reference data. No tenant_id, no deleted_at.

RLS: not applied — reference data.

Conversion formula: base_value = (raw_value + offset) * conversion_factor. Example: Fahrenheit → Celsius, offset = -32, conversion_factor = 5/9.

Nursery pot sizes are nominal, not precise: us_nursery_gal_1/_2/_3 are horticultural trade-size names — a "#1 container" commonly holds ~2.5–3.5 actual quarts, not exactly one gallon, and varies by manufacturer. conversion_factor on these rows is a conventional approximation, not a measured constant.

Column Type Nullable Default Constraints / Notes
code text NOT NULL PK — e.g. 'each', 'lb', 'us_nursery_gal_1'
name text NOT NULL
symbol text nullable
dimension text NOT NULL CHECK IN (length,volume,weight,temperature,area,count)
system text nullable CHECK IN (metric,imperial) OR NULL — count-dimension units belong to neither
base_unit_code text nullable Self-FK → shared.unit_of_measure
conversion_factor numeric nullable See formula above
offset numeric nullable Temperature only
is_active boolean NOT NULL true
sort_order integer NOT NULL 0
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on code.


shared.exchange_rate

Currency-pair exchange rates by effective date. Added 2026-07-08, Remediation Phase 4 Item 16 (PROJECT_DECISIONS.md #40). 0 rows seeded — brand-new table, no data yet.

NOT tenant-scoped — reference data. No tenant_id, no deleted_at, no RLS — matches every other table in this schema with zero exception.

RLS: not applied — reference data.

UUID surrogate PK, not a natural composite key — a deliberate departure from this table's own natural fit (the natural key would be (from_currency_code, to_currency_code, effective_date)), but a codebase-wide grep confirmed zero composite-PK precedent anywhere in ~180 tables, so this table follows that universal single-column-PK convention instead of inventing a shared-local exception. The natural key is still fully enforced, just as a UNIQUE index rather than the PK.

No direct FK consumer yet. Instead, this table enables billing.ar_payment_application's new currency-agreement trigger (billing.validate_ar_payment_application_currency()), which validates that an ar_payment's currency, its target ar_charge's currency, and (when both reference the same account) the ar_account's own currency all agree. See schema_docs/billing.md for the trigger detail.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL gen_random_uuid() PK — surrogate; see note above on why this table doesn't use a natural composite PK
from_currency_code char(3) NOT NULL FK → shared.currency.iso_code
to_currency_code char(3) NOT NULL FK → shared.currency.iso_code
effective_date date NOT NULL
rate numeric NOT NULL CHECK > 0
source text nullable Free-text provenance of the rate (e.g. a rate-provider name); not FK-constrained
created_at timestamptz NOT NULL now()

Indexes: PK on id; UNIQUE on (from_currency_code, to_currency_code, effective_date).

CHECK chk_exchange_rate_positive: rate > 0.

CHECK chk_exchange_rate_distinct_currencies: from_currency_code != to_currency_code — a currency cannot have an exchange rate against itself.


shared.payment_terms_catalog

Global payment-terms catalog — the enum→catalog additive interim step (Item 17b) for what was previously a bare CHECK-enum on consuming tables. Added 2026-07-08, Remediation Phase 4 (PROJECT_DECISIONS.md #40). Seeded at 10 rows.

NOT tenant-scoped — reference data. No tenant_id, no deleted_at, no RLS.

RLS: not applied — reference data.

Deliberately pure global, no tenant-scoped variant. An earlier draft considered an optional tenant-scoped variant mirroring identity.role's mixed-scope pattern, but this was dropped as unrequested scope creep — logged here, not silently dropped.

Additive interim step, not yet synced with the legacy enum. crm.customer.credit_terms (Phase 3's own CHECK-enum, 6 values) is not a clean subset of this catalog's 10 codes — 4 catalog codes (cod, prepaid, net_7, 2_10_net_30) have no credit_terms equivalent at all. The eventual cutover from the legacy enum to this catalog requires a real mapping decision for those 4 codes, not a trivial 1:1 rename. Logged to OPEN_ITEMS.md.

Consumed by: crm.customer.payment_terms_id, purchasing.vendor.payment_terms_id, purchasing.purchase_order.payment_terms_id (all nullable FKs, added in the same migration).

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL gen_random_uuid() PK
code text NOT NULL UNIQUE — e.g. 'net_30', '2_10_net_30'
name text NOT NULL Display name, e.g. '2/10 Net 30'
net_days integer NOT NULL CHECK >= 0
discount_percent numeric nullable Early-payment discount percent, e.g. 2.00. Paired with discount_days (see CHECK below)
discount_days integer nullable Days within which the discount applies. Paired with discount_percent
is_active boolean NOT NULL true
created_at / updated_at timestamptz NOT NULL now()

Indexes: PK on id; UNIQUE on code.

CHECK chk_payment_terms_catalog_net_days_nonneg: net_days >= 0.

CHECK chk_payment_terms_catalog_discount_pair: (discount_percent IS NULL) = (discount_days IS NULL) — a discount must be fully specified (both percent and days) or not specified at all, never half-set.

Seed rows (10): due_on_receipt, cod, prepaid, net_7, net_15, net_30, net_45, net_60, net_90, 2_10_net_30 (the only row with a discount: net_days=30, discount_percent=2.00, discount_days=10 — genuinely representing "2/10 net 30," a shape a bare CHECK-enum cannot express).


Shared — Design Patterns Summary

All 8 tables are NOT tenant-scoped, no RLS

Every table: readable by all authenticated users; writable only via service_role + seed migration. As of 2026-07-18 (Phase 2, PROJECT_DECISIONS.md #70) this is also DB-enforced via REVOKE, not just a code convention — see the "Write access" note in the Global rules above. The 2 tables added in Remediation Phase 4 (exchange_rate, payment_terms_catalog) conform to this same pattern with zero exception. (The AI-enrichment write path — previously plant/plant_common_name — moved to nursery_ref in the same Phase 2 reopen and no longer touches this schema at all.)

No deleted_at — deactivation via is_active

Most tables carry is_active boolean NOT NULL DEFAULT true; reference rows have permanent identity, and deactivation is a display/picker concern, never a delete. exchange_rate has no lifecycle concept at all (a rate row is either the current one for its pair/date or superseded by a later effective_date row — no toggle needed). payment_terms_catalog does carry is_active, matching the other lookup-style tables.

Natural-key PKs — now the norm with zero exception among the original reference tables

currency, country, language, locale, administrative_region, unit_of_measure all use their natural code as the PK (char/text, not UUID) — a deliberate deviation from the platform/identity UUID convention, scoped to this module's pure reference-code tables. exchange_rate and payment_terms_catalog (both added Remediation Phase 4) use a UUID surrogate PK instead — see each table's own section above for why. plant, this schema's original UUID-PK exception (botanical/taxonomic names are not permanently stable identifiers — PROJECT_DECISIONS #17) moved to nursery_ref in Phase 2 (2026-07-18) and is no longer part of this schema's own accounting — see nursery_ref.md.

Circular reference: countrylocale

country.default_locale_code and locale.country_code reference each other, both nullable. Seeded in two passes (see migration): country rows first with default_locale_code = NULL, then locale rows, then an UPDATE closes the loop. The FK constraint on country.default_locale_code is added via a separate ALTER TABLE after shared.locale exists.

JSONB columns

None in this module.

Deferred: country_currency

Considered and explicitly not built this pass — country.default_currency_code (a single FK) covers the common case; a join table would only matter for genuinely dual-currency countries (Panama, Timor-Leste, etc.), which is out of scope for a US-market product today. See OPEN_ITEMS for the trigger to revisit.


Remediation Phase 4 (2026-07-08)

See PROJECT_DECISIONS.md #40 for the full cross-module entry. Two additive changes to this module: 2 new tables (+16 columns), plus a drive-by correction of a stale column-count figure that predates Phase 4 and is unrelated to it. Zero tables/columns dropped; nothing existing was altered.

The pre-existing 108 → 120 baseline correction (NOT a Phase 4 addition)

This doc previously stated the pre-Phase-4 baseline as 10 tables, 108 columns. That figure was stale, independent of anything Phase 4 built:

  • The original 2026-07-05 lock was 10 tables / 101 columns.
  • The 2026-07-06 autonomy-first backfill (PROJECT_DECISIONS.md #19) added data_source, is_verified, created_by_actor_id to plant_common_name and plant_climate_zone+7 columns (101 + 7 = 108). This is the arithmetic behind the "108" figure this doc has stated since.
  • However, a second, later change was never folded into that running total: the 2026-07-06 review-seam fix (PROJECT_DECISIONS.md #21, "FIX 1") added review_status, review_reason, reviewed_by_actor_id, reviewed_at to all 3 plant tables (plant, plant_common_name, plant_climate_zone) — +4 columns × 3 tables = +12 columns. This addition is fully documented per-table above (see each table's column list, all dated "added 2026-07-06"), but the module-level header count was never updated to include it.
  • True pre-Phase-4 baseline: 108 + 12 = 120 columns, still 10 tables. This correction is disclosed transparently in PROJECT_DECISIONS.md #40's own Docs section as a drive-by fix, the same way Phase 3 fixed an unrelated stale count found in passing — it is not part of Phase 4's own scope of work, just corrected in the same doc-update pass for accuracy.

Phase 4's own genuine additions: +2 tables, +16 columns (120 → 136)

  1. shared.exchange_rate (Item 16) — 7 columns: id, from_currency_code, to_currency_code, effective_date, rate, source, created_at. Full detail in the table section above. Uses a uuid surrogate PK + UNIQUE(from_currency_code, to_currency_code, effective_date) rather than a natural composite PK — a deliberate departure from this module's otherwise natural-key-PK convention, justified by a codebase-wide grep confirming zero composite-PK precedent across ~180 tables. Pure global reference data, no direct FK consumer yet (enables billing.ar_payment_application's new currency-agreement trigger instead — see schema_docs/billing.md).
  2. shared.payment_terms_catalog (Item 17b) — 9 columns: id, code, name, net_days, discount_percent, discount_days, is_active, created_at, updated_at. Full detail in the table section above. 10 seeded rows including 2_10_net_30, which genuinely represents "2/10 net 30" (a bare CHECK-enum cannot). Deliberately pure global (no tenant_id) — an earlier draft considered a tenant-scoped variant mirroring identity.role's mixed-scope pattern, dropped as unrequested scope creep. Consumed by crm.customer.payment_terms_id, purchasing.vendor.payment_terms_id, purchasing.purchase_order.payment_terms_id.

7 + 9 = 16 new columns. 120 + 16 = 136 columns, 10 + 2 = 12 tables — matched the header count at the top of this document at the time Phase 4 closed.

Post-Phase-4 addendum (2026-07-18, Phase 2): this 136-column/12-table tally is no longer current. Phase 2 (Nursery Vertical Extraction, PROJECT_DECISIONS.md #70) subsequently moved 4 tables / 61 columns (climate_zone, plant, plant_common_name, plant_climate_zone) out of this schema entirely, to a new nursery_ref schema. 136 − 61 = 75 columns, 12 − 4 = 8 tables — matching the header count at the top of this document today. This section is otherwise left unmodified as the historical record of Phase 4's own work.

Migrations: packages/db/migrations/20260709020000_phase4_item16_exchange_rates.sql (Item 16), packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql (Item 17, part b of a 4-part cross-module migration — parts a/c/d touch pos, tax, admin, not shared).

Tests: shared-schema.spec.ts gained 14 new tests (sections F–G) — exchange_rate's 3 CHECKs (positive rate, distinct currencies, unique pair+date) plus no-RLS/no-tenant_id confirmation; payment_terms_catalog seed spot-check plus no-RLS/no-tenant_id confirmation. 34/34 total for the file.

Independent verification: 2 separate adversarial lenses, both CLEAN — see PROJECT_DECISIONS.md #40 for the full findings (live-reproduced constraint behavior, confirmed zero destructive DDL, confirmed the interim enum→catalog sync gap is durably disclosed in Drizzle TypeScript source, not just migration comments).

Last modified: Jul 12, 2026, 2:52 PM PT
On this page
Esc