# Plan de Desarrollo: NOVASIS CRM 360

**Fecha**: 2026-10-01 · **Spec funcional**: `docs/spec-crm-360.md`
**Memoria del módulo**: `src/crm/CLAUDE.md`
**Equipo**: 2 desarrolladores (Dev A, Dev B) + 1 supervisor (Code100)
**Duración**: ~23 semanas, 5 fases + prerrequisitos
**Rama**: `crm` (backend desde `pos` · frontend desde `main`)

> **Leyenda:** ✅ Implementado | ⚠️ Parcial | ❌ Pendiente | 🔒 Bloqueado por prerrequisito
>
> **Estado al 2026-10-02: Fase 0 completa.** Backend en `crm` de `novasispy-backend-api`
> (CRM-01..04), frontend en `crm` de `novasispy-erp` (CRM-05). **Las cuatro migraciones
> todavía no se aplicaron en ningún entorno.**

> ### ⚠️ Antes de tomar cualquier ticket
> La spec y la v1.0 de este plan fueron escritas sin leer el código. Varios nombres de
> servicios, tablas y columnas **no existen**. Están verificados y corregidos en
> **`src/crm/CLAUDE.md` → "Correcciones verificadas contra el código"**. Los prompts de este
> archivo ya usan los nombres reales.

---

## 0. Decisiones de diseño confirmadas

| # | Tema | Decisión |
|---|---|---|
| D1 | Usuario del CRM | PyMEs clientes de NOVASIS (add-on por empresa) |
| D2 | Niveles | `CRM_ESENCIAL` (incluye bandeja WhatsApp de 1 número) · `CRM_PRO` (+ `CRM_OMNICANAL`, `CRM_ATENCION`, `CRM_RECURRENTE`, `CRM_IA`) |
| D3 | WhatsApp | Adaptador `IWhatsAppProvider` con **Meta Cloud API** (default producción) y **Evolution API** (`cloud` y `baileys`). Twilio solo SMS |
| D4 | Baileys | Solo demo. Con `evolution_baileys` se **bloquean** todos los envíos automáticos |
| D5 | Facturas recurrentes | **Siempre** requieren aprobación del **Supervisor**. Emisión SIFEN y débito solo después |
| D6 | Fecha de emisión recurrente | Fecha de aprobación (SIFEN no admite fecha futura) |
| D7 | IA | **BYOK** vía la config de IA existente (`ai_empresa_config`). NOVASIS no cobra por conversación |
| D8 | Período de prueba | Días configurables por plan, sobrescribibles por cliente en `suscripciones.limite_override` |
| D9 | Datos maestros | El CRM **no duplica**: referencia por FK |
| D10 | Facturación | El CRM arma un **payload** y lo entrega a `FacturasService` / `PedidosService`. Nunca XML SIFEN, stock ni asientos |
| D11 | Nomenclatura | Prefijo `crm_`. "Contrato recurrente" ≠ `suscripciones` |
| **D13** | **Bus de eventos** | **No se usa.** Llamada directa para lo síncrono, cola BullMQ para lo asíncrono. Justificación en `src/crm/CLAUDE.md` |
| D12 | Pendientes de decidir | Facturación por consumo (propuesta: F3, carga manual/Excel) · precios finales |

---

## 1. Cómo trabajar este plan con Claude Code

### 1.1 Ciclo por ticket

```
1. git checkout -b feature/crm-XX-descripcion-corta   (desde la rama crm)
2. Pegar el PROMPT del ticket. Pedir primero el plan y revisarlo.
3. Aprobar → implementar.
4. Correr: migración + prisma generate + build + smoke test del ticket.
5. Revisar el diff completo (empresa_id en cada query, permisos, sin $executeRaw).
6. Actualizar src/crm/CLAUDE.md (❌ → ✅) en el mismo commit.
7. PR → revisión del supervisor → merge a crm.
```

### 1.2 Plantilla de prompt base

```text
Contexto: módulo CRM 360 de NOVASIS. Leé src/crm/CLAUDE.md (incluida la tabla de
correcciones) y docs/plan-crm-360.md (ticket CRM-XX). Respetá TODAS las reglas del
CLAUDE.md raíz: empresa_id desde @GetEmpresa() en toda query, @RequirePermission en cada
endpoint, permisos solo en seed.service.ts (idempotente), migraciones idempotentes, precios
desde lista-precios, stock solo vía movimientos_inventario, facturas solo vía FacturasService,
nunca borrar documentos confirmados. No instales @nestjs/event-emitter (ver D13).
Primero mostrame el plan de archivos a crear/modificar; no escribas código hasta que lo apruebe.
```

