Automaciones
Sistema de notificaciones event-driven. Cualquier acción de negocio (crear reserva, completar encuesta, etc.) puede emitir un evento de dominio; los flujos configurados en el backoffice escuchan esos eventos y disparan acciones (push, in_app, tools_inbox, email, chat). Ruta del backoffice:/app/notifications/automations
Modelo conceptual
trigger → (condition opcional) → action. No hay step de “espera” — para disparos no-inmediatos (3 días antes, 24h después, etc.), se usan eventos derivados emitidos por crons diarios.
Step builder
El builder visual usa drag-and-drop sobre un canvas (xyflow). Tres tipos de step:
Al guardar, el grafo se valida con
validateDAG (un solo trigger, action es leaf node, sin ciclos).
Operadores de condición
Definidos enpackages/common/src/types/automation-flow/conditions.ts y evaluados en
apps/backend/src/notifications/engine/evaluate-rule.ts.
present / absent tratan como ausente undefined, null y el string vacío o de solo espacios. El
0 y el "0" cuentan como presentes.
Un operador no reconocido tampoco falla al guardar: el switch de evaluate-rule.ts cae en default
y devuelve false, con el mismo resultado de rama muerta. Escríbelos siempre desde el desplegable del
builder.
Catálogo de eventos
Lista vivamantenida (también disponible víaGET /public/events/registered-events y en el catálogo informativo de packages/common).
Emitidos por vivla-backend (HTTP POST)
Onboarding —user.welcomed, user.first_login
Reservas — booking.created, booking.info_updated, booking.cancelled, booking.guest_invited, booking.rent_requested, booking.rent_approved (🔴 crítico), booking.rent_created_by_admin, booking.exchange_executed (bidireccional), booking.liberated, booking.third_home_published, booking.third_home_exchanged
Llaves — keys.recovered, keys.expiring_soon, keys.extended, keys.fidelity_bonus_credited (los últimos tres desde crons internos del backend)
Emitidos internamente por vivla-tools
Encuestas —survey.completed (ramifica por lowestNormalized, la nota más baja normalizada a /10: absorbe los antiguos nps_high/nps_low)
Tickets — ticket.created, ticket.updated (discriminador field + previousValue/newValue), ticket.assigned, ticket.resolved (transición de status a resuelto/cerrado — identidad propia, sustituye a ticket.updated para esa transición), ticket.comment_added. Emitidos desde el módulo de tickets (mutaciones del panel + webhook de Zendesk). Los targets creatorId/assigneeId/actorId son users.id de staff interno, direccionables sin mapeos. Ver Avisos de tickets.
Emitidos por crons de Windmill
Oportunidades —booking.stays_available_reminder (mensual)
Lifecycle de usuario — user.inactive (60d sin uso de llaves)
Eventos descartados en la revisión con CX (no emitir, el ingest los rechaza):Eventos derivados (reemplazan los antiguos patterns delay+condition):booking.listed_for_exchange,booking.exchange_requested,booking.exchange_matched,booking.dates_changed,booking.rent_rejected,booking.rent_opportunity,user.payment_not_setup,survey.nps_high,survey.nps_low. Detalle en Events API → Eventos descartados.
T14b — mensajes conversacionales al canal de reserva (no tarjeta):
booking.followup_7d_due
y booking.arrival_greeting_due alimentan flows cuya acción chat lleva text_slug en vez de
título/mensaje — el motor manda el texto de quick_replies (slug seguimiento-reserva /
buen-viaje) interpolado, firmando Vivla, en vez de construir una tarjeta. Candidatos y
exclusiones (cancelada, archivada, @vivla., propiedad inactiva) los resuelve
GET /surveys/reports/survey-candidates?kind=followup|buen_viaje. Flows sembrados
is_active=false.
API pública para integradores
POST /public/events/ingest — el único path por el que cualquier sistema externo (incluyendo vivla-backend) introduce eventos.
Convenciones de la API v1: response envelope { object, id, type, status, created_at }, idempotencia vía header Idempotency-Key, errores estructurados con error.code, headers de rate limit y X-Request-ID. Detalle exhaustivo en Events API.
Request resumido
Nota v3.1: el body fieldevent_idfue eliminado. La idempotencia se hace siempre vía headerIdempotency-Key. Internamente se hashea de forma determinística al campo UNIQUEdomain_events.event_id. Detalle en Idempotencia.
Response shapes
Garantías del servidor
- Idempotencia vía hash determinístico de
Idempotency-Key→domain_events.event_idUNIQUE. - Validación estricta por
event_type— Zod schemas enapps/backend/src/notifications/engine/schemas/event-schemas.ts. - Whitelist de tipos — sólo se aceptan los registrados.
- Rate limiting: 200 req/min con headers
X-RateLimit-Limit/Remaining/ResetyRetry-Afteren 429. - Observabilidad — logs estructurados con
request_id+ métrica PostHogevent.ingested. - Health check —
GET /public/events/health.
Listar eventos soportados
Endpoints CRUD del builder
Aparte del endpoint público de ingest, el backoffice usa los siguientes (auth Auth0 + RolesGuard, envelope v1):Las rutas legacy/api/notifications/automations/*siguen vivas pero están marcadas como deprecadas — el frontend ya consume/v1. Ver Convenciones API para el envelope.
Endpoints de operaciones (admin)
Inspección del pipeline de eventos. Todos requieren rol admin.Mantenimiento
Endpointsx-api-key-protegidos para crons de Windmill (no llamar manualmente):
Frequency warnings
SendingService corre un check soft antes de cada push: cuenta los pushes a ese user en últimas 24h y 7d. Si excede 1/día o 3/semana, NO bloquea el envío pero:
- Loggea warning con
notification_id,user_id, conteos. - Emite evento PostHog
notification.frequency_warningconcap_type(daily/weekly/both).
Modelo de datos
automation_flows (vivla-tools Postgres):
domain_events (vivla-tools Postgres):
Pipeline de procesamiento
v3.0: ya no existe el
delay step ni la tabla scheduled_notifications. Los disparos no-inmediatos se modelan como eventos derivados emitidos por crons diarios.Avisos de tickets (creador + asignado)
El creador y el asignado de un ticket reciben avisos por buzón interno de Tools (tools_inbox) + email de todo lo que le pasa a “su” ticket. Sembrados por las migraciones 171_seed_ticket_notification_flows.sql (created/updated/assigned/comment_added, plantillas + flows ACTIVOS al desplegar; UUIDs con segmento 2081) y 178_seed_ticket_resolved_notification_flow.sql (resolved).
El link de cada plantilla apunta al detalle en Tools:
https://tools.vivla.com/app/tickets/list/{{ticketId}}.
Metadata enriquecida para el buzón
Para los eventosticket.*, el motor añade además un bloque metadata.ticket (ticketId, title, status, priority, ticketKind, propertyName, assigneeName) a la fila de inbox_messages — aditivo, no toca el metadata de otros flows — para que el sidebar pinte una card rica (TicketMessageCard) sin volver a pedir el ticket. Ver Inbox → Tickets via Automation Engine.
Anti-ruido: skipIfTargetEquals
ActionConfig.skipIfTargetEquals (opcional) es una lista de paths del payload: si el destinatario resuelto (target) coincide con el valor de alguno de esos campos, el motor salta la acción entera (engine.service.ts → matchedSkipField). Genérico y declarativo; se evalúa antes de cualquier canal y se ignora en test-mode (targetOverride).
Los flows de tickets lo usan para dos cosas:
- No avisarte de tu propio cambio — todas las acciones excluyen
actorId(quién hizo el cambio;null= sistema/Zendesk, nunca suprime). - Deduplicar creador==asignado — la acción del creador excluye además
assigneeId, así cuando son la misma persona solo se envía la copia del asignado (una notificación, no dos).
null/vacíos del payload nunca matchean.
Suppressions (opt-out)
Ronda D agregó la tablanotification_suppressions (migración 089). El motor consulta PreferencesService.findActiveSuppression() antes de cada send (push/email/in_app/tools_inbox/chat) y skipea con un log si hay match — la flow se sigue ejecutando, simplemente este canal no dispara para ese destinatario.
Reglas de match (en orden):
scope='all'con el canal correspondiente bloquea todo el canal para ese user.scope='category'matchea cuandoscope_value === template.category.scope='template'matchea cuandoscope_value === template_id.
source='list_unsubscribe'), también la UI de preferencias del usuario o inserciones manuales de support.
Inspección desde SQL:
Referencias para integradores
- Backend handoff completo (para vivla-backend dev team):
docs/internal/tools/notifications/implementation/epics/notification-automation-system/specs/backend-handoff.md - PRD técnico v3.0:
docs/internal/tools/notifications/implementation/epics/notification-automation-system/02-prd-technical.md - Catálogo de eventos:
docs/internal/tools/notifications/implementation/epics/notification-automation-system/03-events-catalog.md - Schemas Zod:
apps/backend/src/notifications/engine/schemas/event-schemas.ts