Saltar a contenido

Novu — Notificaciones persistentes en el OMS

Fecha: 2026-08-03 · Estado: ✅ MVP cerrado y funcionando end-to-end (2026-08-04)

Documentos de esta carpeta:

Documento Para qué
Este README Análisis: el problema, qué es Novu, arquitectura del MVP, setup del ambiente y demo
guia-desarrollo.md Mapa guía para desarrolladores: recetas para agregar notificaciones nuevas, convenciones, cómo probar, troubleshooting
roadmap-features.md Backlog de features de notificación pendientes, por fases, mapeado sobre los eventos existentes del backend

1. El problema

Hoy todas las notificaciones del OMS son efímeras:

  • El backend emite por Socket.io (NotificationsGateway) a salas (user-{salesPersonCode}, roles, salas de dominio).
  • El frontend las muestra como toasts de react-toastify (WebSocketContext.tsx → notify()), que desaparecen a los segundos.
  • No hay persistencia en ningún lado: si el vendedor no está conectado en ese momento (logout, otro dispositivo, pestaña cerrada), la notificación se pierde para siempre. La campana del header era un mock con datos estáticos.

2. Qué es Novu y cómo encaja

Novu es una infraestructura de notificaciones open-source (self-hosted en nuestro caso). Conceptos clave:

Concepto Qué es En nuestro caso
Subscriber Un destinatario registrado en Novu Cada usuario del OMS, identificado por su salesPersonCode
Workflow Plantilla de notificación definida en el Dashboard (pasos: in-app, email, push…) oms-in-app: un solo paso In-App que renderiza {{payload.title}} y {{payload.body}}
Trigger Llamada del backend que dispara un workflow para un subscriber con un payload NovuService.notifyUser(...)
Inbox Componente React (campana + bandeja) que lee y marca notificaciones <NotificationInbox /> en el header del dashboard

Novu guarda cada notificación en su base de datos: el usuario la ve al volver a loguearse, desde cualquier dispositivo, con estado leído/no-leído. El socket de Novu (puerto 3002) solo agrega tiempo real cuando el usuario está conectado.

Importante: Novu complementa, no reemplaza al Socket.io actual. Los eventos de dominio (kanbans, comentarios, QR Bancard) siguen por el gateway; Novu se encarga de la bandeja persistente del usuario.

3. Arquitectura del MVP

flowchart LR
    subgraph Backend NestJS
        PS[payments.service] --> GW[NotificationsGateway.sendToUser]
        GW -->|socket efímero| FE1[Toast react-toastify]
        GW -->|"NovuService.notifyUser()"| NAPI
        TC[POST /notifications/novu-test] --> NS[NovuService]
        NS --> NAPI
    end
    subgraph Novu self-hosted 192.168.0.201
        NAPI[API :3100] --> DB[(MongoDB - persistencia)]
        DB --> WS[WebSocket :3002]
        DASH[Dashboard :4000]
    end
    subgraph Frontend Next.js
        WS -->|tiempo real| INBOX["Inbox (campana del header)"]
        NAPI -->|historial al abrir| INBOX
    end

Identidad unificada

El subscriberId de Novu es el salesPersonCode (como string) — el mismo identificador que ya usan las salas user-{salesPersonCode} del gateway. No hace falta sincronizar usuarios: Novu hace upsert del subscriber en cada trigger (el endpoint de prueba además le manda nombre y email).

4. Qué se implementó

Backend