### 1.3 Definition of Done

- [ ] Migración SQL idempotente en `prisma/migrations/<fecha>_crm_<nombre>/` + `npx prisma generate`
- [ ] Toda tabla nueva con `empresa_id UUID NOT NULL` + índice `(empresa_id, ...)`
- [ ] DTOs con class-validator (`@IsUUID()`, `@IsEnum()`, …)
- [ ] Permisos nuevos en `seed.service.ts` por `codigo`
- [ ] Guard de módulo de suscripción
- [ ] Smoke test agregado o extendido
- [ ] Test de multitenancy: empresa B no ve datos de empresa A
- [ ] `src/crm/CLAUDE.md` actualizado
- [ ] Frontend: servicio en `src/api/crm/*.service.js`, página en `src/pages/crm/`, componentes
      en `src/components/crm/`, responsive (card mobile + tabla desktop), entrada de menú en
      `Sidebar.jsx` / `useMenuAcceso.js` / `routers/routes.jsx` con gating de módulo

---

## 2. Estructura objetivo

### Backend (`src/crm/`)

```
crm/
  CLAUDE.md
  crm.module.ts
  common/
    crm-modulo.guard.ts          ← verifica módulo habilitado y prueba vigente
    telefono.util.ts             ← normalización E.164 (+595)
    numeracion-crm.service.ts    ← OPP-, CTR-, TCK- por empresa
  config/        (M90)  pipelines, etapas, orígenes, motivos, SLA, horarios, parámetros
  leads/         (M91)  CRUD, captura, dedupe, asignación, conversión, importación
  timeline/      (M92)  ficha 360 (agregación de lectura)
  oportunidades/ (M93)  CRUD, kanban, ítems, forecast, estancamiento
  actividades/   (M94)  agenda, visitas geo
  canales/       (M95)  IWhatsAppProvider, meta-cloud, evolution, email IMAP/SMTP, webchat
  conversaciones/(M95)  bandeja, mensajes, plantillas, ventana 24h
  tickets/       (M96)  tickets, SLA, KB, CSAT
  portal/        (M97)  OTP, endpoints públicos
  cierre/        (M98)  asistente de cierre → payload-facturacion.ts
  contratos/     (M99)  contratos, líneas, consumos, ciclos, aprobación, débito, dunning, MRR
  automatizaciones/(M100) reglas evento-condición-acción (cola BullMQ)
  ia/            (M101) scoring, copiloto, agente
  reportes/      (M102)
  prueba/               período de prueba del add-on
  jobs/                 crons @nestjs/schedule + processors BullMQ
```

### Frontend (repo `novasispy-erp`, rama `crm`)

```
src/api/crm/        leads.service.js, oportunidades.service.js, bandeja.service.js, ...
src/pages/crm/      CrmPipeline.jsx, CrmLeads.jsx, CrmOportunidad.jsx, CrmBandeja.jsx,
                    CrmTickets.jsx, CrmContratos.jsx, CrmAprobacionRecurrentes.jsx, ...
src/components/crm/ KanbanBoard, OportunidadCard, Timeline360, CierreVentaWizard, ChatPanel, ...
src/store/crmStore.jsx   Zustand (mismo patrón que AuthStore.jsx)
```

---

## 3. Prerrequisitos en el ERP (PRE)

| ID | Tarea | Bloquea | Dueño | Est. | Estado |
|---|---|---|---|---|---|
| PRE-01 | Órdenes de Venta 2B: `POST /ordenes-venta/:id/facturar` + `PedidosService.convertirAFactura()` | F1 (CRM-14) | Dev A | 4 d | ❌ |
| PRE-02 | ~~Aplicar la migración `20260504_orden_tipo_facturacion`~~ — **ya está aplicada**. Verificado el 01/10/2026 en prod (`smartfactpy`) y test (`novasiserp`): ambas la registran el 05/05/2026 y `pedidos.tipo_facturacion` existe con default `total`. No hay nada que hacer | F1 | — | 0 d | ✅ |
| PRE-03 | Control de crédito de clientes: `limite_credito`, `saldo_pendiente`, `verificarCredito()` | F1 (CRM-14 paso 4) | Dev B | 5 d | ⚠️ parcial — `verificarCredito` y las columnas ya existen; validar alcance |
| PRE-04 | POS Retail: botón **"Cobrar Pedido"** | Piloto AGOGO | Dev B | 3 d | ❌ |
| PRE-05 | Verificación Meta Business + número WhatsApp Cloud API (AGOGO y DOBASA) — **trámite, iniciar en semana 1** | F2 | Supervisor | — | ❌ |
| PRE-06 | Bancard real (salir de MOCK) + Débito Automático/tokenización | F3 (CRM-36) | Supervisor + Dev A | 5 d | ❌ |
| PRE-07 | DOBASA: cerrar incidencias abiertas (SIFEN con error, proveedores invisibles si ya son clientes, egresos de Tesorería, reportes de ventas) | Piloto DOBASA | Dev A/B | 5 d | ❌ |

