tcef-notifications/docs/rollout.md
vjrj d20e168c90 Fase 1a: microservicio tcef-notifications (workers FCM v1 + email)
Sustituye el envío de push por la API legacy de GCM (muerta desde jun-2024) y
saca el envío del observer de Meteor a una cola BullMQ controlada.

- poller: consume 'notifications' pendientes (campos correctos; el cron viejo
  tenía un typo nofitied/emailNofitied que nunca casaba)
- fcm-worker: firebase-admin (FCM HTTP v1), payload compatible con la app
  Flutter (FLUTTER_NOTIFICATION_CLICK, collapseKey=_id, data); purga tokens
  muertos sin reintentar
- email-worker: nodemailer + plantillas new-fire portadas verbatim
- idempotencia persistente: notification_sends, indice unico (notificationId,
  channel) -> sobrevive reinicios y replays
- salvaguardas anti-spam: modos dry-run/shadow/canary/full, exclusion mutua por
  canal (OWNED_CHANNELS + notifDisabledChannels en Meteor), kill switch,
  limites por usuario/dia y circuit breaker por batch
- docs/legacy-behavior.md (caracterizacion) + docs/rollout.md
- 47 tests (vitest + mongodb-memory-server), typecheck y build OK
- deploy: systemd + pm2 (Node 22 standalone)

Falta el service account de Firebase para envio real de push (pedir al usuario).
2026-07-13 00:10:29 +02:00

5.6 KiB

Plan de puesta en marcha (rollout) — fase 1a

Regla de oro: nunca se salta un modo, y ningún canal pasa a full sin confirmación explícita del usuario. El riesgo nº1 es spamear a los usuarios.

Modos (env MODE)

Modo Envía Para qué
dry-run no calcula el payload, resuelve destinatario, loguea. Detecta errores de datos/plantillas sin tocar a nadie. No reserva en notification_sends.
shadow no igual que dry-run pero pensado para correr ≥1 día en paralelo con el sistema viejo y comparar decisiones (logs estructurados).
canary solo cohorte envía únicamente a CANARY_USER_IDS / CANARY_SUBS_IDS (dispositivos/suscripciones propias).
full todos operación normal.

Salvaguardas activas en todos los modos: KILL_SWITCH=1 (parada total), límite LIMIT_PER_USER_DAY, circuit breaker LIMIT_MAX_PER_BATCH, idempotencia persistente notification_sends (único por notificationId+channel).

Exclusión mutua por canal (obligatoria)

El sistema viejo (Meteor) y el nuevo jamás deben enviar por el mismo canal a la vez. Se controla con:

  • Meteor (todos-contra-el-fuego-web): settings.private.notifDisabledChannels, un array con "mobile" y/o "web". Si un canal está ahí, el observer/cron viejos lo ignoran (guard en notificationsProcess.js). Cambio reversible sin desplegar código: editar settings y reiniciar el proceso Meteor.
  • Servicio nuevo (OWNED_CHANNELS): fcm y/o email. Solo procesa los que posee.

Mapa de canales: mobile ↔ fcm, web ↔ email.

Orden seguro de cutover de un canal: (1) añadir el canal a notifDisabledChannels en Meteor y reiniciar → el viejo deja de enviarlo; (2) confirmar en logs que el viejo ya no lo toca; (3) añadir el canal a OWNED_CHANNELS del servicio y subir el modo. Nunca al revés.

Secuencia (por canal, empezando por FCM)

FCM es el primer cutover y el más seguro: el canal FCM del viejo está muerto desde jun-2024 (API legacy GCM apagada → 404), así que no hay envío real que solapar. Ver plan-modernizacion/ESTADO.md.

Paso 0 — Infra en shiva (una vez)

  1. Node 22 standalone (nvm o binario), no el Node 8 del sistema.
  2. Redis local (apt install redis-server o binario/contenedor).
  3. Clonar el repo, npm ci && npm run build.
  4. Config desde el repo privado tcef-private-config:
    • MONGO_URL (rsmain, db fuegos), MAIL_URL, GMAPS_KEY, FIRE_ICON_URL, ROOT_URL.
    • Service account de Firebase (FCM_SERVICE_ACCOUNT=/ruta/al.json) — descargar de la consola Firebase del proyecto org.comunes.fires (angular-cosmos-108908) → Configuración → Cuentas de servicio → Generar clave privada. Guardar solo en el repo privado / disco de shiva.

Paso 1 — dry-run FCM (unos ciclos)

MODE=dry-run OWNED_CHANNELS=fcm KILL_SWITCH=0
  • Corre varios imports NASA (cada 15 min). Verifica en logs: payloads bien formados, cuántos users sin token, cuántos con token. No se envía nada.
  • Criterio de avance: sin errores de datos/plantilla; volumen esperado.

Paso 2 — shadow FCM (≥1 día)

MODE=shadow OWNED_CHANNELS=fcm
  • Compara decisiones con el sistema viejo (que en FCM no envía nada real igual).
  • Criterio de avance: sin discrepancias inesperadas; sin duplicados en notification_sends.

Paso 3 — cutover + canary FCM (dispositivos propios)

  1. En Meteor: notifDisabledChannels: ["mobile"], reiniciar tcef_web*.
  2. Verificar que el viejo ya no procesa mobile.
  3. Servicio:
    MODE=canary OWNED_CHANNELS=fcm CANARY_USER_IDS=<tu userId> FCM_SERVICE_ACCOUNT=...
    
  4. Instala la app Flutter en un dispositivo propio con sesión iniciada, genera un fuego sintético cerca de tu suscripción → debe llegar la push (formato e idioma correctos). Verifica notification_sends (status sent) y logs de firebase-admin.
  5. Simulacro de idempotencia: mata el worker a mitad de batch y reinícialo → cero reenvíos (comprobar notification_sends).
  • Criterio de avance: push llega bien a la cohorte varios días, sin duplicados.

Paso 4 — full FCM (requiere confirmación explícita del usuario)

MODE=full OWNED_CHANNELS=fcm
  • Los fireBaseToken llevan ~2 años sin uso → alta tasa de token muerto en el primer envío. Es esperado: se purgan (fireBaseToken=null), no se reintenta.
  • Vigilar el circuit breaker y el límite por usuario/día.

Paso 5 — email (repetir la secuencia, solo tras FCM estable)

  • dry-run → shadow (aquí el viejo SÍ envía email, así que el shadow es crítico: comparar a quién marcó el viejo como emailNotified vs. a quién enviaríamos, volcar discrepancias) → cutover (notifDisabledChannels: ["mobile","web"] en Meteor) → canary (buzón de test vía MAIL_REDIRECT_TO) → full (con confirmación).

Rollback (por canal)

  1. Servicio: KILL_SWITCH=1 (parada inmediata) o quitar el canal de OWNED_CHANNELS.
  2. Meteor: quitar el canal de notifDisabledChannels, reiniciar → el viejo reasume ese canal.
  3. Nada se pierde: los docs pendientes siguen en notifications con notified/emailNotified sin marcar.

Checklist de verificación (por paso)

  • Tests en verde (npm test).
  • dry-run: payloads OK, sin errores, volumen esperado.
  • shadow ≥1 ciclo NASA real sin discrepancias ni duplicados.
  • Exclusión por canal confirmada (viejo no toca el canal migrado).
  • Canary: entrega correcta a cohorte propia, formato/idioma OK.
  • Simulacro kill-a-mitad-de-batch → cero reenvíos.
  • Confirmación explícita del usuario antes de full.