Archivo Qué hace
src/config/novu.config.ts Config registerAs('novu') + schema Joi (NOVU_API_URL, NOVU_SECRET_KEY, opcionales)
src/services/novu/novu.module.ts Módulo siguiendo el patrón de cloudflare-images/algolia
src/services/novu/novu.service.ts Cliente @novu/api apuntando al self-hosted. trigger() genérico + notifyUser() (workflow oms-in-app). Fire-and-forget: un fallo de Novu nunca rompe el flujo de negocio. Si faltan las env vars se deshabilita con un warning (el backend arranca igual)
src/notifications/notifications.gateway.ts sendToUser() ahora también dispara Novu → cobros confirmados y anulados quedan persistidos sin tocar payments.service
src/notifications/order-notifications.listener.ts Escucha order.created y notifica al vendedor del pedido (Pedido #, cliente, total, link a /orders/{docEntry}). El evento se enriqueció en orders.service.ts con docNum/cardName/total/salesPersonCode
src/notifications/notifications.controller.ts POST /notifications/novu-test — dispara una notificación de prueba al usuario autenticado (título/cuerpo/url opcionales en el body)
.env NOVU_API_URL=http://192.168.0.201:3100 + NOVU_SECRET_KEY= (completar)

Frontend

Archivo Qué hace
src/app/(DashboardLayout)/layout/vertical/header/NotificationInbox.tsx Ya existía (componente Inbox de @novu/nextjs con tema MUI adaptado a dark/light). Se corrigió el subscriberId: ahora usa salesPersonCode (antes user.id con un fallback hardcodeado que nunca iba a coincidir con lo que emite el backend)
.env Se agregaron NEXT_PUBLIC_NOVU_BACKEND_URL y NEXT_PUBLIC_NOVU_SOCKET_URL (sin ellas el Inbox apuntaba a la nube de Novu, no a nuestro servidor)
Dockerfile + build-and-push.sh + .env.build-staging.example Las 3 vars NEXT_PUBLIC_NOVU_* propagadas como build args (evita el problema conocido de "la var quedó fuera del build")

5. Pasos para dejarlo funcionando (una sola vez)

  1. Secret key — Dashboard (http://192.168.0.201:4000) → API Keys → copiar la Secret Key → pegarla en backend/.env como NOVU_SECRET_KEY=.... ✅ Hecho (2026-08-03). Verificar de paso que el Application Identifier que muestra esa pantalla coincida con el del frontend/.env (VOHKCgrgMUn2).
  2. Workflow de Inbox — ✅ Creado vía API (2026-08-03) con un paso In-App que renderiza {{payload.title}} / {{payload.body}} y redirige a {{payload.url}}.
  3. ⚠️ Novu genera el identifier con un sufijo aleatorio (quedó oms-in-app-11bs4j37), por eso el backend lo lee de NOVU_IN_APP_WORKFLOW_ID en el .env (default oms-in-app). Si el workflow se recrea en otro ambiente, actualizar esa variable con el identifier que muestre Dashboard → Workflows.
  4. Levantar backend y frontend (npm run start:dev / npm run dev). Recordar que ambos leen su .env solo al arrancar.

6. Demo (el entregable)

Flujo principal — crear un pedido:

  1. Loguearse en el OMS. En el header aparece la campana del Inbox (reemplaza a la campana mock).
  2. Crear un pedido desde la app (/orders/new) como se hace siempre.
  3. Al confirmarse en SAP, el vendedor del pedido recibe en la campana: "Pedido #12345 creado para 'Cliente X' por GS 1.500.000" — en tiempo real si está conectado. Click en la notificación → navega al detalle del pedido.
  4. La prueba clave: cerrar sesión (o abrir otro navegador/dispositivo), volver a entrar → la notificación sigue ahí, con su estado de leída/no leída. Eso es lo que el sistema actual no puede hacer. Nota: si un Autorizador carga el pedido a nombre de un vendedor, la notificación le llega al vendedor, no al creador — se puede demostrar con dos usuarios.

Otros flujos que ya notifican: aprobar/anular un cobro (flujo de confirmación de pagos) → el vendedor recibe "Cobro confirmado"/"Cobro anulado" en su Inbox además del toast actual.

Prueba rápida sin flujo de negocio (requiere JWT): POST /notifications/novu-test desde Swagger (botón Authorize) o curl:

curl -X POST http://192.168.0.220:7000/api/notifications/novu-test \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"title": "Demo Novu", "body": "Notificación persistente 🎉"}'

7. Limitaciones del MVP y siguientes pasos

  • Solo sendToUser está integrado (cobros confirmados/anulados). Los eventos por rol (new_payment_draft → Autorizadores) necesitan Topics de Novu (suscripción de grupos) — siguiente iteración natural.
  • Sin HMAC: el Inbox se conecta con el subscriberId en claro; cualquiera que conozca un salesPersonCode podría leer las notificaciones de otro. Para producción hay que habilitar HMAC encryption (backend firma el subscriberId con la API key y el Inbox manda el hash). No es bloqueante en la red interna de pruebas.
  • URLs de producción/staging: cuando Novu pase de la máquina de pruebas (192.168.0.201) a un servidor definitivo, actualizar NOVU_API_URL (backend) y NEXT_PUBLIC_NOVU_* (frontend, en el .env.build-* del ambiente). El Inbox del navegador debe poder alcanzar esas URLs directamente — en producción convendrá exponerlas vía Nginx con TLS (ej: novu-api.compulandia.com.py).
  • Alta de subscribers: hoy es lazy (upsert en el primer trigger). Si se quiere que todos los usuarios existan en Novu desde el día 1 (p. ej. para campañas), agregar el upsert en el login del SSO.
  • Más canales: el mismo workflow puede sumar pasos de email/push/digest sin tocar el backend — solo se edita en el Dashboard. Candidato natural: reemplazar las tarjetas de Google Chat hardcodeadas del gateway.