### Estado de los entornos (verificado 01/10/2026)

| | Base | Migraciones | Última |
|---|---|---|---|
| Producción | `smartfactpy` @ 167.99.127.42 | 358 | 2026-09-29 |
| Test | **`novasiserp`** @ 143.198.100.72 | 349 | 2026-09-24 |
| Repo (rama `crm`) | `prisma/migrations/` | 346 directorios | — |

Dos cosas para no asustarse al mirar `_prisma_migrations`:

- Hay **10 filas marcadas como revertidas** en prod (9 en test). Son intentos fallidos
  históricos; las 10 se resolvieron. Nueve se reaplicaron con el mismo nombre y la décima
  (`20260908_productos_rubro_override`) se redató a `20260909_` y se aplicó así — por eso la
  fila vieja queda huérfana. `productos.rubro_id` existe en los dos entornos. **No hay hueco
  de esquema.**
- Prod registra más migraciones que directorios hay en el repo. Es esperable: incluye nombres
  resueltos con `migrate resolve` y los intentos fallidos de arriba.

---

## FASE 0 — Fundaciones (semanas 1–2)

| ID | Ticket | Dev | Est. | Estado |
|---|---|---|---|---|
| CRM-01 | Migración base: catálogos y config | A | 2 d | ✅ |
| CRM-02 | Submódulos + permisos + **período de prueba** (el guard ya existe: ver `src/crm/CLAUDE.md`) | B | **2 d** | ✅ |
| CRM-03 | Infraestructura: numeración OPP/CTR/TCK y util E.164. Colas y lock Redis diferidos al ticket que los use (ver `src/crm/CLAUDE.md`) | A | **1 d** | ✅ |
| CRM-04 | Seed de configuración por defecto al habilitar el módulo | B | 1 d | ✅ |
| CRM-05 | Frontend: sección CRM en el menú con gating + layout + `crmStore` + pantalla de configuración | B | 2 d | ✅ |

### CRM-01 — Migración base

Tablas: `crm_pipelines`, `crm_etapas`, `crm_origenes`, `crm_motivos`, `crm_parametros`,
`crm_sla_politicas`, `crm_calendarios`. Columnas en spec §8.2.

```text
Ticket CRM-01. Creá la migración prisma/migrations/<hoy>_crm_base con las tablas de
configuración del CRM descritas en docs/spec-crm-360.md §8.2. Idempotente, UUID
gen_random_uuid(), empresa_id NOT NULL, created_at/updated_at/created_by, índices
(empresa_id). Actualizá schema.prisma y corré prisma generate. Creá el submódulo
src/crm/config con CRUD de pipelines y etapas (reordenar incluido) con permiso CRM_CONFIG.
```

**Aceptación**: CRUD de pipelines/etapas funciona; reordenar persiste; empresa B no ve las de A.

### CRM-02 — Módulos, permisos y prueba

- `plan_modulos`: `CRM_ESENCIAL`, `CRM_PRO`, `CRM_OMNICANAL`, `CRM_ATENCION`,
  `CRM_RECURRENTE`, `CRM_IA` (CRM_PRO implica los 4 submódulos).
- Permisos: los 19 de spec §10.2. Perfil **Supervisor** recibe `CRM_CONTRATOS_APROBAR`;
  Administración **no**.
- Prueba: `suscripcion_modulos.fecha_fin_prueba DATE`, `planes.crm_dias_prueba SMALLINT`;
  override en **`suscripciones.limite_override`** (Json). Estado derivado:
  `prueba | activo | solo_lectura | bloqueado`.
- `CrmModuloGuard`: 403 si el submódulo no está habilitado; en `solo_lectura` permite solo GET.
- Job `crm.prueba-avisos` (diario): avisos a 7, 3 y 1 día; transición a solo_lectura y bloqueo.

