Context
Three closed sets are genuinely cross-cutting: currencies, national ID types and notification channels. Everything else belongs to its own schema (DEC-B3). This is the first delivery in the entire suite for a mechanical reason. Theis_system rows of a
parametric catalog are the single source that generates the Zod and TypeScript unions in
packages/core; nothing that references a currency_code or a channel_code can typecheck until
they exist.
DEC-A2 makes national_id_types the largest of the three and the one carrying real logic: every
South American country, each with the formats, normalizers and check-digit algorithms that apply
to persons and to companies.
Scope (normative)
core.countries: ISO 3166-1 alpha-2, the anchor for national ID types.core.currencies: ISO 4217 withminor_unit— the exponent that makes minor-unit arithmetic correct.core.national_id_types: per country and per contact kind, with format regex, normalizer id and check-digit algorithm id.core.notification_channels:email,in_app,webhook,push,sms,whatsapp,live_activity— the row exists from day 1 even where the adapter arrives later (DEC-E1).- The seed-to-union generation pipeline:
is_systemrows produce the Zod schemas and TS types. - The catalog cache: process memory plus Redis, so hot paths never join.
Non-scope (normative)
- Validating a national ID. This delivers the catalog; applying its regex and check digit to a value is FS-CORE-0002.
- Sending on a channel. The row is a declaration that the channel exists.
- FX rates. F1 is single-currency per tenant (DEC-B6); rates arrive with multi-currency in F2.
Behaviour (normative)
- Every catalog follows the parametric standard exactly:
codeTEXT primary key,label,is_system,is_active,sort_order,metadataJSONB. No exceptions, no extra shape. - These three are not tenant-extensible. A tenant cannot invent a currency or a channel: both govern system logic (DEC-B4).
is_systemrows are seeded in migrations and are immutable through the API. A seed changes by migration, never by an endpoint.- Seeds generate the TS/Zod unions at build time. A drift between the database codes and the generated union fails CI (TS-005) — that check is what keeps the catalog honest.
currencies.minor_unitis mandatory. CLP is 0, USD is 2, and money arithmetic is wrong without it — a fact worth stating because “amounts in cents” quietly assumes 2.- A code is never deleted and never reused. Retiring one means
is_active = false, because historic rows still reference it. - Hot paths read from cache. FORBIDDEN: joining a catalog inside a Runtime-class request.
national_id_typesrows declare whichcontact_kindthey apply to — a RUT applies to both a person and a company in Chile, a CPF only to a person in Brazil.
Data (normative)
Seeded
national_id_types cover every South American country per DEC-A2: CL (RUT) · AR (DNI,
CUIT, CUIL) · BR (CPF, CNPJ) · UY (CI, RUT) · PY (CI, RUC) · BO (CI, NIT) · PE (DNI, RUC, CE) ·
EC (CI, RUC) · CO (CC, CE, NIT) · VE (CI, RIF) · GY and SR generic — plus PASSPORT and
FOREIGN_ID.
No table here is partitioned; all are small, regular and cached.
API (normative)
Read-only. There is no write endpoint for a system catalog, and that absence is the feature.
Events (normative)
None. A catalog is reference data; a seed change is a migration, covered by the deployment record.Acceptance criteria (normative)
- Migrations seed every catalog idempotently: running them twice produces no duplicates and no errors.
- The generated TS union for
notification_channelscontains exactly the seededis_systemcodes, andpackages/corecompiles against it. - Seed drift is detected: removing a code from the seed while leaving it in the database fails the CI check with a message naming the code.
minor_unitis correct for a currency with 0 (CLP) and one with 2 (USD), verified by a formatting test.- Every seeded
national_id_typewith a check-digit algorithm has that algorithm implemented — a catalog row pointing at a missing implementation fails CI. - Catalog reads are served from cache: a Runtime-class request performs zero catalog joins, verified by a query-count assertion.
- Negative: no API path modifies an
is_systemrow.