0008. Auditoría y registro de movimientos (menú por producto)¶
Fecha: 2026-09-01 Estado: Propuesto Módulo(s) afectado(s): todos (core, ERP y CRM) — transversal
Contexto¶
Se requiere saber, de forma completa y centralizada, todo lo que un usuario hace dentro del sistema: qué movimiento ejecutó, quién lo hizo, cuándo, qué acción fue (crear, editar, eliminar, consultar), si subió un archivo, qué entidad y qué registro cambiaron, y de qué valor a qué valor.
Los campos de auditoría de la nomenclatura (ADR 0004: created_at,
updated_at, created_by, updated_by, deleted_at) solo guardan la
última huella de cada fila, no el historial. Para reconstruir "quién y cuándo
cambió qué" hace falta una bitácora de movimientos separada.
Además, los módulos de la aplicación se organizan en un menú por producto
(ERP sigfa.com.ec y CRM sigfapro.com.ec), cada uno con su propia base de
datos y su propio árbol de módulos/páginas. Para poder mostrar "en qué pantalla
y módulo ocurrió la acción" la auditoría debe referenciar ese catálogo de menú.
Decisión¶
Se crea, en cada base de datos de producto (BD del ERP y BD del CRM), una
tabla central de movimientos (audit_log) que registra cada acción del
usuario. El catálogo de navegación que la alimenta vive en core_sigfa
(ADR 0013) y lo sirve svc-identidad.
La navegación se organiza en tres niveles, cada uno en su tabla (ADR 0013):
- cat_aplicaciones — las aplicaciones (ERP, CRM). Contenedor, no cuenta
como nivel.
- cfg_modulos — los módulos de cada aplicación (rrhh, inventario...).
- cfg_menu — las páginas de un módulo, auto-referenciadas por padre_id
con máximo 3 niveles (submenús).
Las acciones (crear, editar, eliminar, ver, subir archivo) se asocian a cada
página de cfg_menu vía cfg_menu_acciones.
APLICACIÓN: ERP (sigfa.com.ec) — cat_aplicaciones ← contenedor (no cuenta como nivel)
└── MÓDULO: rrhh (cfg_modulos)
└── PÁGINA: nomina (cfg_menu, nivel 1)
├── registro (nivel 2)
└── vacaciones (nivel 2)
└── MÓDULO: inventario (cfg_modulos)
└── PÁGINA: ingreso (cfg_menu, nivel 1)
├── producto (nivel 2)
└── facturas (nivel 2)
APLICACIÓN: CRM (sigfapro.com.ec) — cat_aplicaciones
└── MÓDULO: clientes (cfg_modulos)
└── PÁGINA: cartera (cfg_menu, nivel 1)
└── MÓDULO: fuerza-ventas (cfg_modulos)
└── PÁGINA: promotores (cfg_menu, nivel 1)
Tabla cat_aplicaciones (catálogo de productos — ERP, CRM, etc.) vive en
core.identidad (ADR transversal 0013). cfg_modulos la referencia con
aplicacion_id; cfg_menu referencia su módulo con modulo_id y sus
submenús con padre_id. Todo se filtra por empresa_id + RLS.
Relación con la nomenclatura ADR 0004:
cfg_modulos,cfg_menuycfg_menu_accionesllevan prefijocfg_porque son configuración de sistema (se modifican al rearmar la navegación), no catálogos de negocio estáticos (cat_). Ver el ADR 0013 para el esquema completo.
Tabla cfg_menu (páginas de un módulo, máximo 3 niveles):
| Campo | Tipo | Notas |
|---|---|---|
id |
UUID PK | |
empresa_id |
BIGINT | RLS (ADR 0002) — refiere al catálogo mae_empresas (BIGINT, ADR 0011) |
modulo_id |
UUID FK → cfg_modulos |
módulo dueño de la página |
padre_id |
UUID FK → cfg_menu |
NULL = nivel 1 (página); nivel 2 → padre nivel 1; nivel 3 → padre nivel 2 |
nivel |
smallint | 1, 2 o 3 — nunca mayor a 3 |
codigo |
varchar | ej. nomina, registro, vacaciones |
nombre |
varchar | nombre legible |
ruta_frontend |
varchar | path de la SPA en React (ej. /inventario/ingreso), solo para navegación — no es el endpoint del API |
orden |
int | orden dentro del padre |
is_visible |
bool | se muestra en el sidebar |
is_activo |
bool | habilitado |
created_at, updated_at |
timestamptz |
Tabla cfg_menu_acciones (acciones de cada página):
| Campo | Tipo | Notas |
|---|---|---|
id |
UUID PK | |
empresa_id |
BIGINT | RLS (ADR 0002) — refiere al catálogo mae_empresas (BIGINT, ADR 0011) |
menu_id |
UUID FK → cfg_menu |
el ítem/página sobre el que aplica (cualquier nivel) |
codigo |
varchar | ej. crear, editar, eliminar, ver, subir_archivo |
nombre |
varchar | |
metodo_http |
varchar | POST/PUT/PATCH/DELETE/GET (referencia del controlador) |
created_at, updated_at |
timestamptz |
Tabla central audit_log (los movimientos):
| Campo | Tipo | Notas |
|---|---|---|
id |
UUID PK | |
empresa_id |
BIGINT | RLS (ADR 0002) — siempre presente; refiere al catálogo mae_empresas (BIGINT, ADR 0011) |
menu_id |
UUID (correlación → cfg_menu) |
la página exacta donde ocurrió |
accion_id |
UUID (correlación → cfg_menu_acciones) |
qué acción (ID del catálogo, nunca texto libre) |
modulo_id |
UUID (correlación → cfg_modulos) |
módulo donde ocurrió (snapshot del MODULE_MAP, ADR 0009) |
actor_user_id |
UUID (correlación → mae_usuarios) |
quién lo hizo |
actor_username / actor_nombre / actor_email |
varchar | copia del momento (del token): username, nombre y email del actor cuando ocurrió el movimiento |
modulo_codigo / modulo_nombre |
varchar | copia del momento (del MODULE_MAP del servicio): módulo donde ocurrió |
menu_codigo / menu_nombre |
varchar | copia del momento (del catálogo): página donde ocurrió, tal como se veía entonces |
accion_codigo / accion_nombre |
varchar | copia del momento (del catálogo): acción ejecutada |
fecha |
timestamptz | cuándo (default now()); columna de partición (rango mensual) |
metodo_http |
varchar | lo que reportó el header del controlador (POST, DELETE, GET, ...) |
endpoint |
varchar | endpoint del API capturado por el middleware (ADR 0009) |
referencia_tipo |
varchar | entidad de negocio afectada (ej. pedido, cliente, factura) |
referencia_id |
UUID | el registro de negocio afectado (vive en la misma BD del producto) |
detalle |
text | descripción legible: "se modificó el precio de 10 a 12" |
datos_antes |
jsonb | estado previo (opcional, snapshot) |
datos_despues |
jsonb | estado posterior (opcional, snapshot) |
archivos |
jsonb | archivos del request (vacío/null si no hay); ver estructura abaixo |
trace_id |
UUID | trazabilidad distribuida (ADR 0003) |
created_at |
timestamptz |
Qué son
referencia_tipo/referencia_id: apuntan al registro de negocio que se afectó, no al usuario ni al menú: por ejemplopedido+ su UUID, ocliente+ su UUID. El registro vive en la misma base del producto (el pedido está enerp), así que no rompen la regla de no cruzar bases, y sirven para responder "¿qué movimientos tuvo este pedido/cliente?" Mientrasactor_*ymenu_*/accion_*son copias legibles del momento (la auditoría es historia: muestra cómo estaba cada cosa cuando ocurrió el movimiento, ver ADR 0014),referencia_tipo/ididentifican el objeto afectado.
Ejemplo de archivos (jsonb) — 1 archivo:
[
{
"nombre": "factura-001.pdf",
"ruta": "https://s3.amazonaws.com/sigfa-erp/archivos/2026/09/factura-001.pdf",
"tipo": "application/pdf",
"hash": "sha256:abc123...",
"tamano_bytes": 245760,
"descripcion": "Factura original del proveedor"
}
]
Ejemplo de archivos (jsonb) — múltiples archivos, cada uno con su
descripción:
[
{
"nombre": "factura-001.pdf",
"ruta": "https://s3.amazonaws.com/sigfa-erp/archivos/2026/09/f-001.pdf",
"tipo": "application/pdf",
"hash": "sha256:abc123...",
"tamano_bytes": 245760,
"descripcion": "Factura original"
},
{
"nombre": "nota-credito.pdf",
"ruta": "https://s3.amazonaws.com/sigfa-erp/archivos/2026/09/nc-001.pdf",
"tipo": "application/pdf",
"hash": "sha256:def456...",
"tamano_bytes": 182400,
"descripcion": "Nota de crédito asociada"
},
{
"nombre": "comprobante-pago.png",
"ruta": "https://s3.amazonaws.com/sigfa-erp/archivos/2026/09/cp-001.png",
"tipo": "image/png",
"hash": "sha256:ghi789...",
"tamano_bytes": 51200,
"descripcion": "Comprobante de transferencia bancaria"
}
]
Estructura por archivo (campos flexibles):
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
nombre |
string | sí | nombre original del archivo |
ruta |
string | sí* | ruta local o URL de S3 (https://s3.amazonaws.com/...) |
tipo |
string | no | MIME type (application/pdf, image/png, ...) |
hash |
string | no | integridad (sha256:...) |
tamano_bytes |
int | no | tamaño en bytes |
descripcion |
string | no | descripción individual del archivo |
meta |
object | no | metadata libre adicional (ej. {"version": 2, "aprobado_por": "admin"}) |
*rutas: el camporutaacepta tanto ruta local como URL de S3. Hoy puede ser/storage/erp/archivos/f-001.pdf; mañana, cuando se migre a AWS S3, seráhttps://s3.amazonaws.com/sigfa-erp/archivos/.... El frontend solo muestra el link/icono del elemento; el formato del storage es transparente para la auditoría.
metaes un campo libre para que cada servicio agregue la metadata que necesite. Ejemplo: un servicio de calidad podría agregar{"estado_calidad": "aprobado", "lote": "L-2026-001"}; un servicio de RRHH podría agregar{"tipo_nomina": "mensual", "periodo": "2026-09"}. El sistema de auditoría no valida ni consumemeta; solo lo almacena.
Reglas de grabación¶
- Escrituras (POST/PUT/PATCH/DELETE): siempre se auditan.
- Lecturas sensibles (GET): se auditan solo sobre entidades sensibles (ej. ver cartera, ver factura, ver nómina), no en consultas de listado común.
- Un solo punto de emisión: la grabación se dispara de forma consistente
desde un middleware HTTP global (ADR 0009) que intercepta todas las
peticiones. El middleware ya conoce al usuario autenticado (JWT), el
metodo_http, elendpointy resuelve el módulo y la acción automáticamente. Un decorador opcional solo enriquece cuando hay acción especial o lectura sensible. - Mapping por servicio: cada microservicio declara un
MODULE_MAPque conecta los prefijos de sus endpoints con los códigos decfg_modulos(ADR 0013). El middleware usa este mapping para resolver el módulo sin parsing del path (ADR 0009). - La asociación es por IDs, no por rutas: el middleware (FastAPI) captura
metodo_httpyendpointdel request, y el catálogo cacheado resuelvemenu_id+accion_id.ruta_frontendes solo el path de la SPA de React para navegación y no se usa para casar auditoría ni para identificar el endpoint — así React y el API pueden tener rutas distintas sin romper el registro.
Ejemplo de doble ruta (frontend y backend):
Cuando un usuario crea un ingreso en la SPA:
React (SPA) — lo que ve el navegador:
/inventario/ingreso ← página del menú (ruta_frontend en `cfg_menu`)
/inventario/ingreso/crear ← formulario (navegación React Router)
Backend (FastAPI) — lo que invoca la SPA para guardar datos:
POST /api/v1/inventario/ingresos ← endpoint del controlador
GET /api/v1/inventario/ingresos ← listado
La auditoría registrada en audit_log para ese "crear":
modulo_id = módulo inventario ← del MODULE_MAP del servicio (snapshot)
modulo_codigo = inventario ← del MODULE_MAP (snapshot)
modulo_nombre = Inventario ← del MODULE_MAP (snapshot)
menu_id = página ingreso ← del catálogo cacheado (snapshot)
accion_id = crear ← del catálogo cacheado (snapshot)
menu_codigo = ingreso ← del catálogo (snapshot)
menu_nombre = Ingreso ← del catálogo (snapshot)
accion_codigo = crear ← del catálogo (snapshot)
accion_nombre = Crear ← del catálogo (snapshot)
actor_user_id = ... ← del token
actor_username = d.alcivar ← del token (snapshot)
actor_nombre = Duval Alcivar ← del token (snapshot)
metodo_http = POST ← del Request (automático)
endpoint = /api/v1/inventario/ingresos ← del Request (automático)
ruta_frontend (NO se guarda aquí) ← solo vive en `cfg_menu` para navegación
El middleware resuelve automáticamente: esta petición corresponde al módulo
X, a la página Y y a la acción Z". No existe un match automático por string
entre ruta_frontend y endpoint.
- La auditoría es un historial (snapshot del momento): al grabar se
copian también los nombres legibles — actor_username/actor_nombre/
actor_email (del token), modulo_codigo/modulo_nombre (del MODULE_MAP
del servicio) y menu_codigo/menu_nombre/
accion_codigo/accion_nombre (del catálogo). Así el registro es
autónomo: se lee con un solo SELECT local, sin consultar core_sigfa
ni resolver por endpoint, y refleja cómo estaba cada cosa en esa
pantalla cuando ocurrió el movimiento (si el usuario, el módulo o
el menú cambian después, el histórico no cambia; ver ADR 0014).
- Los archivos se referencian, no se copian en la BD: se guardan en
archivos, un único campo jsonb flexible que soporta de 0 a N
archivos. Cada archivo es un objeto con campos estándar (nombre,
ruta, tipo, hash, tamano_bytes, descripcion) más un campo
meta libre para metadata adicional del servicio. La sola presencia de
contenido en el campo (array no vacío) actúa como bandera de "hay
archivo(s)" para la vista de movimientos. El binario vive en S3; el
registro solo lo referencia.
- Migración a AWS S3: hoy ruta puede ser una ruta local
(/storage/erp/archivos/f-001.pdf); cuando se migre a S3, será una URL
(https://s3.amazonaws.com/sigfa-erp/archivos/...). El frontend solo
muestra el link/icono del elemento; el formato del storage es
transparente para la auditoría.
- RLS aplica (ADR 0002): cada empresa solo ve sus propios movimientos;
el catálogo de menú (cfg_menu) también es por empresa (RLS), por si un
producto diferencia permisos/menú por empresa.
- Escritura asíncrona: si el volumen lo exige, la emisión puede ir por
outbox/evento (regla 4 del ADR 0003) para no frenar la transacción de
negocio; la lectura siempre es directa en la BD del producto.
- Una tabla por producto, sin cruce entre bases: nunca un audit_log
global; cada BD (ERP o CRM) mantiene el suyo (ADR 0003: cero consultas
entre bases, correlación por UUID).
Particionamiento¶
audit_log se particiona por rango mensual sobre fecha
(PARTITION BY RANGE (fecha), con empresa_id como índice de RLS en cada
partición). Se elige mes porque mantiene 12 particiones/año (vs. 52 si
fueran semanales), coincide con la consulta típica ("movimientos de un
usuario/mes") y permite purgar o archivar un mes entero con un solo
DROP TABLE/detach, sin DELETE masivo.
Consecuencias técnicas:
- PK compuesta: la clave primaria debe incluir la columna de
partición →
PRIMARY KEY (id, fecha). - La partición se crea por adelantado, de forma programada:
Postgres no crea particiones automáticamente ante INSERT — si
fechacae en un rango sin partición, el INSERT falla. Un job (por ejemplopg_partmano un job del propio servicio que ejecutaCREATE TABLE ... PARTITION OF audit_log FOR VALUES FROM ... TO ...) crea con antelación las particiones del mes actual y del siguiente (margen de+1a+2meses). - Escritura como append: el write path escribe solo en la partición del mes corriente; las anteriores quedan inmóviles, ideales para indexar y para retención/solo lectura.
- Retención/purga: particiones viejas se detach + archivan o se eliminan completas según la política de retención definida por producto.
Alternativas descartadas¶
- Guardar únicamente los campos de auditoría de ADR 0004 — descartado: solo muestran quién tocó por última vez una fila, no el historial de movimientos ni qué cambió cada vez.
- Una sola tabla de auditoría en
corecompartida por ERP y CRM — descartado: viola la regla de cero consultas entre bases (ADR 0003) y mezcla movimientos de dos productos con catálogos de menú distintos. - Triggers de BD para registrar auditoría — descartado: acoplan la auditoría a la DDL, duplican la lógica en cada servicio y no tienen contexto del usuario/acción funcional (no saben qué botón o página fue).
- Texto libre en
accion_id(guardar nombre de acción como string) — descartado: perdería la referencia al catálogo de menú, imposibilita reportar "en qué módulo ocurrió qué" y desnormaliza datos que ya existen. - Páginas como tablas planas por nivel (
cat_paginas+ subniveles) — descartado: una página de nivel 2 y otra de nivel 3 son la misma naturaleza (ítem de menú); una sola tabla auto-referenciada (cfg_menuconpadre_id) recorre el árbol de páginas uniformemente, sin duplicar esquemas por nivel ni romper si el árbol crece distinto. Los módulos sí son tabla aparte (cfg_modulos, ADR 0013) porque son otra entidad: agrupan páginas y se muestran en la grilla de la aplicación.
Consecuencias¶
- Ganancia: trazabilidad completa y centralizada — quién, cuándo, qué acción, en qué módulo/página, sobre qué registro, con qué datos, y qué archivo (ruta/URL) se subió.
- Ganancia: el catálogo de menú se reutiliza para permisos/navegación y para la auditoría; el ID de pantalla/acción se toma del mismo lugar.
- Costo:
audit_logcrece con cada escritura (y cada lectura sensible); la partición mensual la mantiene manejable (purga porDROPde partición), pero exige un job que cree las particiones por adelantado y una política de retención definida por producto. - Pendiente: definir el catálogo inicial de acciones por pantalla (máx. 3 niveles), qué pantallas califican como "lectura sensible" y la política de retención de la tabla (tiempo/volumen) en cada producto.
- Modelo de datos: el esquema de navegación (
cat_aplicaciones,cfg_modulos,cfg_menu,cfg_menu_acciones) está en el ADR 0013;rel_empresa_aplicacionen el ADR 0006 de svc-identidad; yaudit_logen el ADR 0014 (con su PK compuesta(id, fecha), particionado y snapshot del módulo). La captura automática está en el ADR 0009 (middleware + mapping por servicio).