```text
Ticket CRM-02. Agregá los módulos y permisos del CRM al seed (idempotente por codigo) según
docs/spec-crm-360.md §10.1 y §10.2. Asigná CRM_CONTRATOS_APROBAR al perfil Supervisor.
Implementá CrmModuloGuard y el decorador @RequireCrmModulo('CRM_RECURRENTE'). Implementá el
período de prueba: migración (suscripcion_modulos.fecha_fin_prueba, planes.crm_dias_prueba),
CrmPruebaService con estadoModulo(empresaId), job de avisos. El override por cliente va en
suscripciones.limite_override (Json) — NO existe limites_personalizados. En solo_lectura el
guard bloquea POST/PUT/PATCH/DELETE con mensaje claro. Tests: prueba vigente, vencida, bloqueada.
```

### CRM-03 — Infraestructura

- Colas BullMQ: `crm-automatizaciones`, `crm-recurrente`, `crm-mensajeria`.
- Locks por empresa en jobs (Redis `SET NX`) para múltiples instancias PM2.
- `NumeracionCrmService.siguiente(empresaId, 'OPP'|'CTR'|'TCK')` con tabla `crm_numeracion`
  y `SELECT ... FOR UPDATE`.
- **Sin event emitter** (D13).

---

## FASE 1 — MVP Ventas = **CRM Esencial** (semanas 3–7)

| ID | Ticket | Dev | Est. | Estado |
|---|---|---|---|---|
| CRM-10 | Leads: tabla, CRUD, dedupe, asignación round-robin, conversión | A | 4 d | ❌ |
| CRM-11 | Importación de leads Excel/CSV con mapeo y reporte de duplicados | B | 2 d | ❌ |
| CRM-12 | Oportunidades + ítems, totales (único, MRR, TCV), mover etapa, ganar/perder | A | 5 d | ❌ |
| CRM-13 | Actividades + agenda + "próxima actividad" + estancamiento (job diario) | B | 3 d | ❌ |
| CRM-14 | **Asistente de cierre** (M98) 🔒 PRE-01/03 | A | 6 d | ❌ |
| CRM-15 | Presupuesto desde oportunidad + sync de etapa al aceptar/rechazar | B | 2 d | ❌ |
| CRM-16 | Timeline 360 + pestaña CRM en ficha de cliente | B | 3 d | ❌ |
| CRM-17 | Frontend: Kanban (drag & drop), lista, detalle, leads | B | 6 d | ❌ |
| CRM-18 | Frontend: `CierreVentaWizard` (8 pasos) | A | 4 d | ❌ |
| CRM-19 | Reportes F1 | A | 2 d | ❌ |
| CRM-20 | Fechas especiales + fechas comerciales configurables | B | 2 d | ❌ |
| CRM-21 | Smoke `test:smoke:crm-f1` + multitenancy | A | 1 d | ❌ |

### CRM-10 — Leads

Tabla y reglas en spec §6.2 y §8.2. Dedupe: si coincide con **cliente** → no crea lead,
devuelve `{ duplicado: 'cliente', cliente_id }`; si coincide con lead abierto → devuelve el
existente. Índice único parcial `(empresa_id, telefono_e164) WHERE estado <> 'descartado'`.

```text
Ticket CRM-10. Implementá src/crm/leads según docs/spec-crm-360.md §6.2 y §8.2.
Endpoints: GET/POST /crm/leads, PATCH /crm/leads/:id, POST /crm/leads/:id/convertir,
POST /crm/leads/:id/descartar. Usá telefono.util para E.164 y ClientesService para crear
clientes (no insertes directo en clientes). Asignación round-robin por sucursal con índice
rotativo en Redis. Tests de dedupe (cliente existente, lead existente, teléfono en formatos
0981..., +595981..., 595981...).
```

### CRM-12 — Oportunidades

Reglas en spec §6.4. Precios: **`ListaPreciosService.obtenerPrecioProducto({...})`** (NO
`getPrecio`, que no existe); nunca desde el body sin validar. `PATCH /:id/etapa` valida
`campos_obligatorios` de la etapa destino; si destino `es_ganada` → **409** con
`{ requiere: 'cierre' }`. Perder exige `motivo_perdida_id`. Visibilidad: el vendedor ve las
suyas salvo `CRM_OPORTUNIDADES_TODAS`.

### CRM-14 — Asistente de cierre (núcleo)

`CierreVentaService` con dos métodos:

- `preview(empresaId, oportunidadId, dto)` → valida y devuelve `PayloadFacturacion` + las
  primeras 3 facturas si hay recurrencia (**sin persistir**).
- `ejecutar(...)` → en **una transacción Prisma**: (a) cliente fiscal actualizado,
  (b) Orden de Venta vía `PedidosService.crearOrden/confirmarOrden` **o** factura directa vía
  `FacturasService`, (c) contrato BORRADOR→ACTIVO si hay ítems recurrentes (en F1 devolver
  **422** "Recurrente requiere CRM Pro"), (d) oportunidad `ganada` con FKs, (e) llamadas
  directas a lo que deba reaccionar (D13: sin eventos).

