|  |  | # Plan de Onboarding de Usuarios y Resolución de Alias (JID opaco → número)
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Resumen y diagnóstico (basado en el código actual)
 | 
						
						
						
							|  |  | - Fuentes automáticas actuales de aprendizaje de alias (sin intervención de usuarios):
 | 
						
						
						
							|  |  |   1) Mensajes en grupos: en src/server.ts, si participantAlt y participant difieren, se hace IdentityService.upsertAlias(participant, participantAlt, 'message.key'), mapeando un @lid “opaco” al JID real @s.whatsapp.net.
 | 
						
						
						
							|  |  |   2) Sincronización de miembros: en src/services/group-sync.ts, si el payload trae p.id y p.jid, se hace upsertAlias(id, jid, 'group.participants') y además se asegura el usuario con ensureUserExists.
 | 
						
						
						
							|  |  |   3) Actualizaciones de contactos/chats: en src/services/contacts.ts, si se recibe objeto con id=@lid y jid=@s.whatsapp.net, se hace upsertAlias(alias, jid, 'contacts.update').
 | 
						
						
						
							|  |  | - Alta de usuarios sin DM: src/services/group-sync.ts::reconcileGroupMembers llama ensureUserExists(userId) para cada miembro activo; si tenemos número, el usuario “existe” aunque no haya enviado DM.
 | 
						
						
						
							|  |  | - Normalización robusta: utils/whatsapp.normalizeWhatsAppId elimina dominio y sufijo “:xx”, quedando el número limpio si venía en el JID.
 | 
						
						
						
							|  |  | - Problema detectado: en src/services/command.ts, al parsear menciones, si una mención llega como @lid sin alias aún, se descarta (aunque a veces podría venir como JID real). Resultado: “se puede crear tarea pero no asignar responsables” en algunos casos, especialmente con usuarios “silenciosos” (no escriben en grupo) cuando la API no aporta número en ninguno de los eventos.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Objetivo
 | 
						
						
						
							|  |  | - Minimizar fricción y tiempo hasta poder asignar y autenticar: que los usuarios queden asignables con la mínima acción (idealmente sin DM).
 | 
						
						
						
							|  |  | - Evitar spam y códigos por-usuario. Publicar, solo si hace falta, un único mensaje por grupo con enlace wa.me.
 | 
						
						
						
							|  |  | - Cubrir casos de usuarios “silenciosos” (no escriben en grupo) garantizando una vía de mapeo fiable (DM “hola”).
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Estrategia general
 | 
						
						
						
							|  |  | - Exprimir al máximo el aprendizaje automático que ya tenemos (participants y contacts).
 | 
						
						
						
							|  |  | - Ajustar CommandService para no descartar menciones válidas con números reales.
 | 
						
						
						
							|  |  | - Medir cobertura y publicar un único mensaje por grupo con wa.me únicamente si faltan usuarios por resolver; con cooldown.
 | 
						
						
						
							|  |  | - DM “activar” como último recurso confiable para cerrar huecos de usuarios silenciosos; considerar “código por grupo” solo como fallback extremo si una instancia no correlaciona jamás sin token.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fases
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase A0 — Verificación y observabilidad (rápida, sin UX visible)
 | 
						
						
						
							|  |  | - Estado: completada (deploy tras commit d25efb0).
 | 
						
						
						
							|  |  | - Métricas nuevas:
 | 
						
						
						
							|  |  |   - alias_coverage_ratio{group_id}: gauge con el porcentaje aproximado de miembros activos con número resoluble (o alias).
 | 
						
						
						
							|  |  |   - onboarding_prompts_sent_total / onboarding_prompts_skipped_total: counters para controlar ruido.
 | 
						
						
						
							|  |  |   - onboarding_assign_failures_total: counter de menciones no resolubles al crear tareas.
 | 
						
						
						
							|  |  | - Logs de desarrollo (NODE_ENV=development): en src/server.ts, comparar participant vs participantAlt (normalizados) y mentionedJid normalizados para comprobar la frecuencia de correlación automática en tu instancia Evolution.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase A1 — Aprendizaje “agresivo” al entrar en grupos (Completada)
 | 
						
						
						
							|  |  | - Al recibir groups.upsert (src/server.ts):
 | 
						
						
						
							|  |  |   - syncGroups → refreshActiveGroupsCache → syncMembersForActiveGroups (ya implementado).
 | 
						
						
						
							|  |  |   - Efectos:
 | 
						
						
						
							|  |  |     - Crea usuarios con ensureUserExists(userId).
 | 
						
						
						
							|  |  |     - Si el payload incluye id + jid, rellena alias automáticamente.
 | 
						
						
						
							|  |  | - Mantener activo ContactsService.updateFromWebhook para capturar correlaciones adicionales en los primeros minutos.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase A2 — Ajuste clave sin fricción: conservar menciones con números (Completada)
 | 
						
						
						
							|  |  | - Estado: completada (deploy tras commit 8b1af56).
 | 
						
						
						
							|  |  | - En src/services/command.ts, al construir los candidatos a assignees:
 | 
						
						
						
							|  |  |   - Si normalizeWhatsAppId(token) produce dígitos y resolveAliasOrNull devuelve null, CONSERVAR ese número (no descartarlo).
 | 
						
						
						
							|  |  |   - Asegurar ensureUserExists para esos IDs conservados antes de usarlos.
 | 
						
						
						
							|  |  |   - Filtrar CHATBOT_PHONE_NUMBER para evitar autoasignaciones al bot.
 | 
						
						
						
							|  |  | - Métrica: se incrementa onboarding_assign_failures_total con labels {group_id, source, reason} cuando una mención/token no es resoluble ni plausible.
 | 
						
						
						
							|  |  | - Configuración: ONBOARDING_FALLBACK_MIN_DIGITS (por defecto 8) controla la longitud mínima para considerar un número “plausible”.
 | 
						
						
						
							|  |  | - Efecto: reduce drásticamente los fallos de asignación por mención sin necesidad de DM.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase A3 — Mensaje único por grupo con wa.me (Completada)
 | 
						
						
						
							|  |  | - Condición de publicación:
 | 
						
						
						
							|  |  |   - Tras A1 y un breve grace period (≈1–2 min) para permitir contacts/chats updates, calcular alias_coverage_ratio{group_id}.
 | 
						
						
						
							|  |  |   - Si cobertura = 100% → NO publicar.
 | 
						
						
						
							|  |  |   - Si cobertura < 100% → publicar UNA vez un mensaje por grupo con el texto:
 | 
						
						
						
							|  |  |     - “Para poder asignarte tareas y acceder a la web, envía ‘activar’ al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/<NUM_BOT>”
 | 
						
						
						
							|  |  | - Control de ruido:
 | 
						
						
						
							|  |  |   - Persistir timestamp de último envío por grupo (cooldown, p. ej., 7 días) y re-publicar solo si entran nuevos miembros sin resolver tras el cooldown.
 | 
						
						
						
							|  |  | - Fallback extremo (probablemente no necesario con tu stack):
 | 
						
						
						
							|  |  |   - Si en una instancia concreta el DM “hola” no basta para correlacionar, usar “código por grupo” como texto pre-rellenado en wa.me: “alta XYZ123” (único por grupo, nunca por-usuario), almacenado con caducidad. Aplicarlo solo si la métrica demuestra que el caso existe.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase A4 — Asistentes “just-in-time” y UX mínima (Completada)
 | 
						
						
						
							|  |  | - Si una asignación falla por mención no resoluble:
 | 
						
						
						
							|  |  |   - Enviar DM al asignador (ResponseQueue) con: “No puedo asignar a X aún. Pídele que toque este enlace y diga ‘activar’: https://wa.me/<NUM_BOT>”.
 | 
						
						
						
							|  |  | - Primer DM “activar” de un usuario:
 | 
						
						
						
							|  |  |   - Asegurar ensureUserExists y responder con: “Listo, ya puedes reclamar/ser responsable en: …”.
 | 
						
						
						
							|  |  | - Opcional web: si el usuario llega sin estar identificado, mostrar banner con botón a wa.me “activar”.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase A5 — Optimizaciones post-A1 (pendiente)
 | 
						
						
						
							|  |  | - Optimizar encadenado tras groups.upsert para sincronizar solo los grupos afectados cuando el payload lo permita.
 | 
						
						
						
							|  |  | - Añadir debounce/backoff por grupo (2–5 s) para coalescer ráfagas de upserts en corto intervalo.
 | 
						
						
						
							|  |  | - Añadir tests: uno que valide el filtrado a “grupos afectados” y otro que verifique que el debounce evita ejecuciones duplicadas dentro de la ventana.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Fase Final — Pruebas E2E
 | 
						
						
						
							|  |  | - Objetivo: validar end-to-end en un entorno de staging con Evolution API que los prompts A3 funcionan sin efectos secundarios.
 | 
						
						
						
							|  |  | - Casos a verificar:
 | 
						
						
						
							|  |  |   - Envío al grupo (@g.us) mediante sendText: que el backend acepte recipient con @g.us y el mensaje se entregue.
 | 
						
						
						
							|  |  |   - Publicación condicional: cobertura < 100% tras el grace → se envía; cobertura = 100% → se omite; cooldown activo → se omite.
 | 
						
						
						
							|  |  |   - Gating: en modo enforce, grupos no allowed → se omite.
 | 
						
						
						
							|  |  |   - Configuración: sin CHATBOT_PHONE_NUMBER o ONBOARDING_PROMPTS_ENABLED=false → se omite.
 | 
						
						
						
							|  |  |   - Métricas: alias_coverage_ratio, onboarding_prompts_sent_total y onboarding_prompts_skipped_total con su reason se actualizan.
 | 
						
						
						
							|  |  | - Preparación recomendada:
 | 
						
						
						
							|  |  |   - Instancia Evolution apuntando a un grupo de pruebas; CHATBOT_PHONE_NUMBER configurado; METRICS_ENABLED=true.
 | 
						
						
						
							|  |  |   - Reducir ONBOARDING_GRACE_SECONDS y ONBOARDING_COOLDOWN_DAYS para acelerar validación.
 | 
						
						
						
							|  |  |   - Confirmar que ResponseQueue.process está activo y que los workers pueden enviar.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Criterios de aceptación
 | 
						
						
						
							|  |  | - p95 del tiempo desde que un usuario toca el enlace a quedar asignable < 1 minuto.
 | 
						
						
						
							|  |  | - En la mayoría de grupos no se publica ningún mensaje (cobertura ≈100% tras primer sync + contacts).
 | 
						
						
						
							|  |  | - Caída significativa de onboarding_assign_failures_total respecto al baseline.
 | 
						
						
						
							|  |  | - Sin spam: un único mensaje por grupo y cooldown aplicado.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Archivos a ver/editar/crear
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | A) Núcleo bot
 | 
						
						
						
							|  |  | - src/services/command.ts (EDITAR)
 | 
						
						
						
							|  |  |   - Puntos: construcción de mentionsNormalizedFromContext y normalizedFromAtTokens.
 | 
						
						
						
							|  |  |   - Cambio: fallback a número normalizado cuando resolveAliasOrNull no resuelva; ensureUserExists; filtrar CHATBOT_PHONE_NUMBER; incrementar onboarding_assign_failures_total cuando una mención no sea resoluble en absoluto.
 | 
						
						
						
							|  |  | - src/services/group-sync.ts (EDITAR)
 | 
						
						
						
							|  |  |   - Tras reconcileGroupMembers, computar cobertura aproximada (miembros activos con número conocido / miembros activos totales).
 | 
						
						
						
							|  |  |   - Exponer alias_coverage_ratio{group_id} vía Metrics.set. Si coverage < 100% y cooldown vencido → disparar encolado de un mensaje único por grupo (ResponseQueue).
 | 
						
						
						
							|  |  |   - Método util: getActiveGroupIdsForUser, isSnapshotFresh, etc., ya existen; añadir tracking de onboarding_prompted_at (ver persistencia).
 | 
						
						
						
							|  |  | - src/server.ts (EDITAR mínimo)
 | 
						
						
						
							|  |  |   - Logs de desarrollo comparando participant vs participantAlt y mentionedJid normalizados para verificar correlación automática.
 | 
						
						
						
							|  |  |   - Opcional: hook tras groups.upsert para forzar el chequeo de cobertura post-sync (o dejarlo en scheduler de miembros).
 | 
						
						
						
							|  |  | - src/services/contacts.ts y src/services/identity.ts (VER)
 | 
						
						
						
							|  |  |   - Ya aportan alias automáticamente; no requieren cambios inmediatos.
 | 
						
						
						
							|  |  | - src/services/response-queue.ts (VER/EDITAR si hiciera falta)
 | 
						
						
						
							|  |  |   - Asegurar que puedes encolar mensajes hacia group_id@g.us sin cambios (parece OK con sendText si el backend lo admite). Añadir, si quieres, etiquetas/metadata para identificar “onboarding”.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | B) Persistencia
 | 
						
						
						
							|  |  | - Opción simple recomendada: columna en groups
 | 
						
						
						
							|  |  |   - Nueva columna: groups.onboarding_prompted_at TEXT NULL.
 | 
						
						
						
							|  |  |   - Lógica: publicar si coverage < 100% y (onboarding_prompted_at IS NULL o han pasado ≥ X días).
 | 
						
						
						
							|  |  | - Alternativa: tabla dedicada group_onboarding (group_id PK, last_prompt_at, last_coverage, last_pending_count, last_sent_by).
 | 
						
						
						
							|  |  | - Archivos:
 | 
						
						
						
							|  |  |   - src/db/migrations/index.ts (EDITAR): añadir migración para onboarding_prompted_at (o tabla nueva).
 | 
						
						
						
							|  |  |   - src/db.ts (VER): no requiere cambios, migrador ya está integrado.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | C) Mensajería y copy
 | 
						
						
						
							|  |  | - Texto base:
 | 
						
						
						
							|  |  |   - “Para poder asignarte tareas y acceder a la web, envía ‘hola’ al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/<NUM_BOT>”
 | 
						
						
						
							|  |  | - Fallback extremo (solo si es necesario, ver A3):
 | 
						
						
						
							|  |  |   - “… o envía ‘alta XYZ123’ si el enlace no funciona.”
 | 
						
						
						
							|  |  | - Dónde generarlo: en GroupSyncService (o un OnboardingService nuevo) cuando coverage < 100% y cooldown vencido; encolar con ResponseQueue para recipient = group_id@g.us.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | D) UI web (opcional a posteriori)
 | 
						
						
						
							|  |  | - Banner SSR-safe cuando App.Locals.userId no esté disponible: botón wa.me con “hola”; desaparecer al resolver.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Métricas propuestas
 | 
						
						
						
							|  |  | - alias_coverage_ratio{group_id} (gauge).
 | 
						
						
						
							|  |  | - onboarding_prompts_sent_total / onboarding_prompts_skipped_total (counters).
 | 
						
						
						
							|  |  | - onboarding_assign_failures_total (counter).
 | 
						
						
						
							|  |  | - identity_alias_upserts_total, identity_alias_resolved_total / identity_alias_unresolved_total (ya existen).
 | 
						
						
						
							|  |  | - time_to_link_p95 (opcional; aproximable por “primer avistamiento” → “primer DM/alias resuelto”).
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Caveats y buenas prácticas
 | 
						
						
						
							|  |  | - No spamear: un único mensaje por grupo + cooldown; nada si coverage=100%.
 | 
						
						
						
							|  |  | - Privacidad: DM “hola” iniciado por el usuario; no mostrar datos sensibles en grupos.
 | 
						
						
						
							|  |  | - Entornos de test: suprimir publicación y logs invasivos; respetar NODE_ENV='test'.
 | 
						
						
						
							|  |  | - Resiliencia: si Evolution deja de enviar participantAlt/p.jid, el flujo de DM cubre a los silenciosos; si los envía, el DM apenas será necesario.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Checklist de ejecución
 | 
						
						
						
							|  |  | - A0: Añadir métricas y logs de verificación (dev).
 | 
						
						
						
							|  |  | - A1: Confirmar que el sync de miembros corre al entrar; mantener contacts.update. (hecho)
 | 
						
						
						
							|  |  | - A2: Ajuste en CommandService (fallback a número + ensureUserExists + métricas de fallo).
 | 
						
						
						
							|  |  | - A3: Publicación condicional de mensaje por grupo con cooldown + persistencia mínima.
 | 
						
						
						
							|  |  | - A4: DM “just-in-time” al asignador ante fallo de mención + confirmación al primer DM de usuario.
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Archivos adicionales que podríamos necesitar añadir al chat en la implementación
 | 
						
						
						
							|  |  | - src/utils/whatsapp.ts (para confirmar normalizeDigits si lo reutilizamos en copy/URLs).
 | 
						
						
						
							|  |  | - apps/web/src/app.d.ts (si añadimos banner basado en session/locals).
 | 
						
						
						
							|  |  | - tests/unit/* y tests/web/* (para cubrir la nueva lógica de fallback y métricas).
 |