01 · Resumen ejecutivo

Notificar menos, pero mejor: cada alerta es una interrupción justificada

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.

02 · Tesis y principios anti-ruido

8 principios que protegen al usuario del email-terrorismo

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.

Principio 01

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.

Principio 02

Responder seis preguntas

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.

Principio 03

No duplicar

Mismo usuario + misma entidad + mismo evento + mismo día = una sola notificación. Salvo que suba la prioridad.

Principio 04

Agrupar cuando se pueda

Mejor "tenés 5 acciones que vencen esta semana" que cinco emails consecutivos. Lo segundo es terrorismo administrativo.

Principio 05

Re-notificar solo si cambia algo

Vence, se escala, se rechaza evidencia, cambia responsable, sube severidad, se bloquea o se reabre. Si no cambió nada, no hay alerta nueva.

Principio 06

Digest diario para no críticos

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.

Principio 07

Toda alerta tiene link de acción

Sin action_url no hay notificación. El usuario debe poder pasar de "leí" a "actué" en un solo click.

Principio 08

IA controlada, no redacción libre

La IA llena slots predefinidos dentro de templates auditados. Nunca redacta números. Si falla, fallback determinístico.

03 · Canales

In-app + email en MVP · WhatsApp/Slack/push preparados pero fuera

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.

In-app
MVP · Canal principal

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.

Email
MVP · Crítico + digest

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.

WhatsApp
Post-MVP · Preparado

Schema y enum channel lo soporta. Falta integración productiva con WhatsApp Business API + plantillas aprobadas. Roadmap Enterprise.

Slack / Teams
Post-MVP · Preparado

Schema lo soporta vía enum. Falta workspace bot + auth OAuth. Útil para clientes con cultura digital madura. Roadmap Enterprise.

Push mobile
Post-MVP · App móvil

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í.

04 · Tipos de notificación

8 tipos canónicos enumerados en SQL

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.

alert

Alerta operativa

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.

reminder

Recordatorio

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.

escalation

Escalamiento

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.

approval_request

Solicitud de aprobación

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.

evidence_review

Revisión de evidencia

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.

score_warning

Deterioro de Score

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.

digest

Resumen agrupado

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.

system

Mensaje técnico

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.

05 · Prioridades y SLA

4 niveles con trigger, canal y tiempo de respuesta esperado

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.

Critical SLA: 4 hs

Acción inmediata

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
High SLA: 24 hs

Atención en corto plazo

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
Medium SLA: 72 hs

Relevante, no urgente

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
Low SLA: digest

Informativo

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.

06 · Mapa eventos → destinatarios → prioridad

Tabla matriz por categoría de evento

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 · criticalGerente general + DirectorCriticalin-app + emailTPL-TENSION-CRITICAL-DETECTED
tension_assignedResponsable asignadoMediumin-appTPL-TENSION-ASSIGNED
tension_escalatedRol superior según WF-001Critical/Highin-app + emailTPL-ESCALATION-CREATED
tension_reopenedResponsable + GerenciaHighin-app + emailTPL-TENSION-REOPENED
tension_closedGerencia + DirecciónMediumin-appTPL-TENSION-CLOSED
Categoría · Acción
action_createdResponsableMediumin-appTPL-ACTION-ASSIGNED
action_assignedResponsableMediumin-app + emailTPL-ACTION-ASSIGNED
action_startedGerente (opcional)Lowin-app · digestTPL-ACTION-STARTED
action_due_soonResponsableMediumin-app + emailTPL-ACTION-DUE-SOON
action_expiredResponsable + GerenteHigh/Criticalin-app + emailTPL-ACTION-EXPIRED
action_blockedGerente de áreaHighin-app + emailTPL-ACTION-BLOCKED
action_escalatedDestinatario escalamientoCritical/Highin-app + emailTPL-ESCALATION-CREATED
action_closedGerente + DirecciónMediumin-appTPL-ACTION-CLOSED
action_reopenedResponsable + GerenteHighin-app + emailTPL-ACTION-REOPENED
Categoría · Evidencia
evidence_uploadedAprobadorMediumin-appTPL-EVIDENCE-REVIEW-REQUEST
evidence_submittedAprobadorMediumin-app + emailTPL-EVIDENCE-REVIEW-REQUEST
evidence_approvedResponsableMediumin-appTPL-EVIDENCE-APPROVED
evidence_rejectedResponsableHighin-app + emailTPL-EVIDENCE-REJECTED
evidence_needs_more_infoResponsableHighin-app + emailTPL-EVIDENCE-MORE-INFO
evidence_review_overdueAprobador + GerenteHighin-app + emailTPL-EVIDENCE-REVIEW-OVERDUE
Categoría · Score
score_penalized · criticalGerencia + DirecciónCriticalin-app + emailTPL-SCORE-WARNING
score_blockedGerenteHighin-app + emailTPL-SCORE-WARNING
score_recoveredGerenciaMediumin-appTPL-SCORE-RECOVERED
score_deteriorated_weeklyDirecciónHighin-app + emailTPL-SCORE-WARNING
score_recalculatedNo siempre · digestLowdigest

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.