Validaciones: RUC+DV (módulo 11 existente), naturaleza del receptor, crédito con
`verificarCredito()` (override con PIN de supervisor vía `autorizaciones_caja`), stock por depósito.

`PayloadFacturacion` en `cierre/payload-facturacion.ts`: ver spec §7.1.

```text
Ticket CRM-14 (requiere PRE-01 y PRE-03 mergeados). Implementá src/crm/cierre según
docs/spec-crm-360.md §6.9 y §7. Primero definí la interfaz PayloadFacturacion y un mapper
toCreateFacturaDto() que reutilice el DTO existente de FacturasService (leé ese DTO antes).
El CRM NO arma XML SIFEN ni toca stock: delega en FacturasService/PedidosService.
Endpoints: POST /crm/oportunidades/:id/cierre/preview y POST /crm/oportunidades/:id/cierre
(transacción). Precios con ListaPreciosService.obtenerPrecioProducto (NO getPrecio).
Casos de test: contado consumidor final, crédito 3 cuotas con planes_cuotas, crédito excedido
(espera override), con orden de venta y reserva de stock, intento de recurrente sin CRM_PRO (422).
Migración idempotente que agrega oportunidad_id a factura_cab, presupuesto_cab y pedidos
(ojo: la tabla es presupuesto_cab, NO presupuestos).
```

**Aceptación**: la factura emitida coincide 100% con el preview; rollback total si falla
cualquier paso; la factura queda en la cola SIFEN existente.

### CRM-15 — Presupuesto desde oportunidad

Sin bus de eventos (D13): `presupuestos.service.ts` llama a un método del CRM cuando el
presupuesto pasa a ACCEPTED/REJECTED. Es una línea agregada, no un refactor.

> **Nota de campo (DOBA, 30/09/2026)**: "Facturar directo" desde presupuesto exige estado
> ACCEPTED, y desde DRAFT no está disponible "Marcar como aceptado". Si el CRM va a crear
> presupuestos, validar en qué estado nacen.

### CRM-18 — Wizard frontend

Pasos: Cliente fiscal → Ítems y precios → Tipo de facturación → Condición comercial →
Logística → Recurrencia (deshabilitado en Esencial) → Resumen (preview del backend) →
Confirmación. Estado en Zustand (`src/store/crmStore.jsx`), validación por paso, "Confirmar"
llama a `/cierre` solo tras preview OK.

### CRM-20 — Fechas especiales (pedido del piloto AGOGO)

Tabla `crm_fechas_especiales`: `cliente_id`, `tipo` (`cumpleanos|aniversario|otra|comercial`),
`descripcion`, `destinatario_nombre`, `dia`, `mes`, `anio NULL`, `recordar_dias_antes` (7),
`canal_preferido`. Fechas comerciales (Día de la Madre PY 15/05, San Valentín 14/02, Navidad,
Día del Padre, Día del Amigo PY 30/07) como catálogo editable por empresa. Job diario crea
**actividad**; en F2 además mensaje WhatsApp de plantilla.

---

## FASE 2 — Omnicanal y Atención (semanas 8–13)

| ID | Ticket | Dev | Est. | Estado |
|---|---|---|---|---|
| CRM-22 | `IWhatsAppProvider` + `MetaCloudProvider` + `EvolutionProvider` + webhooks firmados 🔒 PRE-05 | A | 6 d | ❌ |
| CRM-23 | Conversaciones y mensajes: ingesta webhook → dedupe → lead/cliente, ventana 24 h | A | 4 d | ❌ |
| CRM-24 | Plantillas (sync con Meta) + estimador de costo por mensaje | B | 3 d | ❌ |
| CRM-25 | Email: IMAP polling + SMTP (`empresas_correo`), hilos por Message-ID | B | 4 d | ❌ |
| CRM-26 | Frontend Bandeja + WebSocket | B | 6 d | ❌ |
| CRM-27 | Acciones desde el chat (presupuesto/KuDE/link de pago, "Listo para retirar") | A | 3 d | ❌ |
| CRM-28 | Tickets + SLA (reloj, pausas, alertas 75%, escalamiento) | A | 5 d | ❌ |
| CRM-29 | NC SIFEN desde ticket + `nota_credito_cab.ticket_id` | A | 2 d | ❌ |
| CRM-30 | CSAT automático al resolver ticket / ganar venta | B | 2 d | ❌ |
| CRM-31 | Automatizaciones: motor evento→condición→acción sobre cola BullMQ, UI, límite 10 en Esencial | B | 5 d | ❌ |
| CRM-32 | Smoke `test:smoke:crm-f2` + test "Baileys bloquea automáticos" | A | 1 d | ❌ |

