You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
taskbot/docs/refactor-command-service.md

206 lines
9.7 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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, lv/weekdays, semanal/weekly, off/apagar/ninguno.
- Hora opcional `HH:MM` (clamp 0023; minutos 0059).
- 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 SHA256, 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 ~300500 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.