07 · Reglas anti-ruido

Dedupe · agrupación temporal · digest diario

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.

Regla 7.1 Dedupe key

Cómo se calcula el dedupe_key

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.

▸ TypeScript
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
Regla 7.2 Agrupación temporal

No más de 1 alerta crítica por categoría por hora

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.

▸ SQL · Query de agrupación temporal
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
Regla 7.3 Digest diario 18:00

Resumen único de no-críticos por destinatario

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.

▸ SQL · Query digest responsable
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;
Regla 7.4 Re-notificar solo si cambia

Triggers que rompen el dedupe

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.

08 · Templates email con tono FARO

6 templates email MVP · sobrio · ejecutivo · accionable

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.

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á.

09 · DDL · notifications + templates + preferences

Tres tablas que sostienen todo el sistema

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.

9.1 · Tabla faro.notifications

▸ SQL · V055__patch_notifications_table.sql
CREATE 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;

9.2 · Tabla faro.notification_templates

▸ SQL · V054__create_notification_templates.sql
CREATE 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)
);

9.3 · Tabla faro.notification_preferences

▸ SQL · V057__create_notification_preferences.sql
CREATE 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.

9.4 · Listado de templates seed MVP

Código Evento Canales seed Priority default
TPL-TENSION-CRITICAL-DETECTEDTensión crítica detectadain-app + emailCritical
TPL-ACTION-ASSIGNEDAcción asignadain-app + emailMedium
TPL-ACTION-DUE-SOONAcción próxima a vencerin-app + emailMedium
TPL-ACTION-EXPIREDAcción vencidain-app + emailHigh
TPL-ACTION-BLOCKEDAcción bloqueadain-app + emailHigh
TPL-ESCALATION-CREATEDEscalamiento generadoin-app + emailCritical
TPL-EVIDENCE-REVIEW-REQUESTEvidencia pendiente de revisiónin-app + emailMedium
TPL-EVIDENCE-APPROVEDEvidencia aprobadain-appMedium
TPL-EVIDENCE-REJECTEDEvidencia rechazadain-app + emailHigh
TPL-ACTION-CLOSEDAcción cerradain-appMedium
TPL-SCORE-WARNINGDeterioro de Scorein-app + emailHigh
TPL-DAILY-DIGESTResumen diarioemailMedium

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.

10 · Integración con WF-001 y OPS-001

Qué evento dispara qué notificación y qué job lo procesa

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.

Flujo 10.1 · WF-001 crea escalamiento
▸ Secuencia · pseudocódigo
on 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" });
  }
Flujo 10.2 · Acción vence
▸ Secuencia
on 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
  }
Flujo 10.3 · Evidencia rechazada
▸ Secuencia
on 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 }
  });

10.4 · Jobs operados desde OPS-001

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:

JOB-NOTIF-001 · notification-dispatcher
cada 1-5 min

Procesa status = 'pending' ordenado por prioridad. Marca queued, llama provider (email) o marca sent (in-app), guarda provider_response.

JOB-NOTIF-002 · due-soon-checker
diario 08:00

Detecta acciones con due_date = CURRENT_DATE + 1 y crea notificaciones TPL-ACTION-DUE-SOON aplicando dedupe por acción.

JOB-NOTIF-003 · digest-daily
diario 18:00

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).

JOB-NOTIF-004 · evidence-review-checker
cada 2-4 hs

Detecta evidencias en status = 'in_review' hace más de 48 hs sin acción y dispara TPL-EVIDENCE-REVIEW-OVERDUE al aprobador + gerente.

JOB-NOTIF-005 · critical-alert-checker
cada 15-30 min

Re-evalúa tensiones críticas activas sin notificación reciente y dispara TPL-TENSION-CRITICAL-DETECTED o re-disparo si subió severidad.

11 · Política IA en notificaciones · D5 aplicado