### CRM-22 — Adaptador WhatsApp

```ts
// canales/whatsapp/whatsapp-provider.interface.ts
export interface IWhatsAppProvider {
  readonly tipo: 'meta_cloud' | 'evolution_cloud' | 'evolution_baileys';
  enviarTexto(canal: CrmCanal, to: string, texto: string): Promise<EnvioResult>;
  enviarPlantilla(canal: CrmCanal, to: string, plantilla: string, idioma: string, vars: string[]): Promise<EnvioResult>;
  enviarAdjunto(canal: CrmCanal, to: string, url: string, mime: string, caption?: string): Promise<EnvioResult>;
  parsearWebhook(headers: Record<string, string>, body: unknown): WebhookEvento[];   // valida firma
  estadoSesion?(canal: CrmCanal): Promise<'conectado' | 'desconectado' | 'qr_pendiente'>;
}
```

Factory por `crm_canales.proveedor`. Credenciales cifradas con el mismo AES de `bancard_config`.
Webhooks: `POST /crm/webhooks/whatsapp/meta` (verifica `X-Hub-Signature-256` + GET de
verificación `hub.challenge`), `POST /crm/webhooks/whatsapp/evolution/:canalId`.
**Regla D4**: `MensajeriaService.enviarAutomatico()` lanza `BaileysAutomaticoBloqueadoError`
si el canal es `evolution_baileys`. Los envíos manuales del agente sí se permiten.
Costos: `crm_tarifas_whatsapp (pais, categoria, precio_usd, vigente_desde)`; desde el
**01/10/2026** también se cobran las respuestas en ventana.

### CRM-23 — Ingesta

Webhook → `ConversacionesService.ingresar()`: normaliza E.164 → busca cliente → si no, lead
(dedupe CRM-10) → upsert conversación → guarda mensaje → `ventana_24h_hasta = now + 24h` →
asigna (regla M90) → encola el trabajo asíncrono → WebSocket a la bandeja (reutilizar el
patrón de `PosGateway`, con un `CrmGateway` nuevo con rooms por empresa y usuario).

### CRM-27 — Flujo AGOGO (retiro en local)

oportunidad → (cierre) Orden de Venta con reserva → link de pago (`pago_link`) → pagado por
link **o** "pagará en el local" → `listo_retirar` (mensaje WhatsApp) → retirado. Si paga en el
local se cobra con **PRE-04**. Flag `crm_parametros.envios_habilitados = false`.

### CRM-28 — Tickets y SLA

`crm_tickets` + `crm_ticket_eventos`. Reloj: `vence_primera_resp`, `vence_resolucion`
calculados con calendario laboral; pausa en `en_espera_cliente` (acumula `segundos_pausa`).
Job `crm.sla` cada 5 min: 75% → notificación; vencido → escala a supervisor + `sla_incumplido`.

---

## FASE 3 — Recurrente y Portal = **CRM Pro** (semanas 14–18)

| ID | Ticket | Dev | Est. | Estado |
|---|---|---|---|---|
| CRM-33 | Contratos: tablas, CRUD, activación desde el cierre | A | 4 d | ❌ |
| CRM-34 | Motor de ciclos: borradores PENDIENTE_APROBACION; fijas + consumo + descuentos + prorrateo | A | 5 d | ❌ |
| CRM-35 | **Aprobación** (Supervisor): bandeja, lote, editar, omitir; al aprobar → `FacturasService` → SIFEN | B | 5 d | ❌ |
| CRM-36 | Débito automático Bancard post-SIFEN + reintentos 🔒 PRE-06 | A | 4 d | ❌ |
| CRM-37 | Dunning (−3, 0, +3, +10, +30, +60) + EN_MORA/SUSPENDIDO + `config_mora` | B | 3 d | ❌ |
| CRM-38 | Renovaciones, ajustes de precio, cambio de plan con prorrateo, baja con motivo | A | 4 d | ❌ |
| CRM-39 | Métricas MRR/ARR/churn/cohortes + snapshot diario | B | 3 d | ❌ |
| CRM-40 | Portal del cliente con OTP | B | 5 d | ❌ |
| CRM-41 | Comisiones: venta ganada **y cobrada** → base de comisión | A | 2 d | ❌ |
| CRM-42 | PWA vendedor offline (Dexie) + check-in geolocalizado | B | 4 d | ❌ |
| CRM-43 | Smoke `test:smoke:crm-f3` | A | 2 d | ❌ |

