> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Platform Catalogs

> After this ships, every other table in the platform has the catalogs it needs to reference, and the TypeScript unions those catalogs generate exist for the whole monorepo to compile against.

## 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. The `is_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 with `minor_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_system` rows 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)*

1. Every catalog follows the parametric standard exactly: `code` TEXT primary key, `label`,
   `is_system`, `is_active`, `sort_order`, `metadata` JSONB. No exceptions, no extra shape.
2. These three are **not tenant-extensible**. A tenant cannot invent a currency or a channel: both
   govern system logic (DEC-B4).
3. `is_system` rows are seeded in migrations and are **immutable through the API**. A seed changes
   by migration, never by an endpoint.
4. 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.
5. `currencies.minor_unit` is 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.
6. A code is **never deleted and never reused**. Retiring one means `is_active = false`, because
   historic rows still reference it.
7. Hot paths read from cache. FORBIDDEN: joining a catalog inside a Runtime-class request.
8. `national_id_types` rows declare which `contact_kind` they apply to — a RUT applies to both a
   person and a company in Chile, a CPF only to a person in Brazil.

## Data *(normative)*

| Table                        | Key invariants                                                                                                                                                                                  |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.countries`             | `code` CHAR(2) PK (ISO 3166-1 alpha-2); `name`; `is_active`                                                                                                                                     |
| `core.currencies`            | `code` CHAR(3) PK (ISO 4217); `minor_unit` SMALLINT NOT NULL; `symbol`; standard parametric columns                                                                                             |
| `core.national_id_types`     | `code` TEXT PK (e.g. `CL_RUT`, `BR_CPF`); FK `country_code`; `applies_to` person\|company\|both; `format_regex`; `normalizer_id`; `check_digit_algorithm` nullable; standard parametric columns |
| `core.notification_channels` | `code` TEXT PK; `label`; `is_active`; `available_from_phase`; standard parametric columns                                                                                                       |

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)*

| Endpoint                          | Class      | Permission           | Budget    |
| --------------------------------- | ---------- | -------------------- | --------- |
| `GET /v1/core/catalogs/{catalog}` | Management | `core.catalogs.read` | p95 \<1 s |

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)*

1. Migrations seed every catalog idempotently: running them twice produces no duplicates and no
   errors.
2. The generated TS union for `notification_channels` contains exactly the seeded `is_system` codes,
   and `packages/core` compiles against it.
3. 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.
4. `minor_unit` is correct for a currency with 0 (CLP) and one with 2 (USD), verified by a
   formatting test.
5. Every seeded `national_id_type` with a check-digit algorithm has that algorithm implemented —
   a catalog row pointing at a missing implementation fails CI.
6. Catalog reads are served from cache: a Runtime-class request performs zero catalog joins,
   verified by a query-count assertion.
7. **Negative:** no API path modifies an `is_system` row.

## Execution

Single slice, synchronous command. Ships as the first part of TS-001. Nothing else in the module
can be written before it merges.

## Open questions

| # | Question                                                                         | Decides | By              |
| - | -------------------------------------------------------------------------------- | ------- | --------------- |
| 1 | Do we seed Central America and Mexico now, or wait for a tenant that needs them? | Daniel  | before approval |

## Changelog

| Version | Date       | Change        | Why | Author                 |
| ------- | ---------- | ------------- | --- | ---------------------- |
| 0.1.0   | 2026-08-17 | Initial draft | —   | daniel + claude-opus-5 |

## Delivery record

*Not implemented yet.*