IA con guardrails: slots controlados, nunca redacción libre

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.

D5 · IA con guardrails

Tres slots controlados · todo lo demás es determinístico

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.

{{subject_intro}}

Frase corta (≤ 80 caracteres) que personaliza el contexto del asunto del email. Ej.: "Atención requerida en política comercial". Nunca incluye números.

{{body_summary}}

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.

{{recommendation_text}}

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:

No incluye dígitos No incluye fechas No incluye URLs No incluye códigos canónicos Largo < 200 chars Idioma = español Sin emojis Sin signos !

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.

▸ TypeScript · invocación controlada
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 };
  }
}
12 · Auditoría de envío y métricas de efectividad

Qué se registra y cómo se mide si el sistema funciona

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.

12.1 · Eventos auditados

Evento Dónde se registra Columna / tabla
Notification createdInsert principalnotifications.created_at
Notification queuedDispatcher captura para envíonotifications.queued_at
Notification sentProvider devolvió 2xx (email) o markeo (in-app)notifications.sent_at
Notification deliveredWebhook provider confirma entreganotifications.delivered_at
Notification readUsuario abre o marca como leídanotifications.read_at
Notification failedProvider devolvió error o timeoutnotifications.failed_at + failure_reason
Notification suppressedDedupe activadonotifications.status = 'suppressed'
Provider responseRespuesta cruda del provider de emailnotifications.provider_response jsonb
Critical alert generatedAudit log paralelo (opcional)audit_log · entry adicional
AI usageCuando IA llenó slotsnotifications.ai_used / ai_model / ai_call_id

12.2 · Métricas de efectividad

Métrica 1

Tasa de lectura

read_count / sent_count
(ventana 7 días)
Target: ≥ 75%
Métrica 2

Time-to-read

avg(read_at - sent_at)
por prioridad
Critical < 4 hs · High < 24 hs
Métrica 3

Tasa de click-to-action

clicks_to_action_url / read_count
Target: ≥ 40%
Métrica 4

Tasa de supresión

suppressed / (sent + suppressed)
Healthy: 5-15%
Métrica 5

Tasa de fallo provider

failed_count / (sent + failed)
Target: < 2%
Métrica 6

Volumen por usuario

notif_count / user / week
Healthy: 5-15 · Alarma: > 30
Métrica 7

IA usage rate

ai_used = true / total
Tracking · sin target
Métrica 8

IA fallback rate

fallback / ai_attempted
Healthy: < 5%

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.

13 · Cross-references

Dónde se cruza este sistema con el resto del pack

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.

DISPARA PENDIENTE
workflow-escalamiento-mvp.html

FARO-WF-001 produce los eventos (escalation_created, action_expired, evidence_rejected) que generan notificaciones. En construcción.

CONTROLA PENDIENTE
ai-gateway-mvp.html

FARO-AI-001 expone el endpoint slot_fill con guardrails que esta pieza consume para llenar los 3 slots IA controlados. En construcción.

CONSUME PENDIENTE
ui-bandeja-tensiones.html

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.

CONSUME PENDIENTE
ui-dashboard-responsable.html

FARO-UI-002 muestra resumen ejecutivo del responsable, incluyendo notificaciones pendientes agrupadas. En construcción.

REFERENCIA
frecuencia-sincronizacion.html

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.

OPERA PENDIENTE
observabilidad-ops-mvp.html

FARO-OPS-001 orquesta los 5 jobs cron (notification-dispatcher, due-soon-checker, digest-daily, evidence-review-checker, critical-alert-checker). En construcción.

CONSUME
catalogo-tensiones-mvp.html

30 tensiones canónicas. Las notificaciones de tensión consumen tension_code y enriquecen el payload con metadatos del catálogo.

CONSUME
catalogo-acciones-mvp.html

Catálogo canónico de acciones. Los action_code y closure_criteria alimentan los templates de acción asignada/vencida/cerrada.

CONSUME
catalogo-evidencias-mvp.html

Catálogo canónico de evidencias. Los evidence_code y evidence_required alimentan los templates de revisión y rechazo.

RESUELVE
matriz-raci-105.html

La resolución de destinatarios por rol (resolveNotificationRecipients) consulta user_roles alimentada por la matriz RACI canónica.

IMPLEMENTA
modelo-sql.html

DDL consolidado del sistema FARO Connect. Incluye notifications, notification_templates y notification_preferences.

SEGURIDAD
seguridad-rls-mvp.html

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.