diff --git a/docs/refactor-command-service.md b/docs/refactor-command-service.md new file mode 100644 index 0000000..30633ce --- /dev/null +++ b/docs/refactor-command-service.md @@ -0,0 +1,205 @@ +# Plan de refactor de CommandService (enfoque A: handlers por acción) + +Este documento describe un plan incremental y sin cambios de UX para refactorizar `src/services/command.ts` en módulos más pequeños y testeables, manteniendo `CommandService` como punto de entrada público (handle/handleWithOutcome) y preservando textos y métricas existentes. + +## Objetivos + +- Reducir tamaño y complejidad cognitiva de `command.ts`. +- Separar responsabilidades por comando y por utilidades comunes. +- Mantener compatibilidad de API, textos, límites y métricas (no romper tests ni dashboards). +- Mejorar testabilidad y facilidad de evolución. + +## Principios + +- Compatibilidad primero: no cambiar mensajes ni nombres de métricas. +- Refactor incremental: PRs pequeñas por etapa/handler. +- Dependencias hacia abajo: handlers dependen de servicios existentes (tasks, contacts, group-sync, etc.), nunca al revés. +- Flags centralizadas o inyectadas: evitar leer las mismas ENV en múltiples sitios. +- Sin reordenar líneas de salida salvo que sea necesario (p. ej., quitar la última línea en blanco, como ahora). + +## Estructura objetivo + +- src/services/commands/index.ts ← router/orquestador (sustituye a processTareaCommand) +- src/services/commands/shared.ts ← helpers comunes (alias, fechas, IDs, membresía, límites) +- src/services/commands/parsers/nueva.ts +- src/services/commands/handlers/ + - ver.ts + - nueva.ts + - completar.ts + - tomar.ts + - soltar.ts + - configurar.ts + - web.ts +- src/services/onboarding.ts ← JIT y bundle de 2 DMs (disparado desde “nueva”) +- (opcional) src/services/web-access.ts ← gestión de tokens web (invalidate, generate, hash, URL) + +Servicios existentes a reutilizar (no mover): +- TaskService, GroupSyncService, ContactsService, IdentityService, AllowedGroups, ResponseQueue, Metrics, utils/crypto, utils/formatting, utils/icons, utils/whatsapp, db. + +## Etapas del refactor + +Etapa 0 — Red de seguridad y decisiones +- Congelar mensajes de usuario y métricas actuales. +- Inventario de dependencias por comando (DB y servicios). +- Añadir/reforzar tests de integración por comando: + - Parseo multi-IDs (completar/tomar). + - Gating de grupo (enforce). + - TZ y vencidas en “ver”. + - Web tokens (invalida previos, expira 10 min). + - “nueva”: asignación por contexto, @yo, hoy/mañana, YY-MM-DD→20YY. + +Etapa 1 — Router y shared +- Crear `commands/index.ts`: + - Resolver alias → acción canónica. + - Delegar por acción a handlers (temporalmente a un fallback que llama a la lógica actual). + - Mantener Help v1/v2 y CTA. +- Crear `commands/shared.ts`: + - ACTION_ALIASES y SCOPE_ALIASES. + - ymdInTZ(TZ), todayYMD. + - resolveTaskIdFromInput. + - parseMultipleIds(tokens, max=10) con truncado y dedup. + - enforceMembership(userId, task, flags) usando `GroupSyncService.isSnapshotFresh` y `GROUP_MEMBERS_ENFORCE`. + +Etapa 2 — Parser de “nueva” +- Mover `parseNueva` a `parsers/nueva.ts`. +- Tests unitarios: hoy/mañana (con y sin acento), YYYY-MM-DD y YY-MM-DD→20YY, @yo, limpieza de puntuación, excluir @menciones de la descripción. + +Etapa 3 — Handlers pequeños +- configurar.ts: upsert en `user_preferences`, validación `HH:MM`, etiquetas de respuesta. +- web.ts: invalidar tokens vigentes, generar token, hash (sha256), insertar y construir URL. Métrica `web_tokens_issued_total`. +- Conectar ambos en el router. + +Etapa 4 — Handler de lectura “ver” +- Soportar scopes “mis” y “todas”, límite 10, DM vs grupo (transición a DM). +- Cálculo de vencidas según TZ (dd/MM con ⚠️ si vencida). +- Agrupar por grupo, “y X más”, nombres de asignados (ContactsService). +- Métricas: `commands_alias_used_total` (info/mias/todas), `ver_dm_transition_total`. + +Etapa 5 — Handlers de mutación multi-ID +- completar.ts, tomar.ts, soltar.ts. +- Reutilizar parseMultipleIds, resolveTaskIdFromInput y enforceMembership. +- Mantener mensajes por caso (already/completed/not_found/bloqueadas) y resumen final, ayudas de uso en vacío. + +Etapa 6 — Handler “nueva” y Onboarding +- nueva.ts: + - Normalización de menciones (@tokens y menciones del contexto), exclusión bot, IdentityService, plausibilidad, @yo. + - Asignación por contexto (grupo sin menciones → sin dueño; DM sin menciones → creador). + - Guardar `task_origins` cuando aplique. Actualizar `last_command_at`. + - Acks al creador y DMs a asignados (idéntico formato, menciones). +- onboarding.ts: + - JIT por menciones/tokens irrecuperables (flags: ONBOARDING_PROMPTS_ENABLED, ONBOARDING_ENABLE_IN_TEST). + - Bundle de 2 DMs con cap (`ONBOARDING_EVENT_CAP`), cooldown (`ONBOARDING_DM_COOLDOWN_DAYS`), gating AllowedGroups, delays (`ONBOARDING_BUNDLE_DELAY_MS`), ResponseQueue y métricas: + - onboarding_prompts_sent_total / skipped_total + - onboarding_bundle_sent_total + - onboarding_recipients_capped_total + - onboarding_dm_skipped_total + - Mantener `ResponseQueue.setOnboardingAggregatesMetrics()` tras comando. + +Etapa 7 — Limpieza +- Reducir `CommandService` a: + - parseo de trigger (/t), registro de `last_command_at` y gating global inicial. + - delegación al router y clasificación de outcome (ok/error) como ahora. +- Centralizar CTA y textos estáticos compartidos si aplica. +- Opcional: centralizar flags en un `config.ts` liviano. + +Etapa 8 — Hardening y observabilidad +- Revisar ciclos de import (handlers no deben importar CommandService). +- Confirmar puntos de métricas y logs condicionales (`NODE_ENV !== 'test'`). +- Tests de humo por handler con DB en memoria. +- Documentar contratos en `commands/README.md` (opcional). + +## Detalles técnicos por handler + +- configurar: + - Validar mapa de alias: diario/daily, l‑v/weekdays, semanal/weekly, off/apagar/ninguno. + - Hora opcional `HH:MM` (clamp 00–23; minutos 00–59). + - Upsert con `last_reminded_on` a NULL al cambiar frecuencia. +- web: + - Solo por DM (en grupo: mensaje instructivo). + - Requiere `WEB_BASE_URL`; si falta, advertir. + - Invalidar tokens vigentes (used_at = now) con `expires_at > now`. + - Generar token base64url (32 bytes), guardar hash SHA‑256, expira en 10 min. +- ver: + - “mis”: agrupar por `group_id` (nombre desde cache de grupos), due_date con `formatDDMM`, marcar vencidas según TZ local (no del host). + - “todas”: “Tus tareas” + “Sin responsable” por grupo (DM: usando snapshot fresca de membresía). + - En grupo: no listar; responder por DM (transición). +- completar/tomar/soltar: + - Multi-IDs: espacios y/o comas, dedup, máx. 10; resumen de resultados. + - enforceMembership si `GROUP_MEMBERS_ENFORCE=true` y snapshot fresca disponible. + +## Utilidades compartidas (shared.ts) + +- ACTION_ALIASES y SCOPE_ALIASES (incluyendo: n/+/crear/nueva, ver/ls/listar/mostrar, x/hecho/completar/done, tomar/claim, soltar/unassign, ayuda/help/info/?, configurar/config, web). +- ymdInTZ(d, TZ) y todayYMD. +- formatters: reutilizar `formatDDMM`, `codeId`, `padTaskId`, `bold`, `italic`, `code`, `section`. +- parseMultipleIds(tokens: string[], max=10): retorna números válidos > 0, dedup, truncado. +- resolveTaskIdFromInput(n): `TaskService.getActiveTaskByDisplayCode`. +- enforceMembership(sender, task, flags): si `task.group_id` y snapshot fresca, bloquear si no es miembro activo. + +## Flags/ENV a centralizar o respetar + +- FEATURE_HELP_V2 +- GROUP_GATING_MODE, GROUP_MEMBERS_ENFORCE +- TZ (por defecto: Europe/Madrid) +- WEB_BASE_URL, CHATBOT_PHONE_NUMBER +- ONBOARDING_PROMPTS_ENABLED, ONBOARDING_ENABLE_IN_TEST +- ONBOARDING_EVENT_CAP, ONBOARDING_DM_COOLDOWN_DAYS, ONBOARDING_BUNDLE_DELAY_MS + +Sugerencia: leer una vez y pasar por contexto/env a los handlers. + +## Métricas a preservar + +- commands_alias_used_total (labels: action=info/mias/todas) +- ver_dm_transition_total +- web_tokens_issued_total +- commands_unknown_total +- commands_blocked_total +- onboarding_prompts_sent_total / onboarding_prompts_skipped_total +- onboarding_bundle_sent_total +- onboarding_recipients_capped_total +- onboarding_dm_skipped_total +- setOnboardingAggregatesMetrics() tras recibir comandos + +## Riesgos y mitigaciones + +- Cambios de texto rompen tests/UX: mantener textos y orden de líneas. +- Ciclos de import: mantener dependencias unidireccionales hacia servicios. +- Lectura repetida de ENV: centralizar o inyectar en contexto. +- Puntos de métricas: conservar nombres y ubicaciones lógicas. + +## Criterios de aceptación por etapa + +- Compila y pasa tests existentes. +- Salidas de texto idénticas (incluye saltos de línea). +- Mismas métricas y side effects (ResponseQueue). +- Para multi-IDs: mismo conteo y resumen. +- Para “ver”: mismas reglas de vencidas y agrupación. +- Para “web”: mismo comportamiento de invalidad y expiración de tokens. + +## Estrategia de PRs + +- 1 PR por etapa o por handler (máximo ~300–500 LOC cambiadas). +- Cada PR: + - Mantiene API de `CommandService`. + - Sustituye una pieza con tests. + - Checklist (abajo). + - Sin reformat masivo no relacionado. + +## Checklist rápida por PR + +- [ ] Textos exactos (incluye emojis, mayúsculas, espacios). +- [ ] Métricas: nombres y contadores como antes. +- [ ] Flags: respetadas (valores por defecto idénticos). +- [ ] Lógica de límites (máx. 10 IDs, “y X más”, etc.). +- [ ] En grupo vs DM (transiciones e instrucciones). +- [ ] Onboarding: condiciones y métricas (si aplica). +- [ ] Sin ciclos de import; dependencias correctas. +- [ ] Tests de humo/integración cubren ruta feliz y errores principales. + +## Notas de compatibilidad + +- No cambiar el contrato público: `CommandService.handle` y `handleWithOutcome`. +- Mantener `CommandService.dbInstance` para DI mínima y compatibilidad con tests (inyectar en handlers cuando lo necesiten). +- Logs condicionales (`NODE_ENV !== 'test'`) como hoy. + +Con este plan podrás avanzar en pasos pequeños, verificando en cada etapa que el comportamiento externo se mantiene mientras mejoras la mantenibilidad interna.