0003. Catálogos y tablas maestras generales¶
Estado: Propuesto
Fecha: 2026-09-08
Autor: Duval Alcivar
Módulo(s) afectado(s): svc-identidad (core_sigfa), todos los servicios que referencian los catálogos
Contexto¶
svc-identidad es el dueño de la identidad digital en toda la plataforma.
Además de usuarios, empresas, aplicaciones y permisos, centraliza los
catálogos y maestros comunes que otros servicios referencian
(cat_/mae_ sin negocio propio), para que no se dupliquen en cada base de
datos.
La regla de negocio inicial: el mae_empresas referencia país y
ciudad, las personas referencian un tipo de identificación. Hoy esas
listas no tienen tabla; este ADR las define con margen para crecer.
Decisión¶
Crear un conjunto mínimo de catálogos globales (sin empresa_id, sin
RLS), con esquema core.identidad, PK UUID y auditoría estándar. Un
catálogo nuevo solo es una tabla cat_<nombre> nueva bajo el mismo esquema:
no se tocan los servicios.
1. Tablas iniciales¶
| Tabla | Contenido | Quién la usa |
|---|---|---|
cat_paises |
países (ISO 3166-1 alfa-3) | mae_empresas.pais_id, mae_personas |
cat_ciudades |
ciudades (pais_id) |
mae_empresas.ciudad_id |
cat_tipos_identificacion |
cédula, RUC, pasaporte | mae_empresas.tipo_identificacion_id, mae_personas.tipo_identificacion_id |
cat_generos |
masculino, femenino, otros |
mae_personas.genero_id |
cat_tipos_usuario |
usuario, root, externo |
mae_usuarios.tipo_usuario_id (ADR 0007/0022) |
Todas comparten la misma estructura base:
| Campo | Tipo | Notas |
|---|---|---|
id |
UUID PK | gen_random_uuid() |
codigo |
VARCHAR | UNIQUE NOT NULL cuando aplica (ej. EC); no todas las tablas lo llevan |
nombre |
VARCHAR | NOT NULL, nombre legible |
is_activo |
BOOLEAN | DEFAULT TRUE |
created_at / updated_at |
TIMESTAMPTZ | default now() |
deleted_at |
TIMESTAMPTZ | NULL, soft delete |
created_by / updated_by |
UUID | auditoría |
Ninguna de estas tablas tiene
empresa_idni RLS: son catálogos globales que todas las empresas comparten.
CREATE TABLE core.identidad.cat_paises (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
codigo VARCHAR(3) NOT NULL,
nombre VARCHAR(100) NOT NULL,
is_activo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
created_by UUID,
updated_by UUID,
CONSTRAINT uq_cat_paises_codigo UNIQUE (codigo)
);
CREATE TABLE core.identidad.cat_ciudades (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pais_id UUID NOT NULL,
nombre VARCHAR(100) NOT NULL,
is_activo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
created_by UUID,
updated_by UUID,
CONSTRAINT fk_cat_ciudades_pais
FOREIGN KEY (pais_id) REFERENCES core.identidad.cat_paises(id)
);
CREATE TABLE core.identidad.cat_tipos_identificacion (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nombre VARCHAR(100) NOT NULL,
is_activo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
created_by UUID,
updated_by UUID
);
CREATE TABLE core.identidad.cat_generos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nombre VARCHAR(100) NOT NULL,
is_activo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
created_by UUID,
updated_by UUID
);
# models.py — SQLAlchemy 2.0
class CatalogoBase:
"""Columnas comunes a todo catálogo. No es un mixin de herencia real,
se copia por claridad en cada modelo."""
pass
class CatPais(Base):
__tablename__ = "cat_paises"
__table_args__ = {"schema": "core.identidad"}
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
codigo: Mapped[str] = mapped_column(String(3), unique=True, nullable=False)
nombre: Mapped[str] = mapped_column(String(100), nullable=False)
is_activo: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_by: Mapped[uuid.UUID | None] = mapped_column(as_uuid=True)
updated_by: Mapped[uuid.UUID | None] = mapped_column(as_uuid=True)
2. Referencias desde otras tablas¶
mae_personas.tipo_identificacion_idesUUIDy FK →cat_tipos_identificacion.id(ADR 0005). El valoridentificacionya es único por persona.mae_empresas.tipo_identificacion_idesUUIDy FK →cat_tipos_identificacion.id(ADR 0004).mae_personas.genero_idesUUIDy FK →cat_generos.id(ADR 0005).mae_empresas.pais_id/mae_empresas.ciudad_idpasan a ser FK →cat_paises/cat_ciudades(ADR 0004).
3. Cómo crecer el catálogo¶
- Nuevo catálogo = nueva tabla
cat_<nombre>+ seed en un script de migración. No se agregan catálogos dentro de tablas de negocio. - La carga inicial (seed) vive en el pipeline de
svc-identidadcomo data migration; los catálogos globales no dependen de la empresa. - Los servicios consumen los catálogos que necesitan y los cachean; no los duplican en su base (regla de cero consultas entre bases, ADR transversal 0003).
Alternativas descartadas¶
- Catálogos en cada servicio — duplica listas (un país diferente por base) y descuadra referencias comunes como país/ciudad.
- Catálogo como
ENUMen código — no es dato gestionable: cualquier país/cuidad nueva requiere despliegue; una tabla no. - Colchón de columnas sueltas en
mae_empresas— acopla negocio y catálogo; el catálogo es global y reutilizable.
Consecuencias¶
- Un solo lugar para países, ciudades y tipos de documento; los servicios los referencian por FK y los cachean.
mae_empresasymae_personaspasan a tener FKs reales a los catálogos, sin valores sueltos.- Los catálogos son globales (sin RLS): cualquier empresa los ve igual.
- Pendiente: definir si sucursal necesita catálogo de tipos o si se deja
como texto libre; por ahora se usa el texto de
mae_sucursales.