Notifications
¿Qué es?
Section titled “¿Qué es?”Sistema centralizado de notificaciones in-app para todos los portales de NappAI Fluency. Características clave:
- 79 tipos definidos en
notification-type.config.ts, cada uno con categoría, icono, severidad y canales predeterminados. - Deduplicación automática en ventana de 24 horas: mismo
userId + type + metadatano genera duplicados. - Multi-canal:
in_app(siempre),email(vía Resend),push(para futuras apps móviles). - Preferencias por usuario: el usuario puede silenciar tipos
disableable: true. - Severidad
critical: bypasea siempre las preferencias del usuario. - Push en tiempo real vía WebSocket (gateway en
notifications.gateway.ts). - Notificaciones a rol:
sendToRole()envía a todos los usuarios de un rol (capped a 50).
La infraestructura vive en apps/api/src/modules/notifications/. NotificationsModule exporta NotificationService para que cualquier módulo pueda enviar notificaciones.
Diagrama
Section titled “Diagrama”flowchart TD A[Domain Service / Job] -->|send / sendToRole| B[NotificationService] B --> C{Deduplicación 24h} C -->|Duplicado| D[Retorna existing] C -->|Nuevo| E[INSERT Notification] E --> F[EventEmitter → notification.created] E --> G{channels.includes email?} F --> H[NotificationsGateway → WebSocket push] G -->|Sí| I{severity critical?} I -->|No| J{user.notificationPrefs.emailEnabled?} J -->|true| K[Resend → email] I -->|Sí| K J -->|false| L[Skip email]API del NotificationService
Section titled “API del NotificationService”Archivo: apps/api/src/modules/notifications/notification.service.ts
send(params)
Section titled “send(params)”Método principal. Crea una notificación, aplica deduplicación y dispara los canales configurados (WebSocket, email).
async send(params: CreateNotificationParams): Promise<NotificationRecord>Parámetros de CreateNotificationParams:
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
userId | string | Sí | ID del usuario destinatario |
type | NotificationType | Sí | Tipo de la notificación — ver notification-type.config.ts |
title | string | Sí | Título corto (se muestra en la campana) |
body | string | Sí | Texto descriptivo de la notificación |
actionUrl | string | Sí | Ruta relativa o URL absoluta del CTA |
actionLabel | string | No | Texto del botón CTA (default: “Ver en NappAI Fluency”) |
metadata | Record<string, unknown> | No | Datos extra; forma parte de la clave de deduplicación |
sendToRole(role, params)
Section titled “sendToRole(role, params)”Envía una notificación a todos los usuarios de un rol (máximo 50, por seguridad).
async sendToRole( role: string, params: Omit<CreateNotificationParams, 'userId'>,): Promise<void>Otros métodos disponibles (para el propio controller de notificaciones):
| Método | Descripción |
|---|---|
findForUser(userId, query) | Lista paginada para un usuario, filtrable por category e isRead |
getUnreadCount(userId) | Número de notificaciones no leídas |
markAsRead(id, userId) | Marca una notificación como leída (verifica ownership) |
markAllRead(userId) | Marca todas como leídas |
markCategoryRead(userId, category) | Marca toda una categoría como leída |
deleteNotification(id, userId) | Elimina (solo info/success — no critical/warning) |
getPreferences(userId) | Devuelve preferencias de notificación del usuario |
updatePreferences(userId, prefs) | Actualiza (merge) preferencias del usuario |
getStats() | Estadísticas del día (solo SUPER_ADMIN) |
Patrón de uso
Section titled “Patrón de uso”1. Importar NotificationsModule
Section titled “1. Importar NotificationsModule”import { NotificationsModule } from '../notifications/notification.module'
@Module({ imports: [NotificationsModule],})export class MiFeatureModule {}2. Inyectar y llamar — siempre fire-and-forget
Section titled “2. Inyectar y llamar — siempre fire-and-forget”@Injectable()export class MiFeatureService { constructor(private readonly notificationService: NotificationService) {}
async onAlgoImportante(userId: string, quiz: Quiz) { // Fire-and-forget — NUNCA await sin .catch() — no debe bloquear el flujo principal this.notificationService.send({ userId, type: 'QUIZ_RETRY_AVAILABLE', title: '¡Puedes reintentar el quiz!', body: `Ya puedes volver a intentar "${quiz.title}".`, actionUrl: `/learn/quiz/${quiz.id}`, actionLabel: 'Ir al quiz', metadata: { quizId: quiz.id }, // ← incluir id para deduplicación correcta }).catch(() => {}) }}3. Notificación a todos los usuarios de un rol
Section titled “3. Notificación a todos los usuarios de un rol”// Alertar a todos los SUPER_ADMIN de un evento de plataformaawait this.notificationService.sendToRole('SUPER_ADMIN', { type: 'PLATFORM_HEALTH_DEGRADED', title: 'Degradación de la plataforma detectada', body: `Métrica "http_error_rate": valor 0.12 supera el umbral de 0.05.`, actionUrl: '/admin/platform/health', actionLabel: 'Ver estado del sistema', metadata: { alertType: 'http_error_rate', currentValue: 0.12 },})Deduplicación
Section titled “Deduplicación”La ventana es de 24 horas. Antes de crear una notificación, el servicio busca una existente con el mismo userId + type + metadata. Si existe, la devuelve sin crear duplicado.
const DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000 // 24 horasImplicación práctica: si el mismo evento puede ocurrir varias veces en un día (por ejemplo, el mismo quiz reintentado varias veces), incluir en metadata el identificador que hace única cada instancia:
// Incorrecto — todas las notificaciones de quiz del día se deduplicarán en una solametadata: { quizId: quiz.id }
// Correcto — cada intento genera su propia notificaciónmetadata: { quizId: quiz.id, attemptNumber: attempt.number }Severidad y preferencias de usuario
Section titled “Severidad y preferencias de usuario”| Severidad | ¿Respeta emailEnabled? | Ejemplos |
|---|---|---|
info | Sí | Progreso, recordatorios, recomendaciones |
success | Sí | Certificados, badges, hitos |
warning | Sí | Renovación próxima, racha en riesgo |
critical | No — siempre se envía | Alertas de seguridad, pagos fallidos, trials expirados |
Los tipos con disableable: false en la config siempre se crean en BD, independientemente de preferencias.
Tipos de notificación
Section titled “Tipos de notificación”Hay 79 tipos configurados en notification-type.config.ts, organizados por categoría:
LEARNING (estudiante)
Section titled “LEARNING (estudiante)”STREAK_RISK, STREAK_MILESTONE, STREAK_LOST, LESSON_COMPLETED, MODULE_COMPLETED, QUIZ_EVALUATED, TUTOR_RESPONSE, ADAPTIVE_RECOMMENDATION, WELCOME_BACK, CORP_NUDGE, PATH_ENROLLED, FLUENCY_TEST_RESULT, INACTIVITY_NUDGE, COURSE_DEADLINE_REMINDER, QUIZ_RETRY_AVAILABLE
ACHIEVEMENT (logros)
Section titled “ACHIEVEMENT (logros)”BADGE_EARNED, CERTIFICATE_EARNED, STREAK_MILESTONE
CONTENT (contenido)
Section titled “CONTENT (contenido)”NEW_CONTENT, CONTENT_UPDATED, NEW_CONTENT_RELEVANT, GUIDELINE_UPDATED
CREATOR (creadores)
Section titled “CREATOR (creadores)”PATH_REVIEW_SUBMITTED, PATH_REVIEW_APPROVED, PATH_REVIEW_REJECTED, PATH_REVIEW_FEEDBACK, NEW_ENROLLMENT, NEW_REVIEW, MILESTONE_ENROLLMENTS, EARNINGS_PAYOUT, BADGE_EARNED_CREATOR, CREATOR_APPLICATION_APPROVED, CREATOR_APPLICATION_REJECTED, PATH_UNPUBLISHED, EARNINGS_MILESTONE, CREATOR_WEEKLY_STATS, GUIDELINE_VIOLATION_WARNING, CASE_STUDY_EVALUATED
TEAM (corporativo — corp admin)
Section titled “TEAM (corporativo — corp admin)”MEMBER_CHURN_RISK, MEMBER_CERTIFICATE, MEMBER_ENROLLED, MEMBER_COMPLETED_PATH, SEATS_THRESHOLD, WEEKLY_DIGEST, MONTHLY_REPORT, SEAT_AUTO_RECLAIMED, CORP_PATH_ASSIGNED, MEMBER_INACTIVE_LONG, MEMBER_JOINED, BULK_IMPORT_COMPLETED, TEAM_MILESTONE
SYSTEM (plataforma — super admin)
Section titled “SYSTEM (plataforma — super admin)”PLAN_RENEWAL, PLAN_EXPIRED, INVOICE_GENERATED, AI_PROVIDER_DOWN, AI_HIGH_ERROR_RATE, AI_COST_THRESHOLD, AI_LATENCY_DEGRADED, QDRANT_UNAVAILABLE, CREDENTIAL_EXPIRING, NEW_INSTRUCTOR_APPLICATION, PATH_PENDING_REVIEW, USER_MILESTONE, TRIAL_EXPIRING, TRIAL_EXPIRED, SSO_CONFIG_ERROR, INVOICE_READY, NEW_ORG_PROVISIONED, ORG_TRIAL_EXPIRING, ORG_CHURNED, PAYMENT_FAILED, SECURITY_ALERT, PATH_REVIEW_OVERDUE, ABUSE_REPORT, PLATFORM_HEALTH_DEGRADED, CREATOR_CONTENT_SPIKE
AI_TASK (tareas IA)
Section titled “AI_TASK (tareas IA)”AI_TASK_COMPLETED, AI_TASK_FAILED, AI_TASK_SYSTEM
Añadir un nuevo tipo
Section titled “Añadir un nuevo tipo”Paso 1 — Definir en notification-type.config.ts
Section titled “Paso 1 — Definir en notification-type.config.ts”NUEVO_TIPO: { category: 'LEARNING', // LEARNING | ACHIEVEMENT | CONTENT | CREATOR | TEAM | SYSTEM | AI_TASK icon: '🎯', iconBg: 'var(--green-bg)', severity: 'info', // info | success | warning | critical channels: ['in_app', 'email'], // subconjunto de: in_app, email, push disableable: true, // false = siempre se envía},Paso 2 — Añadir al enum en schema.prisma y al union type en shared-types
Section titled “Paso 2 — Añadir al enum en schema.prisma y al union type en shared-types”// apps/api/prisma/schema.prisma — enum NotificationTypeenum NotificationType { // ...tipos existentes... NUEVO_TIPO}Después del cambio en el schema, aplicar con:
cd apps/api && pnpm prisma db pushexport type NotificationType = | 'TIPO_EXISTENTE_1' | 'TIPO_EXISTENTE_2' | 'NUEVO_TIPO' // ← añadir aquíEso es todo. El servicio ya maneja deduplicación, canales, WebSocket y email de forma transparente.
Notificaciones programadas vs. event-triggered
Section titled “Notificaciones programadas vs. event-triggered”| Criterio | Usar cron job | Usar send() directo |
|---|---|---|
| Consulta muchos usuarios a la vez | Si | — |
| Responde a una acción del usuario | — | Si |
| Depende de tiempo transcurrido | Si | — |
| Inmediata en el mismo request | — | Si |
Los cron jobs de notificaciones van en apps/api/src/modules/notifications/jobs/ y deben seguir el patrón de JobExecutionTrackerService. Ver Scheduled Jobs.
Endpoints REST
Section titled “Endpoints REST”/api/notifications JwtAuthGuard Lista paginada del usuario actual (filtros: category, isRead) /api/notifications/unread-count JwtAuthGuard Número de notificaciones no leídas /api/notifications/:id/read JwtAuthGuard Marca una notificación como leída /api/notifications/mark-all-read JwtAuthGuard Marca todas como leídas /api/notifications/mark-category-read JwtAuthGuard Marca toda una categoría como leída /api/notifications/:id JwtAuthGuard Elimina (solo info/success — no critical/warning) /api/notifications/preferences JwtAuthGuard Preferencias de notificación del usuario /api/notifications/preferences JwtAuthGuard Actualiza preferencias (merge) /api/notifications/stats SUPER_ADMIN Estadísticas del día por categoría y canal Modelo en BD
Section titled “Modelo en BD”Notification
Section titled “Notification”| Campo | Tipo | Descripción |
|---|---|---|
id | String (cuid) | PK |
userId | String | FK al usuario destinatario |
type | NotificationType | Tipo de la notificación |
category | NotificationCategory | Categoría derivada del config |
title | String | Título de la notificación |
body | String | Cuerpo descriptivo |
actionUrl | String | URL del CTA |
actionLabel | String? | Texto del botón CTA |
icon | String | Emoji del icono (del config) |
iconBg | String | CSS var del fondo del icono |
severity | String | info | success | warning | critical |
isRead | Boolean | Si ha sido leída (default false) |
readAt | DateTime? | Timestamp de lectura |
channels | String[] | Canales usados (in_app, email, push) |
metadata | Json? | Datos extra del evento (parte de la clave de dedup) |
emailSentAt | DateTime? | Timestamp de entrega de email |
pushSentAt | DateTime? | Timestamp de entrega push |
createdAt | DateTime | Timestamp de creación |
Índices: [userId, isRead, createdAt] y [userId, category, createdAt] para queries del panel de notificaciones.