Diagramas Técnicos ODIN
Documento interno del proyecto. Acceso restringido.
Diagramas Técnicos
Arquitectura real del sistema ODIN (Huginn / Muninn / Corporativo), generada a partir del estado actual de la base de datos y los workflows de n8n en producción — no es un diseño aspiracional, refleja lo que está construido y funcionando hoy. Incluye además las decisiones de estrategia y los problemas reales resueltos en el camino.
← Volver a la Bitácora01 Diagrama Entidad-Relación
Las 29 tablas de base_vectores_n8n agrupadas por dominio: identidad/negocio, consumo/facturación, y el módulo de expedientes de ODIN Corporativo — que ya tiene frontend en producción (antes solo backend). Se excluye el esquema de Cerveza Chalaca (cch_*), que vive en la misma base pero es un proyecto aparte.
erDiagram
TIPO_CLIENTE ||--o{ CLIENTES : clasifica
TIPO_CUENTA ||--o{ CUENTAS : clasifica
CLIENTES ||--o{ CUENTAS : tiene
CLIENTES ||--o{ USUARIOS_ABOGADO : emplea
CLIENTES ||--o{ EXPEDIENTES : "es dueño de"
CLIENTES ||--o{ CONTACTOS_CASO : registra
CLIENTES ||--o{ CONVERSACIONES_WHATSAPP : origina
CLIENTES ||--o{ INGESTED_FILES : sube
CLIENTES ||--o{ N8N_VECTORS2 : posee
CLIENTES ||--o{ JURISPRUDENCIA_BUSQUEDAS : busca
CLIENTES ||--o{ PROSPECTOS_CASOS : "publica (Huginn)"
CLIENTES ||--o{ PROSPECTOS_CASOS : "reclama (Muninn)"
CLIENTES ||--o{ MENSAJES_CASO : envia
CLIENTES ||--o{ BASES_VECTORIALES : configura
CLIENTES ||--o{ LITIGANTES : "vinculado a"
TIPO_CLIENTE ||--o{ PLANES : define
TIPO_CLIENTE ||--o{ NIVELES_TARIFA : define
PLANES ||--o{ CUENTAS : suscribe
NIVELES_TARIFA ||--o{ CUENTAS : "tarifa opcional"
CUENTAS ||--o{ CONSUMO_USO : genera
CUENTAS ||--o{ MOVIMIENTOS_CUENTA : registra
CUENTAS ||--o{ HISTORIAL_CONSULTAS : acumula
CUENTAS ||--o{ SESIONES_USUARIO : abre
PROSPECTOS_CASOS ||--o{ CALIFICACIONES : recibe
PROSPECTOS_CASOS ||--o{ MENSAJES_CASO : contiene
USUARIOS_ABOGADO ||--o{ EXPEDIENTES : atiende
USUARIOS_ABOGADO ||--o{ CITAS : agenda
USUARIOS_ABOGADO ||--o{ HORARIO_ATENCION : define
USUARIOS_ABOGADO ||--o{ PLAZOS_EXPEDIENTE : "responsable de"
EXPEDIENTES ||--o{ DOCUMENTOS : contiene
EXPEDIENTES ||--o{ CONTACTOS_CASO : involucra
EXPEDIENTES ||--o{ CITAS : origina
EXPEDIENTES ||--o{ HISTORIAL_CONSULTAS : "vinculado a"
EXPEDIENTES ||--o{ N8N_VECTORS2 : "vinculado a"
EXPEDIENTES ||--o{ PLAZOS_EXPEDIENTE : define
CONTACTOS_CASO ||--o{ CITAS : solicita
CONTACTOS_CASO ||--o{ CONVERSACIONES_WHATSAPP : sostiene
CLIENTES {
uuid id PK
int tipo_cliente_id FK
text nombre
text email
text telefono
text device_id
text sexo
smallint edad
text estado
}
CUENTAS {
uuid id PK
uuid cliente_id FK
int tipo_cuenta_id FK
int plan_id FK
int nivel_tarifa_id FK
numeric saldo_soles
numeric factor_margen
text estado
}
CONSUMO_USO {
bigint id PK
uuid cuenta_id FK
text canal
text tipo_operacion
int tokens_input
int tokens_output
numeric costo_final
numeric costo_cliente_soles
boolean exito
}
HISTORIAL_CONSULTAS {
uuid id PK
uuid cuenta_id FK
uuid expediente_id FK
text pregunta
text respuesta
text canal
}
PROSPECTOS_CASOS {
uuid id PK
uuid huginn_cliente_id FK
uuid muninn_cliente_id FK
text descripcion_caso
text estado
}
MENSAJES_CASO {
uuid id PK
uuid prospecto_id FK
uuid remitente_cliente_id FK
text remitente_tipo
boolean leido
}
CALIFICACIONES {
uuid id PK
uuid prospecto_id FK
smallint estrellas
text comentario
}
JURISPRUDENCIA_BUSQUEDAS {
uuid id PK
uuid cliente_id FK
text tema
text fuente
text tipo_norma_dispositivo
text sector
date fecha_inicio
date fecha_fin
}
N8N_VECTORS2 {
uuid id PK
uuid cliente_id FK
uuid expediente_id FK
text text
jsonb metadata
vector embedding
}
EXPEDIENTES {
uuid id PK
uuid cliente_id FK
uuid abogado_asignado_id FK
text nombre
text estado
}
LITIGANTES {
uuid id PK
uuid cliente_id FK
text nombre
text telefono
text rol_proceso
}
SESIONES_USUARIO {
uuid id PK
uuid cliente_id FK
uuid cuenta_id FK
text token
timestamp expira_en
}
PLAZOS_EXPEDIENTE {
uuid id PK
uuid expediente_id FK
uuid abogado_id FK
text tipo_plazo
date fecha_vencimiento
text estado
}
No incluida en el diagrama por no tener relación directa (es una tabla de referencia, no de negocio): precios_modelo (tarifas USD por token/consulta usadas por registrar_consumo_est()).
02 Diagrama de Clases
ODIN no es un backend orientado a objetos (es N8N + Postgres), así que este diagrama representa el modelo de dominio conceptual — cada clase agrupa sus atributos reales y las operaciones (webhooks) que actúan sobre ella. BusquedaJurisprudencia hoy cubre 3 fuentes reales (SPIJ, Poder Judicial, Indecopi) más una búsqueda preliminar por IA dentro del chat mismo.
classDiagram
class Cliente {
+uuid id
+TipoCliente tipo
+string nombre
+string email
+string telefono
+string device_id
+registrar()
+actualizarPerfil()
}
class Cuenta {
+uuid id
+TipoCuenta tipo
+decimal saldo_soles
+decimal factor_margen
+consultarSaldo()
+recargar()
+resetMensual()
}
class Plan {
+string nombre
+decimal precio
+string periodicidad
+bool incluye_spij
}
class NivelTarifa {
+string codigo
+decimal fijo_prepago
+decimal fijo_mensual
+decimal cuota_incluida_kb
}
class ConsumoUso {
+string tipo_operacion
+int tokens_input
+int tokens_output
+decimal costo_cliente_soles
+registrar()
}
class HistorialConsulta {
+string pregunta
+string respuesta
+string canal
}
class ProspectoCaso {
+string descripcion_caso
+string estado
+publicar()
+reclamar()
}
class MensajeCaso {
+string remitente_tipo
+string mensaje
+bool leido
+enviar()
+marcarLeido()
}
class Calificacion {
+int estrellas
+string comentario
+calificar()
}
class BusquedaJurisprudencia {
+string tema
+string fuente
+string tipoNorma
+string sector
+buscar()
+descargarDocumento()
+analizarConIA()
}
class Expediente {
+string nombre
+string estado
+UsuarioAbogado abogadoAsignado
}
class UsuarioAbogado {
+string nombre
+string rol
+string emailInstitucional
}
class Cita {
+datetime fechaHoraInicio
+string estado
+agendar()
+confirmarWhatsApp()
}
class PlazoExpediente {
+string tipoPlazo
+date fechaVencimiento
+string estado
}
Cliente "1" *-- "1..*" Cuenta
Cuenta "1" o-- "0..1" Plan
Cuenta "1" o-- "0..1" NivelTarifa
Cuenta "1" *-- "0..*" ConsumoUso
Cuenta "1" *-- "0..*" HistorialConsulta
Cliente "1" *-- "0..*" BusquedaJurisprudencia
Cliente "1" --> "0..*" ProspectoCaso : publica (Huginn)
Cliente "1" --> "0..*" ProspectoCaso : reclama (Muninn)
ProspectoCaso "1" *-- "0..*" MensajeCaso
ProspectoCaso "1" *-- "0..1" Calificacion
Cliente "1" *-- "0..*" Expediente
Expediente "0..*" --> "1" UsuarioAbogado : atendido por
Expediente "1" *-- "0..*" Cita
Expediente "1" *-- "0..*" PlazoExpediente
03 Diagrama de Casos de Uso
Los 5 actores del sistema y lo que cada uno puede hacer hoy. A diferencia de la primera versión de este diagrama (ago 2026), Expedientes y Predicción Judicial ya tienen frontend en producción — dejaron de ser solo backend.
graph LR
A1[Usuario Huginn
anónimo]
A2[Usuario Huginn
registrado]
A3[Abogado Muninn]
A4[Estudio Jurídico
Corporativo]
A5[Administrador
ODIN]
UC1((Consultar IA
legal))
UC2((Adjuntar PDF
o foto))
UC3((Ver historial
de consultas))
UC4((Publicar caso
a un abogado))
UC5((Chatear con
el abogado))
UC6((Calificar
abogado))
UC7((Ver bandeja
de casos))
UC8((Gestionar
perfil público))
UC9((Consultar y
recargar saldo))
UC10((Buscar Jurisprudencia
SPIJ / PJ / Indecopi))
UC11((Descargar
documento real))
UC12((Analizar
varios docs con IA))
UC13((Gestionar
expedientes))
UC14((Agendar citas
por WhatsApp *))
UC15((Confirmar pagos
Yape))
UC16((Configurar
tarifas))
UC17((Predecir fallo
por magistrado))
UC18((Ver cuentas
y conexiones))
UC19((Pedir jurisprudencia
preliminar en el chat))
A1 --> UC1
A1 --> UC3x[Límite 3/hora]
A2 --> UC1
A2 --> UC2
A2 --> UC3
A2 --> UC4
A2 --> UC5
A2 --> UC6
A3 --> UC1
A3 --> UC3
A3 --> UC7
A3 --> UC5
A3 --> UC8
A3 --> UC9
A3 --> UC10
A3 --> UC11
A3 --> UC12
A3 --> UC19
A4 --> UC1
A4 --> UC10
A4 --> UC11
A4 --> UC12
A4 --> UC13
A4 --> UC14
A4 --> UC17
A4 --> UC19
A5 --> UC15
A5 --> UC16
A5 --> UC18
style A1 fill:#1a1b1e,stroke:#07e3d4,color:#fdfdfd
style A2 fill:#1a1b1e,stroke:#07e3d4,color:#fdfdfd
style A3 fill:#1a1b1e,stroke:#07e3d4,color:#fdfdfd
style A4 fill:#1a1b1e,stroke:#07e3d4,color:#fdfdfd
style A5 fill:#1a1b1e,stroke:#d9705a,color:#fdfdfd
04 Secuencia — Jurisprudencia (SPIJ / PJ / Indecopi)
El mismo patrón arquitectónico se repite en las 3 fuentes reales de jurisprudencia. Muestra por qué existe el relay por celular: SPIJ, Poder Judicial e Indecopi bloquean por WAF cualquier IP de datacenter, incluida la del VPS de producción.
sequenceDiagram
actor Ab as Abogado (Muninn/Corporativo)
participant FE as Frontend
(SPIJ / PJ / Indecopi)
participant N8N as N8N Workflow
participant PG as Postgres
participant Cel as Relay
(celular, spij_relay.py)
participant Fuente as SPIJ / PJ / Indecopi
participant IA as Gemini 3.1
Ab->>FE: Busca por tema, fecha o número
FE->>N8N: POST /webhook/odin-jurisprudencia-buscar
N8N->>PG: ¿Suscripción/cuenta activa?
PG-->>N8N: Sí
N8N->>PG: ¿Búsqueda exacta en caché (24h)?
alt Hay caché con resultados
PG-->>N8N: Resultados guardados
else Sin caché
N8N->>Cel: POST /buscar (vía túnel SSH reverso)
Cel->>Fuente: Autentica + busca (curl, IP residencial)
Fuente-->>Cel: Resoluciones reales
Cel-->>N8N: JSON resultados
N8N->>PG: Vectoriza y guarda (categoria=fuente)
end
N8N-->>FE: Lista de resultados
FE-->>Ab: Tarjetas con checkbox de selección
Ab->>FE: Selecciona documentos + criterio
FE->>N8N: POST /webhook/.../analizar (rápido o profundo)
N8N->>Cel: POST /documento (por cada id)
Cel->>Fuente: Descarga documento real
Fuente-->>Cel: Texto/PDF completo
Cel-->>N8N: Documento
N8N->>IA: Documentos + criterio del abogado
IA-->>N8N: Análisis citando cada norma/resolución
N8N->>PG: Registra historial + consumo
N8N-->>FE: Respuesta con marca de agua
FE-->>Ab: Análisis + opción de descarga
05 Secuencia — Búsqueda preliminar de jurisprudencia (IA, nuevo sep 2026)
Flujo distinto a todos los anteriores: no usa el relay ni scraping propio. Cuando el abogado pide jurisprudencia dentro de la misma conversación del chat, ODIN llama directo a la búsqueda nativa de Google integrada en Gemini y teje el resultado en la misma respuesta, con un disclaimer explícito de que es preliminar.
sequenceDiagram
actor Ab as Abogado (Corporativo/Muninn)
participant FE as Chat ODIN
participant N8N as N8N Workflow
participant Agente as Gemini
(Agente RAG)
participant Ground as Gemini
(grounding: google_search)
Ab->>FE: "...y tráeme jurisprudencia y normas relacionadas"
FE->>N8N: POST /webhook/odin
N8N->>Agente: Documento + pregunta (base de documentos propia)
Agente-->>N8N: Análisis del caso
N8N->>N8N: ¿El mensaje pide jurisprudencia o normas?
alt Sí
N8N->>Ground: Área de derecho + caso (tools: google_search)
Ground-->>N8N: Tabla markdown con normas/jurisprudencia reales
N8N->>N8N: Concatena análisis + tabla + disclaimer fijo
end
N8N-->>FE: Un solo mensaje (análisis, y tabla si aplica)
FE-->>Ab: Respuesta narrativa continua, sin panel aparte
Costo real: US$ 14 por 1000 consultas de Gemini 3.x (5000 gratis/mes compartidas), registrado en consumo_uso con tipo_operacion=jurisprudencia_referencial_chat.
06 Secuencia — Predicción Judicial
Arquitectónicamente distinto de los flujos de jurisprudencia: en vez de buscar por similitud semántica en la base vectorial (RAG), busca en vivo en Google Drive y lee el texto completo de las sentencias — una estimación de patrón de un juez necesita el documento entero, no fragmentos parecidos.
sequenceDiagram
actor Ab as Abogado (Corporativo)
participant N8N as N8N Workflow
participant PG as Postgres
participant GD as Google Drive
(carpeta del cliente)
participant IA as Gemini 3.1
Ab->>N8N: POST /webhook/odin-prediccion-analizar
{juez, tipo_caso, caso_cliente, adjunto?}
N8N->>PG: ¿Cuenta activa y tipo_cliente = Corporativo?
alt No es Corporativo
PG-->>N8N: tipo_cliente ≠ ODIN
N8N-->>Ab: 403 "solo para clientes Corporativo"
else Es Corporativo
PG-->>N8N: cuenta + drive_folder_id del cliente
N8N->>GD: Buscar carpeta "Predicciones"
N8N->>GD: Buscar subcarpeta {juez}
N8N->>GD: Buscar subcarpeta {tipo_caso}
N8N->>GD: Buscar subcarpeta "sentencias"
alt Falta alguna carpeta
N8N-->>Ab: 404 con el nombre exacto que no encontró
else Carpetas completas
N8N->>GD: Listar y descargar cada sentencia (PDF)
GD-->>N8N: Texto completo de cada sentencia
alt Menos de 2 sentencias
N8N-->>Ab: 400 "se necesitan al menos 2"
else 2 o más sentencias
N8N->>IA: Sentencias completas + caso actual del cliente
IA-->>N8N: Criterios del juez, comparación,
rango % y descargo legal
N8N->>PG: Registra historial + consumo
(tipo_operacion=prediccion_judicial)
N8N-->>Ab: Análisis con marca de agua institucional
end
end
end
07 Estrategias
Decisiones de arquitectura y producto que se repitieron como principio a lo largo del proyecto, no casos aislados.
Workflows independientes, no un monolito que sigue creciendo
El workflow principal ya supera los 450 nodos. Cada funcionalidad grande nueva (Indecopi, Admin Consumo, Reset de saldo) se construyó como un workflow de n8n propio y separado, en vez de seguir agregando nodos al principal. Pendiente: extraer patrones repetidos del workflow grande a sub-workflows reutilizables.
Relay residencial contra bloqueos de red estatales
SPIJ, Poder Judicial e Indecopi bloquean por WAF cualquier IP de datacenter (incluida la del VPS). La solución no fue negociar con esas entidades ni pagar un proxy comercial: un teléfono Android con Termux hace de puente vía túnel SSH reverso, usando su propia IP residencial — el mismo relay sirve a las 3 fuentes.
Búsqueda con IA como complemento, no como reemplazo de las fuentes oficiales
Para «jurisprudencia preliminar» dentro del chat se optó por el grounding nativo de Gemini en vez de construir un cuarto scraper. Explícitamente marcado como no verificado, con disclaimer, y siempre remite a las pestañas oficiales (SPIJ/PJ/Indecopi) para el dato certero.
Autoalojar en vez de depender de terceros
Correo propio (Mailu) en lugar de Gmail/Workspace, y analítica propia (Umami) en lugar de Google Analytics — mismo patrón repetido: control total de los datos, sin cookies de terceros, todo en el mismo VPS que ya se paga.
Migraciones sin interrumpir el servicio
Tanto el cambio de dominio (ergosystemsperu.com → ergosystems.lat, forzado por una disputa con el proveedor) como cualquier cambio de infraestructura se hicieron en paralelo: el sistema viejo sigue vivo hasta confirmar que el nuevo funciona de punta a punta, nunca al revés.
El chat debe conocer sus propias herramientas
En vez de que la IA responda «no tengo información suficiente» cuando la pregunta calza con una pestaña real de ODIN (jurisprudencia, predicción), el prompt del agente ahora la reconoce y redirige explícitamente — diferenciado por tipo de cliente, para no ofrecer algo que ese cliente no tiene.
08 Problemas resueltos
Una selección de fallas reales encontradas y corregidas — no errores hipotéticos, todos ocurrieron en producción o durante pruebas contra producción.
WordPress reescribe JS nuevo y rompe toda la página
Un <, > o && suelto en un <script> nuevo hace que el filtro de texto de WordPress lo trate como si fuera una etiqueta HTML, y reescribe un && posterior en la página completa rompiendo todos los scripts. Fix permanente: escribir JS nuevo sin esos caracteres (usar regex/ternários), y revisar con node --check después de cada despliegue.
n8n: doble llave anidada rompe el parseo de un campo
Meter la sintaxis de expresión de n8n (doble llave) dentro de un template literal que ya está envuelto en otra doble llave de nivel superior produce «invalid syntax». La interpolación interna debe ser con signo de dólar y llave simple (JS nativo), nunca otra doble llave.
Ejecución duplicada al insertar un nodo intermedio
Al conectar un nodo nuevo entre dos que ya estaban conectados, no se eliminó la conexión directa original — el flujo corrió dos veces en paralelo y el cliente recibió la respuesta más rápida (la vieja, sin la funcionalidad nueva) en vez de la correcta.
SPIJ/PJ bloqueados por WAF de datacenter
El VPS de producción recibe 403/reset de TLS al conectar directo a los sitios del Poder Judicial. Resuelto con el relay residencial (ver Estrategias). Bug adicional encontrado: el WAF de SPIJ bloquea específicamente la huella TLS de urllib de Python, pero no la de curl — el relay usa curl via subprocess.
Caché guardando búsquedas vacías
Una búsqueda que devolvía 0 resultados se guardaba igual en caché, y las siguientes 24 horas cualquier abogado que buscara lo mismo recibía una respuesta vacía aunque el sitio original sí tenía resultados esa segunda vez. Fix: la caché solo aplica si resultados_encontrados > 0.
Nodos Code que reconstruyen el objeto desde cero pierden campos
Repetido más de una vez: un nodo Code que arma el objeto de salida a mano, campo por campo, en vez de esparcir ...item.json, descarta silenciosamente cualquier campo que no se liste explícitamente — provocó que la tarifa asignada a una cuenta se ignorara sin ningún error visible.
Certificado TLS de Mailu nunca se emite solo
El router de correo en Traefik es TCP passthrough (nunca termina TLS), así que Traefik jamás completa el reto ACME automáticamente aunque el DNS ya exista. Solución: forzar el certificado con un router HTTP temporal, y luego sí queda guardado y disponible para el correo.
09 Situación a la fecha
Estado real del sistema al 29 de septiembre de 2026.
Dominio
Migración completa de ergosystemsperu.com a ergosystems.lat (GoDaddy suspendió el dominio anterior por una disputa de cobro sin resolver). Todos los servicios — web, n8n, POS Callao, Chatwoot, correo, analítica — funcionan en el dominio nuevo. El dominio viejo sigue pagado hasta 2027, sin usar, por si se resuelve el reclamo.
Productos en producción
ODIN con sus 3 modalidades (Huginn público, Muninn por suscripción, Corporativo para estudios); POS Callao (PWA offline-first, aún no lanzado oficialmente al público); Tracking de Marcas (monitoreo de la Gaceta de Indecopi); Cerveza Chalaca (bot de WhatsApp con IA para venta directa, en cerveza-chalaca.com).
Infraestructura propia
Correo autoalojado (Mailu, con SPF/DKIM/DMARC verificados) y analítica autoalojada (Umami) — ambos en el mismo VPS. Respaldo automático semanal en 5 paquetes independientes por proyecto, con restauración probada de verdad, no solo asumida.
Pendiente / próximos pasos
Lanzamiento oficial de POS Callao; formalizar el flujo de login para litigantes de estudios Corporativo; WhatsApp para litigantes (diseñado, sin construir); extraer sub-workflows reutilizables del workflow principal; salida del modo «Prueba» de Google OAuth para onboarding de nuevos clientes Corporativo.
