Diagramas Técnicos ODIN

Diagramas Técnicos ODIN

Documento interno del proyecto. Acceso restringido.

Ergo Systems · Proyecto ODIN

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.

Última actualización: 29 sep 2026 Base de datos: 29 tablas activas Workflow principal: 455 nodos Dominio: ergosystems.lat
← Volver a la Bitácora

01 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
Frontend construido y probado Panel administrativo (*) — diseñado, todavía sin construir

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.

455NODOS EN EL WORKFLOW PRINCIPAL
29TABLAS ACTIVAS EN LA BASE
3FUENTES REALES DE JURISPRUDENCIA
4WORKFLOWS INDEPENDIENTES ACTIVOS

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.

Ergo Systems.com — Documentación técnica interna del proyecto ODIN