Notificar menos, pero mejor
Si una notificación no exige acción, decisión o validación, no debería enviarse. Filtrar antes de redactar.
Notificar menos, pero mejor. Cada alerta FARO debe ser una interrupción justificada: con destinatario, prioridad, acción esperada y consecuencia si no se actúa. Si todo alerta, nada alerta.
Este documento define el sistema MVP de alertas y notificaciones de FARO Connect. Cubre canales, tipos, prioridades, templates, anti-ruido, política de IA con guardrails, auditoría y métricas de efectividad. Pertenece a la familia FARO-TPL-001.
La regla rectora es simple: una alerta no es ruido; una alerta es una interrupción justificada. Si FARO notifica por cada respiración del sistema, el usuario lo manda a spam mental en tres días y el producto pierde su valor ejecutivo. Y si nada alerta, FARO queda como esos reportes que nadie abre: técnicamente existen, empresarialmente no.
El sistema MVP cubre dos canales (in-app + email), ocho tipos de notificación (alert, reminder, escalation, approval_request, evidence_review, score_warning, digest, system) y cuatro prioridades (critical, high, medium, low). WhatsApp, Slack/Teams y push mobile quedan preparados arquitectónicamente pero no entran al MVP: postergar canales es disciplina, no debilidad.
Cada notificación responde seis preguntas obligatorias: qué pasó, por qué importa, quién debe hacer algo, antes de cuándo, dónde actuar y qué pasa si no se actúa. Las que no responden las seis no entran al catálogo de templates. La diferencia entre "Nueva acción creada" y "FARO detectó crecimiento no rentable; se asignó acción crítica para revisar política de descuentos; vence el 06/06; si no se ejecuta, la recuperación del Score queda bloqueada" es la diferencia entre ruido y producto ejecutivo.
Sobre IA: aplicando D5 (IA con guardrails), la IA no redacta libremente alertas. Llena slots predefinidos (subject_intro, body_summary, recommendation_text) dentro de templates auditados, nunca redacta números y cada llamada queda registrada. Si la IA cae o el costo se dispara, hay fallback determinístico sin IA. Esto cumple la prohibición explícita de "IA redactando libremente alertas: riesgo de inconsistencia" que aparece en la lista MVP-No.
El contexto operativo es Empresa Demo Cuyo S.A.: las notificaciones de ejemplo usan códigos canónicos (TNS-001 Crecimiento no rentable, ACT-COM-001 Revisar política de descuentos, EVD-007 Política de descuentos firmada). El sistema notifica a los roles correctos según matriz RACI y consume catálogos canónicos definidos en catálogo de tensiones MVP, catálogo de acciones MVP y catálogo de evidencias MVP.
FARO no debe transformarse en una máquina de emails inútiles. Estos ocho principios filtran qué entra al sistema y qué se descarta antes de la primera línea de código.
Tesis de producto. FARO debe comunicar solo lo que requiere acción, decisión o validación. La notificación no debe decir "Nueva acción creada"; debe decir "FARO detectó crecimiento no rentable. Se asignó una acción crítica para revisar la política de descuentos. Vence el 06/06. Si no se ejecuta, la recuperación del Score queda bloqueada." Eso sí sirve.
Si una notificación no exige acción, decisión o validación, no debería enviarse. Filtrar antes de redactar.
Qué pasó, por qué importa, quién debe actuar, antes de cuándo, dónde, y qué pasa si no se actúa. Sin las seis, no entra.
Mismo usuario + misma entidad + mismo evento + mismo día = una sola notificación. Salvo que suba la prioridad.
Mejor "tenés 5 acciones que vencen esta semana" que cinco emails consecutivos. Lo segundo es terrorismo administrativo.
Vence, se escala, se rechaza evidencia, cambia responsable, sube severidad, se bloquea o se reabre. Si no cambió nada, no hay alerta nueva.
Lo crítico interrumpe. Lo medio y bajo se agrupa en un resumen 18:00 hs. Email crítico es la excepción, no la regla.
Sin action_url no hay notificación. El usuario debe poder pasar de "leí" a "actué" en un solo click.
La IA llena slots predefinidos dentro de templates auditados. Nunca redacta números. Si falla, fallback determinístico.
Dos canales activos en MVP. Tres canales arquitectónicamente preparados pero no productivos. Postergar es disciplina: cada canal nuevo es código, soporte, fallbacks y SLAs adicionales que no agregan valor hasta validar in-app + email.
Campana + panel lateral + centro de notificaciones. Para todo lo relevante. Sin costos por envío, sin dependencias externas, sin fallbacks complejos. Es el canal por defecto del sistema.
Solo para critical, high, evidencias pendientes y digest diario. Provider externo (Resend / SendGrid / SES / Postmark). Si mandamos email por cada respiración, se va a spam mental.
Schema y enum channel lo soporta. Falta integración productiva con WhatsApp Business API + plantillas aprobadas. Roadmap Enterprise.
Schema lo soporta vía enum. Falta workspace bot + auth OAuth. Útil para clientes con cultura digital madura. Roadmap Enterprise.
Depende de app nativa, que no es MVP. Cuando exista app, se conecta a la misma tabla notifications filtrando por channel = 'push'.
Regla operativa. El esquema de la tabla notifications ya incluye los cinco valores en el CHECK de channel. Activar un canal nuevo no requiere migración: requiere implementar el dispatcher, el provider, el template específico y el control de costos. La arquitectura no es el cuello de botella; la disciplina sí.
El enum notification_type de la tabla faro.notifications tiene exactamente estos 8 valores. Cualquier template debe declarar uno. Cualquier código que intente insertar otro valor recibe un constraint violation.
Evento que requiere atención inmediata o de corto plazo. Es el tipo más común para acciones vencidas, tensiones críticas detectadas o bloqueos.
Ej.: TNS-001 detectada en Empresa Demo · ACT-COM-001 vencida hace 2 días.
Aviso anticipado antes de que algo venza o se bloquee. No requiere acción inmediata pero sí planificación.
Ej.: ACT-COM-001 vence mañana · 3 evidencias pendientes esta semana.
Generado por WF-001 cuando una acción supera SLA, se bloquea sin solución o el responsable no responde. Va a rol superior.
Ej.: ACT-FIN-001 escalada L3 a director comercial · responsable sin avance hace 5 días.
Va al aprobador cuando una acción requiere su firma para avanzar (RACI tipo A). Bloquea el workflow hasta resolver.
Ej.: ACT-COM-002 requiere aprobación del gerente general antes de ejecutar.
El responsable cargó evidencia y el aprobador debe aprobar, rechazar o pedir más información. Es el evento más frecuente del workflow.
Ej.: EVD-007 cargada por responsable de ACT-COM-001 · pendiente revisión gerente.
Alerta ejecutiva cuando el FARO Score cae por debajo de umbral semanal o por tensión crítica acumulada. Va a dirección.
Ej.: Score pasó de 74 a 67 · principal tensión TNS-001 sin acción cerrada.
Email diario o semanal con consolidación de no-críticos. Evita que medium/low se envíen sueltos. Una sola interrupción por día.
Ej.: Resumen del día · 3 acciones abiertas · 1 vencida · 2 evidencias por revisar.
Mensajes administrativos del sistema: error de integración, mantenimiento, cambios de rol, password reset. No bloquean operación.
Ej.: Integración Tango sin sincronizar hace 6 hs · cambio de responsable confirmado.
El enum priority tiene exactamente 4 valores. Cada uno define qué canal usa, en cuánto tiempo se espera respuesta y cuándo gatilla escalamiento automático en WF-001.
Canal: in-app + email. Notifica al responsable y al rol superior simultáneamente. Si no hay respuesta en SLA, WF-001 escala L4 a director.
tension_detected critical
action_expired critical
escalation L3/L4
score_penalized critical
Canal: in-app + email. Notifica al responsable directo. Si vence SLA, sube a critical y se notifica al gerente. Escalamiento L2.
action_expired
evidence_rejected
action_blocked
tension_reopened
score_blocked
Canal: in-app (siempre) + email (opcional según preferencias). Notifica al responsable. Si vence SLA, sube a high. La mayoría de eventos del MVP.
action_assigned
action_due_soon
evidence_uploaded
tension_closed
action_closed
Canal: solo in-app + entra al digest diario. Nunca envía email suelto. Pensado para no interrumpir, solo informar cuando el usuario decide mirar.
action_started
comment_added
score_recalculated
system_info
Reglas prácticas operativas. Tensión crítica detectada → critical. Acción crítica vencida → critical. Escalamiento L3/L4 → critical. Evidencia crítica rechazada → high. Evidencia pendiente de aprobación → high/medium según severidad de acción. Acción por vencer (24-48 hs) → medium. Acción asignada → medium. Acción cerrada → low/medium. Comentario suelto → low. Estas reglas se aplican en createNotification.priorityOverride o se heredan del default_priority del template.
Cada evento del sistema (disparado por WF-001, OPS-001, motor evaluador o UI) tiene destinatario y prioridad definida acá. Si un evento no aparece, no genera notificación: el silencio también es decisión de producto.
| Evento | Destinatario | Prioridad | Canal | Template |
|---|---|---|---|---|
| Categoría · Tensión | ||||
tension_detected · critical | Gerente general + Director | Critical | in-app + email | TPL-TENSION-CRITICAL-DETECTED |
tension_assigned | Responsable asignado | Medium | in-app | TPL-TENSION-ASSIGNED |
tension_escalated | Rol superior según WF-001 | Critical/High | in-app + email | TPL-ESCALATION-CREATED |
tension_reopened | Responsable + Gerencia | High | in-app + email | TPL-TENSION-REOPENED |
tension_closed | Gerencia + Dirección | Medium | in-app | TPL-TENSION-CLOSED |
| Categoría · Acción | ||||
action_created | Responsable | Medium | in-app | TPL-ACTION-ASSIGNED |
action_assigned | Responsable | Medium | in-app + email | TPL-ACTION-ASSIGNED |
action_started | Gerente (opcional) | Low | in-app · digest | TPL-ACTION-STARTED |
action_due_soon | Responsable | Medium | in-app + email | TPL-ACTION-DUE-SOON |
action_expired | Responsable + Gerente | High/Critical | in-app + email | TPL-ACTION-EXPIRED |
action_blocked | Gerente de área | High | in-app + email | TPL-ACTION-BLOCKED |
action_escalated | Destinatario escalamiento | Critical/High | in-app + email | TPL-ESCALATION-CREATED |
action_closed | Gerente + Dirección | Medium | in-app | TPL-ACTION-CLOSED |
action_reopened | Responsable + Gerente | High | in-app + email | TPL-ACTION-REOPENED |
| Categoría · Evidencia | ||||
evidence_uploaded | Aprobador | Medium | in-app | TPL-EVIDENCE-REVIEW-REQUEST |
evidence_submitted | Aprobador | Medium | in-app + email | TPL-EVIDENCE-REVIEW-REQUEST |
evidence_approved | Responsable | Medium | in-app | TPL-EVIDENCE-APPROVED |
evidence_rejected | Responsable | High | in-app + email | TPL-EVIDENCE-REJECTED |
evidence_needs_more_info | Responsable | High | in-app + email | TPL-EVIDENCE-MORE-INFO |
evidence_review_overdue | Aprobador + Gerente | High | in-app + email | TPL-EVIDENCE-REVIEW-OVERDUE |
| Categoría · Score | ||||
score_penalized · critical | Gerencia + Dirección | Critical | in-app + email | TPL-SCORE-WARNING |
score_blocked | Gerente | High | in-app + email | TPL-SCORE-WARNING |
score_recovered | Gerencia | Medium | in-app | TPL-SCORE-RECOVERED |
score_deteriorated_weekly | Dirección | High | in-app + email | TPL-SCORE-WARNING |
score_recalculated | No siempre · digest | Low | digest | — |
La matriz cubre 25 eventos canónicos agrupados en 4 categorías (tensión, acción, evidencia, score). El motor evaluador, WF-001 y el job JOB-NOTIF-001 producen estos eventos; createNotification los traduce a registros en faro.notifications aplicando dedupe y resolución de destinatarios.
Tres mecanismos que impiden que el sistema se convierta en spam: dedupe_key determinístico, ventana temporal de agrupación y digest 18:00 hs. Sin estos tres, el MVP fracasa el primer mes.
El dedupe_key identifica notificaciones equivalentes para suprimir duplicados en ventana de 24 hs. Si el caller no lo pasa explícito, se construye determinísticamente con cuatro elementos: template, usuario o rol, tipo de entidad y entidad. Insertar una segunda notificación con el mismo dedupe_key dentro de 24 hs devuelve suppressed: true sin crear nueva fila.
function buildDedupeKey(params: { templateCode: string; userId?: string | null; roleCode?: string | null; entityType?: string | null; entityId?: string | null; }) { return [ params.templateCode, params.userId ?? params.roleCode ?? "unknown", params.entityType ?? "none", params.entityId ?? "none" ].join(":"); } // Ejemplo: TPL-ACTION-EXPIRED:user-uuid:action:act-uuid // Esta key dura 24 hs · segunda inserción → suppressed
Cuando se disparan múltiples eventos críticos de la misma categoría (ej.: 5 tensiones detectadas en la misma sincronización), solo la primera genera notificación inmediata. Las siguientes se agrupan en una "actualización" 60 minutos después. Esto evita el efecto "flood" que aparece cuando una integración trae datos atrasados de un día.
SELECT COUNT(*) AS recent_critical FROM faro.notifications WHERE company_id = $1 AND priority = 'critical' AND entity_type = $2 -- tension / action / evidence / score AND created_at >= now() - INTERVAL '1 hour'; -- Si recent_critical >= 1 · suppress, programar digest +60min -- Si recent_critical = 0 · enviar inmediato
El job digest-daily corre a las 18:00 hs (zona horaria empresa). Consolida todos los eventos medium y low de las últimas 24 hs por destinatario en un único email. Los critical y high ya se enviaron en tiempo real. El usuario recibe máximo dos emails productivos por día: críticos al momento + digest al cierre.
SELECT COUNT(*) FILTER ( WHERE status NOT IN ('closed', 'cancelled', 'rejected') ) AS open_actions, COUNT(*) FILTER ( WHERE status NOT IN ('closed', 'cancelled', 'rejected') AND due_date < CURRENT_DATE ) AS expired_actions, COUNT(*) FILTER ( WHERE status = 'in_review' ) AS in_review_actions, COUNT(*) FILTER ( WHERE status = 'blocked' ) AS blocked_actions, COALESCE(SUM((payload->>'expected_score_recovery_max')::numeric), 0) AS potential_recovery FROM faro.actions WHERE company_id = $1 AND responsible_user_id = $2;
El dedupe se invalida y se genera nueva notificación cuando: (1) sube la severidad o prioridad, (2) la entidad vence, (3) se escala a otro nivel, (4) se rechaza evidencia, (5) cambia el responsable, (6) se bloquea o (7) se reabre. En todos los demás casos, dentro de 24 hs, el sistema asume "ya te avisamos" y suprime.
Cada template usa slots {{variable}} que el renderer reemplaza con el payload. Validados contra required_payload_keys: si falta una key, la notificación falla antes de enviarse para no renderizar basura.
Tono FARO en email. Sin emojis, sin signos de exclamación, sin colores estridentes. El asunto empieza con [FARO] y el código de entidad para que filtros de email puedan auto-clasificar. El cuerpo responde las seis preguntas ejecutivas en bloques cortos. El CTA es un único link a la entidad en FARO Connect. Cero marketing.
Asunto: [FARO] Tensión crítica detectada: {{tension_code}}
FARO detectó una tensión crítica que requiere atención.
Tensión:
{{tension_code}} · {{tension_title}}
Lectura ejecutiva:
{{executive_diagnosis}}
Prioridad:
{{priority_score}}/100
Impacto estimado en Score:
{{score_impact}} pts
Acción recomendada:
Ingresar a FARO Connect y revisar la Bandeja de Tensiones.
Ver tensión:
{{action_url}}
Asunto: [FARO] Nueva acción asignada: {{action_code}}
Se te asignó una nueva acción FARO.
Acción:
{{action_code}} · {{action_title}}
Tensión asociada:
{{tension_code}} · {{tension_title}}
Vencimiento:
{{due_date}}
Criterio de cierre:
{{closure_criteria}}
Evidencia requerida:
{{evidence_required}}
Ver acción:
{{action_url}}
Asunto: [FARO] Acción vencida: {{action_code}}
Una acción FARO se encuentra vencida.
Acción:
{{action_code}} · {{action_title}}
Responsable:
{{responsible_name}}
Venció el:
{{due_date}}
Tensión asociada:
{{tension_code}} · {{tension_title}}
Impacto:
La recuperación del Score puede quedar bloqueada hasta regularizar la acción.
Próximo paso:
Actualizar estado, cargar evidencia o escalar el bloqueo.
Ver acción:
{{action_url}}
Asunto: [FARO] Escalamiento {{escalation_level}} generado
FARO generó un escalamiento operativo.
Nivel:
{{escalation_level}}
Motivo:
{{reason_description}}
Entidad:
{{entity_code}} · {{entity_title}}
Responsable original:
{{from_user_name}}
Escalado a:
{{target_name_or_role}}
Próximo paso:
Revisar el bloqueo, tomar decisión o reasignar.
Ver escalamiento:
{{action_url}}
Asunto: [FARO] Evidencia pendiente de revisión: {{evidence_code}}
Hay una evidencia pendiente de revisión.
Evidencia:
{{evidence_code}} · {{evidence_title}}
Acción:
{{action_code}} · {{action_title}}
Cargada por:
{{submitted_by_name}}
Requiere revisión:
{{requires_review}}
Próximo paso:
Aprobar, rechazar o pedir más información.
Revisar:
{{action_url}}
Asunto: [FARO] Evidencia rechazada: {{evidence_code}}
Una evidencia fue rechazada.
Evidencia:
{{evidence_code}} · {{evidence_title}}
Acción:
{{action_code}} · {{action_title}}
Motivo:
{{review_comment}}
Próximo paso:
Corregir la evidencia y volver a enviarla.
Ver acción:
{{action_url}}
El catálogo completo de 12 templates seed (in-app + email) está definido en sección 9 como inserts SQL idempotentes con ON CONFLICT (template_code, version, channel) DO UPDATE. Los templates restantes (TPL-ACTION-DUE-SOON, TPL-ACTION-BLOCKED, TPL-EVIDENCE-APPROVED, TPL-ACTION-CLOSED, TPL-SCORE-WARNING, TPL-DAILY-DIGEST) siguen el mismo patrón y no se replican acá.
DDL completo de faro.notifications, faro.notification_templates y faro.notification_preferences. Si faro.notifications ya existe desde WF-001, este documento la formaliza y extiende.
faro.notificationsCREATE TABLE IF NOT EXISTS faro.notifications ( notification_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), company_id uuid NOT NULL, user_id uuid NULL, role_code text NULL, channel text NOT NULL CHECK ( channel IN ('in_app', 'email', 'whatsapp', 'slack', 'teams') ), notification_type text NOT NULL CHECK ( notification_type IN ( 'alert', 'reminder', 'escalation', 'approval_request', 'evidence_review', 'score_warning', 'digest', 'system' ) ), priority text NOT NULL DEFAULT 'medium' CHECK ( priority IN ('low', 'medium', 'high', 'critical') ), title text NOT NULL, body text NOT NULL, action_label text NULL, action_url text NULL, entity_type text NULL CHECK ( entity_type IS NULL OR entity_type IN ( 'tension', 'action', 'evidence', 'score', 'report', 'system' ) ), entity_id uuid NULL, tension_id uuid NULL, action_id uuid NULL, evidence_id uuid NULL, escalation_id uuid NULL, template_code text NULL, template_version integer NOT NULL DEFAULT 1, status text NOT NULL DEFAULT 'pending' CHECK ( status IN ( 'pending', 'queued', 'sent', 'delivered', 'read', 'failed', 'cancelled', 'suppressed' ) ), provider text NULL, provider_message_id text NULL, provider_response jsonb NOT NULL DEFAULT '{}'::jsonb, dedupe_key text NULL, payload jsonb NOT NULL DEFAULT '{}'::jsonb, -- IA audit trail (D5) ai_used boolean NOT NULL DEFAULT false, ai_model text NULL, ai_slots_filled text[] NOT NULL DEFAULT ARRAY[]::text[], ai_call_id uuid NULL, ai_latency_ms integer NULL, scheduled_at timestamptz NOT NULL DEFAULT now(), queued_at timestamptz NULL, sent_at timestamptz NULL, delivered_at timestamptz NULL, read_at timestamptz NULL, failed_at timestamptz NULL, failure_reason text NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_notifications_company_user_status ON faro.notifications(company_id, user_id, status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_notifications_company_role_status ON faro.notifications(company_id, role_code, status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_notifications_entity ON faro.notifications(company_id, entity_type, entity_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_notifications_dedupe ON faro.notifications(company_id, dedupe_key); CREATE INDEX IF NOT EXISTS idx_notifications_pending ON faro.notifications(status, scheduled_at) WHERE status IN ('pending', 'queued'); CREATE INDEX IF NOT EXISTS idx_notifications_ai_audit ON faro.notifications(company_id, ai_used, created_at DESC) WHERE ai_used = true;
faro.notification_templatesCREATE TABLE IF NOT EXISTS faro.notification_templates ( notification_template_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), template_code text NOT NULL, version integer NOT NULL DEFAULT 1, name text NOT NULL, description text NOT NULL, channel text NOT NULL CHECK ( channel IN ('in_app', 'email', 'whatsapp', 'slack', 'teams') ), notification_type text NOT NULL CHECK ( notification_type IN ( 'alert', 'reminder', 'escalation', 'approval_request', 'evidence_review', 'score_warning', 'digest', 'system' ) ), subject_template text NULL, title_template text NOT NULL, body_template text NOT NULL, action_label_template text NULL, action_url_template text NULL, default_priority text NOT NULL DEFAULT 'medium' CHECK ( default_priority IN ('low', 'medium', 'high', 'critical') ), required_payload_keys text[] NOT NULL DEFAULT ARRAY[]::text[], -- IA controlada (D5) ai_enabled boolean NOT NULL DEFAULT false, ai_slots text[] NOT NULL DEFAULT ARRAY[]::text[], is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(template_code, version, channel) );
faro.notification_preferencesCREATE TABLE IF NOT EXISTS faro.notification_preferences ( notification_preference_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), company_id uuid NOT NULL, user_id uuid NOT NULL, channel text NOT NULL CHECK ( channel IN ('in_app', 'email', 'whatsapp', 'slack', 'teams') ), notification_type text NOT NULL, enabled boolean NOT NULL DEFAULT true, minimum_priority text NOT NULL DEFAULT 'medium' CHECK ( minimum_priority IN ('low', 'medium', 'high', 'critical') ), digest_enabled boolean NOT NULL DEFAULT false, digest_frequency text NULL CHECK ( digest_frequency IS NULL OR digest_frequency IN ('daily', 'weekly') ), quiet_hours_start time NULL, quiet_hours_end time NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(company_id, user_id, channel, notification_type) ); -- Regla MVP simple sin lookup en preferences: -- critical/high → siempre se notifica; -- medium → según canal; -- low → solo in-app.
| Código | Evento | Canales seed | Priority default |
|---|---|---|---|
TPL-TENSION-CRITICAL-DETECTED | Tensión crítica detectada | in-app + email | Critical |
TPL-ACTION-ASSIGNED | Acción asignada | in-app + email | Medium |
TPL-ACTION-DUE-SOON | Acción próxima a vencer | in-app + email | Medium |
TPL-ACTION-EXPIRED | Acción vencida | in-app + email | High |
TPL-ACTION-BLOCKED | Acción bloqueada | in-app + email | High |
TPL-ESCALATION-CREATED | Escalamiento generado | in-app + email | Critical |
TPL-EVIDENCE-REVIEW-REQUEST | Evidencia pendiente de revisión | in-app + email | Medium |
TPL-EVIDENCE-APPROVED | Evidencia aprobada | in-app | Medium |
TPL-EVIDENCE-REJECTED | Evidencia rechazada | in-app + email | High |
TPL-ACTION-CLOSED | Acción cerrada | in-app | Medium |
TPL-SCORE-WARNING | Deterioro de Score | in-app + email | High |
TPL-DAILY-DIGEST | Resumen diario | Medium |
12 templates seed obligatorios. Cada uno se inserta vía V056__seed_notification_templates_mvp.sql con ON CONFLICT (template_code, version, channel) DO UPDATE para que el seed sea idempotente.
El sistema de notificaciones es disparado por el workflow de escalamiento (WF-001) y procesado por jobs cron operados desde observabilidad ops (OPS-001). No hay disparo manual desde UI.
WF-001 crea escalamientoon escalation_created: // 1. log_execution_event en timeline await logExecutionEvent({ company_id, entity_type, entity_id, event_type: "escalation_created", payload: { escalation_level, reason, from_user, target_role } }); // 2. createNotification con template TPL-ESCALATION-CREATED await createNotification({ client, companyId, roleCode: target_role, channel: "in_app", templateCode: "TPL-ESCALATION-CREATED", entityType: "escalation", entityId: escalation_id, escalationId: escalation_id, priorityOverride: escalation_level >= 3 ? "critical" : "high", payload: { escalation_level, reason_description, target_role, escalation_id } }); // 3. duplicar para email si priority >= high if (priority in ["critical", "high"]) { await createNotification({ ...same, channel: "email" }); }
Acción venceon action_expired: await markActionStatus({ action_id, status: "expired" }); await logExecutionEvent({ event_type: "action_expired" }); await createNotification({ templateCode: "TPL-ACTION-EXPIRED" }); // Si la acción es critical, escalamiento automático if (action.severity === "critical") { await createEscalation({ level: 2, reason: "critical_action_expired" }); // → dispara flujo 10.1 }
Evidencia rechazadaon evidence_rejected: await markEvidenceStatus({ evidence_id, status: "rejected", review_comment }); await markActionStatus({ action_id, status: "waiting_evidence" }); await logExecutionEvent({ event_type: "evidence_rejected" }); await createNotification({ templateCode: "TPL-EVIDENCE-REJECTED", payload: { evidence_code, review_comment, action_id } });
El job JOB-NOTIF-001 (notification dispatcher) corre cada 1-5 minutos y procesa la cola de notificaciones pending. Los demás jobs producen eventos que generan notificaciones nuevas:
Procesa status = 'pending' ordenado por prioridad. Marca queued, llama provider (email) o marca sent (in-app), guarda provider_response.
Detecta acciones con due_date = CURRENT_DATE + 1 y crea notificaciones TPL-ACTION-DUE-SOON aplicando dedupe por acción.
Consolida medium/low del día por destinatario y envía TPL-DAILY-DIGEST por email. Si no hay nada, no envía (empty state silencioso).
Detecta evidencias en status = 'in_review' hace más de 48 hs sin acción y dispara TPL-EVIDENCE-REVIEW-OVERDUE al aprobador + gerente.
Re-evalúa tensiones críticas activas sin notificación reciente y dispara TPL-TENSION-CRITICAL-DETECTED o re-disparo si subió severidad.
FARO-TPL-001 lista explícitamente entre las exclusiones MVP: "IA redactando libremente alertas → riesgo de inconsistencia". La decisión D5 (IA con guardrails) resuelve cómo usar IA sin romper esa regla.
La IA solo puede llenar tres slots predefinidos dentro de templates auditados. El esqueleto de la notificación, los códigos canónicos, las URLs, las fechas y todos los números provienen del payload determinístico. La IA aporta narrativa breve, no datos ni decisiones.
Frase corta (≤ 80 caracteres) que personaliza el contexto del asunto del email. Ej.: "Atención requerida en política comercial". Nunca incluye números.
Resumen ejecutivo de 2-3 líneas que sintetiza el contexto de la tensión o acción. Sin números, sin fechas, sin códigos. Sí menciona el área impactada y el carácter del problema.
Recomendación ejecutiva accionable de 1-2 líneas. Ej.: "Revisar política de descuentos y validar criterio comercial con gerencia." Nunca prescribe montos ni fechas.
Guardrails de validación. Cada slot pasa por validador antes de inyectarse al template:
Audit trail obligatorio. Cada llamada a IA queda registrada en columnas dedicadas de faro.notifications: ai_used, ai_model, ai_slots_filled[], ai_call_id, ai_latency_ms. Permite auditar costo, calidad y blast radius si un prompt empieza a fallar en producción.
Fallback sin IA. Si el ai-gateway no responde, devuelve error, supera 500ms de latencia o supera budget mensual, el sistema usa el slot por defecto del template (texto estático cuidado) y marca ai_used = false. La notificación se envía igual: el negocio no se detiene porque la IA esté caída.
async function fillAiSlots(params: { templateCode: string; context: { tension?: Tension; action?: Action; evidence?: Evidence }; fallback: Record<string, string>; }): Promise<{ slots: Record<string, string>; aiUsed: boolean; callId?: string }> { try { const response = await aiGateway.complete({ template_code: params.templateCode, mode: "slot_fill", slots: ["subject_intro", "body_summary", "recommendation_text"], context: params.context, max_tokens: 240, timeout_ms: 500, guardrails: { forbid_digits: true, forbid_dates: true, forbid_urls: true, forbid_codes: true, max_length: 200, language: "es" } }); if (response.guardrails_violated) { throw new Error("AI_GUARDRAIL_VIOLATION"); } return { slots: response.slots, aiUsed: true, callId: response.call_id }; } catch (error) { // Fallback determinístico return { slots: params.fallback, aiUsed: false }; } }
Notificación enviada no significa notificación útil. Estas métricas validan si FARO está cumpliendo el principio "notificar menos, pero mejor" o si está derivando a spam ejecutivo.
| Evento | Dónde se registra | Columna / tabla |
|---|---|---|
| Notification created | Insert principal | notifications.created_at |
| Notification queued | Dispatcher captura para envío | notifications.queued_at |
| Notification sent | Provider devolvió 2xx (email) o markeo (in-app) | notifications.sent_at |
| Notification delivered | Webhook provider confirma entrega | notifications.delivered_at |
| Notification read | Usuario abre o marca como leída | notifications.read_at |
| Notification failed | Provider devolvió error o timeout | notifications.failed_at + failure_reason |
| Notification suppressed | Dedupe activado | notifications.status = 'suppressed' |
| Provider response | Respuesta cruda del provider de email | notifications.provider_response jsonb |
| Critical alert generated | Audit log paralelo (opcional) | audit_log · entry adicional |
| AI usage | Cuando IA llenó slots | notifications.ai_used / ai_model / ai_call_id |
Alarma operativa. Si la tasa de lectura cae por debajo de 50% en ventana de 14 días, o si el volumen por usuario supera 30 notificaciones semana, el sistema está produciendo ruido. Revisar templates, dedupe y prioridades antes de seguir agregando eventos al catálogo.
El sistema de alertas no vive solo. Consume catálogos canónicos, es disparado por workflow, procesado por jobs, validado por matriz RACI y orquestado por IA gateway.
FARO-WF-001 produce los eventos (escalation_created, action_expired, evidence_rejected) que generan notificaciones. En construcción.
FARO-AI-001 expone el endpoint slot_fill con guardrails que esta pieza consume para llenar los 3 slots IA controlados. En construcción.
FARO-UI-001 incluye la campana de notificaciones y el panel lateral. Recibe las notificaciones in-app vía API GET /api/v1/notifications. En construcción.
FARO-UI-002 muestra resumen ejecutivo del responsable, incluyendo notificaciones pendientes agrupadas. En construcción.
Las 10 frecuencias de sincronización determinan cuándo el motor evaluador re-evalúa tensiones y, por extensión, cuándo se generan nuevas notificaciones.
FARO-OPS-001 orquesta los 5 jobs cron (notification-dispatcher, due-soon-checker, digest-daily, evidence-review-checker, critical-alert-checker). En construcción.
30 tensiones canónicas. Las notificaciones de tensión consumen tension_code y enriquecen el payload con metadatos del catálogo.
Catálogo canónico de acciones. Los action_code y closure_criteria alimentan los templates de acción asignada/vencida/cerrada.
Catálogo canónico de evidencias. Los evidence_code y evidence_required alimentan los templates de revisión y rechazo.
La resolución de destinatarios por rol (resolveNotificationRecipients) consulta user_roles alimentada por la matriz RACI canónica.
DDL consolidado del sistema FARO Connect. Incluye notifications, notification_templates y notification_preferences.
Row Level Security por company_id. Una notificación nunca debe revelar datos de otra empresa; las políticas RLS lo garantizan a nivel SQL.
El esquema, los 12 templates seed, los 5 jobs y la política IA están definidos. Falta cerrar FARO-WF-001 (workflow de escalamiento) y FARO-AI-001 (ai-gateway con guardrails) para activar el sistema end-to-end. Pasá al hub para ver el resto del pack o revisá los catálogos canónicos que alimentan estos templates.
→ Volver al hub modelos NDA