# tcef-notifications Microservicio de **envío de notificaciones** para *Todos contra el Fuego* (alertas tempranas de incendios NASA FIRMS). Sustituye el envío de push que hacía la web Meteor con la API legacy de GCM (apagada por Google en jun-2024) y saca el envío del observer de Meteor a una cola controlada. Fases **1a** (workers de envío) y **1b** (matching geoespacial) del [plan de modernización](../plan-modernizacion/README.md). La 1a consume la colección `notifications`; la 1b la **genera** (fuego → subs 2dsphere), sustituyendo el matching O(fuegos×subs) de node-red. Telegram (1c) viene después. ## Qué hace - **poller**: cada N s busca docs pendientes en `notifications` (con los nombres de campo **correctos** — el cron viejo tenía un typo, ver [`docs/legacy-behavior.md`](docs/legacy-behavior.md) §2) y encola un job por canal. - **fcm-worker**: envía push con `firebase-admin` (FCM HTTP v1), preservando el payload que espera la app Flutter (`FLUTTER_NOTIFICATION_CLICK`, collapseKey = `_id`, data). Purga tokens muertos sin reintentar. - **email-worker**: `nodemailer` + las plantillas `new-fire` portadas verbatim. - **idempotencia persistente**: colección `notification_sends`, índice único `(notificationId, channel)` — sobrevive reinicios y replays de cola. ## Salvaguardas anti-spam El riesgo nº1 es spamear a los usuarios. Por eso: - **Modos graduales** `MODE`: `dry-run` → `shadow` → `canary` → `full`. Nunca se salta un modo; `full` solo con confirmación explícita del usuario. - **Exclusión mutua por canal**: `OWNED_CHANNELS` aquí + `notifDisabledChannels` en Meteor. El viejo y el nuevo jamás envían por el mismo canal a la vez. - **Kill switch**: `KILL_SWITCH=1` detiene todo envío al instante. - **Límites**: máx envíos/usuario/día y circuit breaker por batch (para y alerta). Detalle del despliegue y la secuencia de cutover: [`docs/rollout.md`](docs/rollout.md). ## Desarrollo ```bash npm ci npm test # 47 tests (vitest + fake-repo en memoria, sin infra externa) npm run typecheck npm run build # -> dist/ (+ plantillas) ``` > **Nota MongoDB**: producción corre **MongoDB 3.2** (EOL), así que el servicio > usa el driver `mongodb` **v3.7** (el moderno no conecta a 3.2) y no hay change > streams (fase 1b usará polling). Los tests van contra un fake en memoria > porque no hay mongod tan antiguo ejecutable en hosts modernos. Detalle en > [`docs/legacy-behavior.md`](docs/legacy-behavior.md) §0. ## Ejecución Config por env (ver [`.env.example`](.env.example)) o `config.json` (ver `config.example.json`); env gana. Secretos (Mongo, `MAIL_URL`, service account de Firebase) desde el repo privado `tcef-private-config` — nunca en este repo. ```bash MODE=dry-run OWNED_CHANNELS=fcm MONGO_URL=... npm start ``` Despliegue en shiva: `deploy/tcef-notifications.service` (systemd) o `deploy/pm2-tcef-notifications.json`. Node 22 standalone (no el Node 8 del sistema) + Redis local. En fase 3 pasa a Docker Compose. ## Estado - [x] **1a**: workers FCM v1 + email, poller, idempotencia, salvaguardas, tests. - [x] **1a**: flag de cutover en Meteor (`notifDisabledChannels`). - [x] **1a**: canary FCM verificado (push real entregada a dispositivos propios). - [x] **1b**: matcher geoespacial (content i18n, geolib, `sealed` vía Iron — compatible con la web —, dedupe 500 m), ingesta por polling, modos `shadow`/`full`, comparador de shadow. Tests en verde. - [x] **1c**: telegram-worker (texto + ubicación, firelink, rate limiting global 25/s + 1/s por chat, 429/bloqueo, idempotencia). Tests en verde. **Código migrado; sin tokens configurados → inerte.** - [ ] **1a full**: envío masivo FCM (~8176 users) — con el usuario, vigilando. - [ ] **1b**: rollout `shadow` ≥3 días → comparar → cutover node-red → `full`. - [ ] **1c**: extraer tokens de bot → rollout `shadow`/canary → cutover node-red.