feat(funnel): leads entran como Afluencia al funnel de Oportunidades

- Captura rápida crea lead + oportunidad en etapa Afluencia (vinculadas
  por nueva tabla LeadOportunidad); también aplica a leads creados
  directo en Monday vía webhook
- Transiciones automáticas: AGENDA CITA → Agendo Cita, descarte →
  Perdido (+ dropdown Motivo No Compra), cotización con mismo teléfono
  ACTUALIZA la oportunidad (no duplica) → Cotizador, alta de cliente
  con leadMondayId → Aparto (dedup previo para no duplicar Apartado)
- Columna Desarrollo agregada al board Oportunidades (color_mm6sa20t)
- Migración lead_oportunidad aplicada con migrate diff + resolve (la DB
  tiene drift histórico por db push; migrate dev pedía reset)
- Tests: 322 verdes; smoke en vivo verificado y limpiado
This commit is contained in:
2026-09-01 20:10:51 +00:00
parent ead7a71025
commit 98fedbd8d3
14 changed files with 854 additions and 53 deletions

View File

@@ -36,6 +36,26 @@ Notas técnicas (quirks de Monday, ya contemplados en el código):
- Monday no firma sus webhooks: el servidor usa el evento solo como disparador
y re-consulta el tablero por API antes de escribir nada.
### 0.1 La cadena del funnel: Leads → Oportunidades
Los leads **no** se cargan como clientes directo: cada lead capturado entra al
tablero **Oportunidades** en etapa **Afluencia** y la oportunidad avanza sola
conforme avanza el proceso. La vinculación lead ↔ oportunidad vive en la tabla
`LeadOportunidad` de la base local (una oportunidad por lead; idempotente).
| Transición | Evento que la dispara |
|---|---|
| → **Afluencia** | `POST /api/leads` (captura) o webhook `create_pulse` en Leads (alta directa en la UI de Monday): crea la oportunidad con nombre, teléfono, email y Desarrollo del lead |
| → **Agendo Cita** | `POST /api/leads/:id/estado` con estado `AGENDA CITA` |
| → **Cotizador** | `POST /api/cotizaciones` con un teléfono que ya tiene lead mapeado: **actualiza** la misma oportunidad (Unidad, Tipo propuesta, precios, Fecha cotización) en vez de crear una segunda |
| → **Aparto** | `POST /api/clientes` con `leadMondayId` en el payload (app alta-clientes). Se escribe el dedup `apartado-creado:<opId>` **antes** del cambio de etapa para que el webhook/cron no duplique el Apartado |
| → **Ganado** | Manual en Monday (o flujo Enkontrol): el escaneo crea el item en Ventas |
| → **Perdido** | `POST /api/leads/:id/descartar`: mueve la oportunidad y, si el motivo es label del dropdown **Motivo No Compra**, lo setea también |
Reglas de resiliencia: un fallo de Monday en cualquiera de estas transiciones
solo se loguea — la acción principal (crear el lead, la cotización o el alta)
nunca se rompe por el funnel.
Para re-registrar un webhook (p. ej. si cambia el dominio):
```graphql