### CRM-34/35 — Reglas críticas de la facturación recurrente

1. El job **nunca** llama a `FacturasService`; solo crea `crm_contrato_ciclos` en
   `pendiente_aprobacion` con las líneas calculadas en `detalle JSONB`.
2. Solo `POST /crm/contratos/ciclos/aprobar` (`CRM_CONTRATOS_APROBAR`) emite.
3. Fecha de emisión = fecha de aprobación. USD → cotización del día de aprobación.
4. Débito Bancard **solo** cuando el DE está aprobado por SIFEN.
5. Un ciclo pendiente **no genera mora**. El dunning corre sobre facturas emitidas.
6. Recordatorio al Supervisor el día de emisión y escalamiento al Administrador a las 48 h.
7. Edición previa a aprobar queda auditada (old/new vía AuditInterceptor).

```text
Ticket CRM-34. Implementá el motor de ciclos de src/crm/contratos según docs/spec-crm-360.md
§6.10 y las reglas críticas 1–7. Job crm.facturacion-recurrente (05:00, lock por empresa) que
selecciona contratos ACTIVO/EN_MORA con proxima_emision <= hoy + rec_dias_anticipacion y crea
ciclos en 'pendiente_aprobacion' con detalle calculado (fijas + consumos del período +
descuentos con ciclos_restantes + prorrateo del primer período por días). NO emitas facturas
en este ticket. Idempotencia por UNIQUE(contrato_id, periodo_desde) con ON CONFLICT DO NOTHING.
Tests: mensual anticipado, trimestral, prorrateo, descuento 3 ciclos, consumo, doble ejecución.
```

```text
Ticket CRM-35. Implementá la aprobación de ciclos: GET /crm/contratos/ciclos/pendientes,
PATCH /crm/contratos/ciclos/:id (editar antes de aprobar), POST /crm/contratos/ciclos/:id/omitir
(motivo obligatorio), POST /crm/contratos/ciclos/aprobar { ids[] } (lote). Al aprobar:
construir PayloadFacturacion (reutilizar cierre/payload-facturacion.ts), emitir vía
FacturasService con fecha de hoy y numeración de la sucursal del contrato, pasar a 'emitido',
enviar KuDE + link de pago. Permiso CRM_CONTRATOS_APROBAR. Frontend CrmAprobacionRecurrentes.jsx
con selección múltiple y diferencias vs ciclo anterior resaltadas. Test: usuario sin permiso →
403; ninguna factura recurrente existe sin aprobado_por.
```

---

## FASE 4 — IA y canales Meta (semanas 19–23)

| ID | Ticket | Dev | Est. | Estado |
|---|---|---|---|---|
| CRM-44 | IA: resumen de conversación/ticket + respuesta sugerida (copiloto) | A | 4 d | ❌ |
| CRM-45 | Agente IA WhatsApp/web chat: herramientas read-only, handoff, horario | A | 6 d | ❌ |
| CRM-46 | Lead/deal scoring + predicción de churn — job nocturno | B | 4 d | ❌ |
| CRM-47 | Instagram/Facebook Messenger en la bandeja | B | 4 d | ❌ |
| CRM-48 | Web chat widget (JS embebible) + `CrmGateway` público | B | 4 d | ❌ |
| CRM-49 | Base de conocimiento + embeddings pgvector (ya disponible) | A | 3 d | ❌ |
| CRM-50 | Secuencias de seguimiento con salida al responder | B | 3 d | ❌ |
| CRM-51 | Sync calendario Google/Outlook | B | 2 d | ❌ |
| CRM-52 | Reportes completos + widgets Dashboard | A | 3 d | ❌ |
| CRM-53 | Smoke `test:smoke:crm-f4` + seguridad del agente (prompt injection, empresa_id) | A | 2 d | ❌ |

### Reglas IA (D7)

- Usa **siempre** la configuración BYOK de la empresa (`ai_empresa_config`, API key cifrada).
  Sin key → funciones IA deshabilitadas con mensaje.
- El agente usa tool calling con herramientas tipadas **de solo lectura** filtradas por
  `empresa_id`. Prohibido: crear descuentos, modificar documentos, SQL libre.
- Toda llamada se registra en la auditoría de IA existente (tokens, tiempo, resultado).
- **El texto del cliente es dato no confiable, nunca instrucción al sistema.**

---

## 4. Cronograma

