Saltar a contenido

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_id ni 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_id es UUID y FK → cat_tipos_identificacion.id (ADR 0005). El valor identificacion ya es único por persona.
  • mae_empresas.tipo_identificacion_id es UUID y FK → cat_tipos_identificacion.id (ADR 0004).
  • mae_personas.genero_id es UUID y FK → cat_generos.id (ADR 0005).
  • mae_empresas.pais_id / mae_empresas.ciudad_id pasan a ser FK → cat_paises / cat_ciudades (ADR 0004).
  • 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-identidad como 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 ENUM en 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_empresas y mae_personas pasan 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.