| Semana | Dev A | Dev B | Hito |
|---:|---|---|---|
| 1–2 | PRE-01, CRM-01, CRM-03 | PRE-03, CRM-02, CRM-04, CRM-05 | PRE-05 iniciado (Meta) · PRE-02 ya cumplida |
| 3–4 | CRM-10, CRM-12 | CRM-11, CRM-13, PRE-04 | |
| 5–6 | CRM-14, CRM-18 | CRM-15, CRM-16, CRM-17 | |
| 7 | CRM-19, CRM-21 | CRM-20, CRM-17 | **Release CRM Esencial** · piloto AGOGO · DOBASA F1 (si PRE-07 cerrado) |
| 8–10 | CRM-22, CRM-23, CRM-27 | CRM-24, CRM-25, CRM-26 | |
| 11–13 | CRM-28, CRM-29, CRM-32 | CRM-30, CRM-31 | **AGOGO: bandeja + retiro en local** · DOBASA: bandeja + reclamos |
| 14–16 | CRM-33, CRM-34, CRM-36 | CRM-35, CRM-37 | PRE-06 Bancard real |
| 17–18 | CRM-38, CRM-41, CRM-43 | CRM-39, CRM-40, CRM-42 | **Release CRM Pro** |
| 19–23 | CRM-44, CRM-45, CRM-49, CRM-52, CRM-53 | CRM-46, CRM-47, CRM-48, CRM-50, CRM-51 | Add-on IA |

> **Camino crítico para el release de la semana 7**: PRE-01 → PRE-03 → CRM-14 → CRM-18.
> PRE-01 y PRE-03 están en devs distintos y suman 9 días; CRM-14 necesita los dos mergeados.
>
> **Supuesto del plan**: TURNE APP ya en producción al iniciar (prioridad actual de Blue
> Dragon). Si no se cumple, todo el cronograma se corre. La spec lo reconoce como riesgo (§14.1).

### Checklist piloto AGOGO (CRM Esencial, 2 usuarios, sin envíos)

- [ ] PRE-04 Cobrar Pedido en POS
- [ ] Número AGOGO verificado en Meta Cloud API (**no Baileys**)
- [ ] Teléfonos de clientes normalizados a E.164
- [ ] Fotos de productos cargadas
- [ ] Fechas comerciales cargadas
- [ ] 2 usuarios con perfil vendedor/atención; `envios_habilitados = false`
- [ ] Métricas: % ventas WhatsApp facturadas desde el CRM · respuesta < 15 min · conversión · recompra

### Checklist piloto DOBASA (CRM Pro)

- [ ] PRE-07 incidencias ERP cerradas
- [ ] Pipeline mayorista configurado
- [ ] Bandeja WhatsApp + email del equipo comercial
- [ ] Categorías de reclamo: faltante, vencido, devolución, precio, facturación
- [ ] Supervisor con `CRM_CONTRATOS_APROBAR`
- [ ] Métricas: SLA reclamos ≥ 90% · trazabilidad punta a punta

---

## 5. Smoke tests a crear (`package.json`)

```json
"test:smoke:crm-f1": "ts-node scripts/smoke/crm-f1.ts",
"test:smoke:crm-f2": "ts-node scripts/smoke/crm-f2.ts",
"test:smoke:crm-f3": "ts-node scripts/smoke/crm-f3.ts",
"test:smoke:crm-f4": "ts-node scripts/smoke/crm-f4.ts",
"test:smoke:crm-tenancy": "ts-node scripts/smoke/crm-tenancy.ts"
```

`crm-tenancy` recorre todos los GET de `/crm/*` con token de empresa B y verifica que no
aparezcan IDs creados por empresa A.

---

## 6. Riesgos técnicos

| Riesgo | Mitigación |
|---|---|
| Verificación Meta tarda | PRE-05 en semana 1; desarrollar con Evolution (número de prueba) |
| Bug de Prisma con locale español (visto en Órdenes de Venta) | Correr migraciones con `LC_ALL=C` / `LANG=en_US.UTF-8` |
| Doble emisión recurrente con varias instancias PM2 | Lock Redis por empresa + UNIQUE en ciclos |
| Rechazos SIFEN en lote | Reutilizar la cola con reintentos; ciclo `rechazado_sifen` visible en la bandeja |
| Volumen de webhooks | Responder 200 inmediato y procesar en cola `crm-mensajeria` |
| Prompt injection en el agente IA | Herramientas read-only, filtros `empresa_id`, contenido del cliente como dato |
| **Schema ya grande** (385 modelos, 10.516 líneas, 346 migraciones) | El CRM suma ~30 tablas (+8%). Vigilar tiempos de `prisma generate` y build |

---

**Versión:** 1.1 (corregida contra el código) · **Estado:** Fase 0 pendiente
