feat: modulo WhatsApp (WA CRM) con round-robin de conversaciones

- Servicio wacrm en api-gateway (pg pool lazy): round-robin de conversaciones
  sin agente entre vendedores vinculados y miembro-agentes de la cuenta wacrm,
  cron cada 30 s; bandeja del vendedor; estado/config/reasignar para admin.
- Endpoints /api/wacrm/* con guarda de rol admin/manager.
- Usuario.wacrmUserId en schema Prisma (vinculo CRM<->wacrm).
- App apps/wa-crm (/wa-crm/): bandeja + administracion del round-robin.
- Landing: tarjeta WhatsApp; clave 'whatsapp' en MATRIZ de roles.
- Plan de nuevas reglas de negocio del cotizador (docs/PLAN-REGLAS-COTIZADOR.md).
- WACRM_PG_URL apunta a Postgres de Supabase por 127.0.0.1:5433.
- Tests de integracion wacrm (3) y suite completa 764/764 en verde.
This commit is contained in:
2026-09-30 00:30:17 +00:00
parent cb1c36b669
commit e3b5660769
28 changed files with 5744 additions and 1 deletions

54
docs/MODULO-WHATSAPP.md Normal file
View File

@@ -0,0 +1,54 @@
# Módulo WhatsApp (WA CRM) — CRM360 FRASA
Integración entre el CRM y [wacrm](https://wacrm.tech) (WhatsApp CRM
self-hosted en `/root/wacrm`, datos en Supabase self-hosted `/root/supabase`).
## Cómo funciona
- **Vínculo de usuarios**: cada `Usuario` del CRM tiene `wacrmUserId` (id de
`auth.users` en Supabase). Los 30 usuarios activos del CRM están dados de
alta en wacrm y su perfil pertenece a la **cuenta compartida**
(`7b17a9ab-6723-43aa-a9da-28e97f0071ae`, del usuario administrador).
Vendedores = rol `agent`, managers/admins = rol `admin`.
- **Round-robin**: el API Gateway (`frasa-api`) revisa cada 30 s las
conversaciones de la cuenta compartida que llegan sin asignar
(`assigned_agent_id IS NULL`, estatus `open`) y las turna entre los
vendedores del pool (rol `vendedor`, activos, vinculados y miembros agente
de la cuenta). El índice del turno vive en `ConfiguracionSistema`
(`wacrm_rr_idx`), así que sobrevive reinicios.
- **Bandeja del vendedor**: `GET /api/wacrm/mi-bandeja` devuelve las
conversaciones asignadas al usuario logueado (contacto, último mensaje,
no leídos). La app `apps/wa-crm` (`/wa-crm/`) las muestra con liga a
`/wa/inbox` para responder desde wacrm.
- **Administración** (rol admin/manager, pestaña en `/wa-crm/`):
- Estado: sin asignar, total, pool con "es miembro" y cuántas tiene cada uno.
- Activar/desactivar round-robin y cambiar la cuenta (`POST /api/wacrm/config`).
- "Asignar ahora" dispara el reparto a demanda (`POST /api/wacrm/round-robin`).
- Reasignar una conversación a cualquier vendedor (`POST /api/wacrm/reasignar`).
## Conexión técnica
- `WACRM_PG_URL` en `/root/frasa-ecosystem/.env` apunta al Postgres de
Supabase por `127.0.0.1:5433` (puerto publicado solo en localhost del
contenedor `supabase-db`; el 5432 es Supavisor y exige tenant).
- El servicio `api-gateway/src/services/wacrm.service.ts` usa `pg` (Pool lazy;
sin URL lanza 503 y el resto del CRM sigue igual).
- Al escribir (`UPDATE conversations`) se hace como superusuario postgres,
bypass RLS — solo el API lo hace, nunca el cliente.
## Puesta en marcha del número
El round-robin está activo sobre la cuenta compartida. Falta conectar el
número de WhatsApp (API oficial de Meta) en wacrm: credenciales y pasos en
`/root/wacrm/ENV-PENDIENTES.md`. Cuando Meta entregue el número, conectarlo
desde **wacrm → Settings → WhatsApp** con la cuenta del administrador.
## Tests
`api-gateway/src/services/wacrm.service.test.ts` — integración real contra
Supabase (se auto-salta si no hay `WACRM_PG_URL`); crea usuarios/cuenta/
conversaciones de prueba y los limpia. Correr:
```bash
cd api-gateway && npx vitest run src/services/wacrm.service.test.ts
```

View File

@@ -0,0 +1,125 @@
# Plan — Nuevas reglas de negocio del Cotizador FRASA
Fuente: `Reglas de negocio Cotizador FRASA .xlsx` (entregado por el cliente).
Alcance: aplicar estas reglas al cotizador web (`apps/cotizador`) y a su espejo
del lado del API (`api-gateway/src/services/cotizacion-esquema.service.ts`).
## 1. Lectura de las reglas (interpretación)
### Globales (todos los tipos)
| Concepto | Regla |
|---|---|
| Apartado | $15,000 (cantidad fija, ya confirmada por el cliente) |
| Gastos de escrituración | 5.5 % del precio de venta |
| Gastos — opción del cliente | Puede **incluirlos** en el pago de firma (toggle). Si los incluye, se liquidan junto con el enganche para firma de contrato |
| Mensualidades a diferir | Número de **meses calendario completos** entre la fecha de cotización y la fecha de entrega de la vivienda (ya implementado vía captura) |
| Mensualidad a pagar | Enganche diferido ÷ mensualidades |
| Enganche total | El **% total que el cliente elige pagar** como enganche (incluye el apartado). Tope por tipo de crédito |
### Tipos de crédito
| Tipo | Rango enganche **para firma** | Tope enganche **total** | Aforo | Notaría |
|---|---|---|---|---|
| Bancario | 2.5 % – 15 % | 15 % | 84.5 % | No aplica |
| Infonavit-Cofinavit | 2.5 % – 15 % | 15 % | 89.5 % | No aplica |
| Semi Contado | 50 % – 85 % | 85 % | No aplica | Debe quedar **15 %** para notaría |
| Contado | 86 % – 95 % | 95 % | No aplica | Debe quedar **mínimo 5 %** para notaría |
### Fórmulas
```
gastos = 0.055 × precio
firma = % enganche firma elegido (dentro del rango del tipo)
totalEng = % enganche total elegido (≤ tope del tipo; firma ≤ totalEng)
diferido = totalEng − firma (lo que se difiere en mensualidades)
mensualidad = diferido × precio ÷ N_meses
saldoPorPagar = precio + gastos − (firma% + diferido%) × precio
(= precio + gastos − totalEng% × precio)
```
- **Bancario / Infonavit-Cofinavit**: `monto a financiar = saldoPorPagar`.
Validación de **aforo**: `saldoPorPagar ≤ aforo × precio`; si se rebasa, la
combinación elegida no es viable y hay que pedir más enganche.
- **Semi Contado**: no hay financiamiento. Restricción: el saldo por pagar debe
dejar **≥ 15 %** para el pago en notaría (`saldoPorPagar ≥ 0.15 × precio`,
equivalente a `totalEng ≤ 0.85 + gastos%`). El "monto pago en notaría" = 15 %.
- **Contado**: sin mensualidades (diferido = 0; todo el enganche se paga a la
firma). Restricción: saldo ≥ 5 % para notaría (`totalEng ≤ 0.95 + gastos%`).
### Diferencias contra el esquema actual
| Aspecto | Esquema actual (FRASA) | Nuevo |
|---|---|---|
| Enganche firma | fijo 2.5 % | rango por tipo (2.5–15 / 50–85 / 86–95 %) |
| Enganche diferible | fijo 7.5 % editable | **total elegido − firma** |
| Tope enganche | 90 % global | por tipo: 15 / 15 / 85 / 95 % |
| Aforo | no se valida | validación 84.5 / 89.5 % (bancario/infonavit) |
| Gastos 5.5 % | siempre aparte | **toggle**: incluidos en firma u aparte |
| Notaría | no existe | piso 15 % (semi) / 5 % (contado) |
| Anticipo adicional | campo extra (decisión previa: sin espacio separado) | se absorbe en "enganche total" |
## 2. Puntos a confirmar con el cliente (antes o durante la implementación)
1. **Apartado dentro del %**: se interpreta que el % de enganche total es
dinero total (el apartado de $15,000 forma parte de él), según la columna
K ("% de enganche elegido incluido el apartado"). Confirmar.
2. **Semi Contado / Contado**: ¿el "pago en notaría" (15 % / 5 %) es parte del
saldo por pagar que el cliente liquida al final? Se asume que sí.
3. **Aforo**: si el financiamiento calculado supera el aforo, ¿bloquear la
opción o mostrarla en rojo como "no viable"? Se propone mostrar no viable.
4. **Rango de enganche para firma en créditos**: ¿el 2.5 % mínimo sigue
incluyendo el apartado como parte del pago a firma (como hoy)? Se propone
que sí.
## 3. Plan de implementación
### 3.1 Motor de cálculo — `esquema-frasa.ts` (app) y `cotizacion-esquema.service.ts` (API, espejo)
1. Nuevo tipo `TipoCredito = 'bancario' | 'infonavit' | 'contado' | 'semicontado'`
(reemplaza/usa el catálogo actual de opciones de crédito).
2. Tabla de reglas por tipo (única fuente de verdad en ambos lados, misma
constante): `REGLAS_TIPO` con `{rangoFirmaMin, rangoFirmaMax, topeTotal,
aforo, pisoNotaria, permiteDiferir}`.
3. Entrada del motor: `{precio, apartado, tipo, pctFirma, pctTotal, incluirGastosEnFirma, meses}`.
4. Salida: `{gastos, montoFirma, montoDiferido, mensualidad, saldoPorPagar,
montoFinanciar, pagoNotaria, viable, motivos[]}` con validaciones:
- firma dentro de rango; total ≤ tope; total ≥ firma;
- si diferido > 0 → meses ≥ 1 (tope por fecha de entrega, ya existente);
- aforo (bancario/infonavit): `saldo ≤ aforo × precio`;
- piso notaría (semi/contado): `saldo ≥ piso × precio`.
5. Quitar del esquema actual lo que queda reemplazado: enganche fijo 2.5/7.5,
tope global 90 %, `engancheAdicional` (el "total elegido" lo absorbe).
### 3.2 UI del cotizador (`apps/cotizador`)
1. Selector de **tipo de crédito** con las 4 opciones y sus topes visibles.
2. **Slider/input de enganche total** (0 → tope del tipo) y **slider/input de
enganche a firma** (rango del tipo, ≤ total).
3. **Toggle "Incluir gastos de escrituración en el pago de firma"**.
4. Tabla de resultados: apartado, firma, diferido, mensualidad (×N meses),
gastos, saldo, monto a financiar (o pago en notaría), y badge **viable /
no viable** con motivo (aforo excedido, piso notaría, etc.).
5. Las 4 opciones de crédito del guardado existente pasan a ser las 4
combinaciones tipo+porcentajes que el vendedor guarde, no 4 presets fijos.
### 3.3 Persistencia y reportes
1. `Cotizacion`: columnas nuevas `tipoCredito`, `pctFirma`, `pctTotal`,
`incluirGastosFirma` (migración SQLite manual, estilo existente). Mantener
`opcionesCredito` JSON para compatibilidad de lectura histórica.
2. PDF: sección de desglose según nuevo esquema (firma / diferido / notaría /
financiamiento, viable o no).
### 3.4 Pruebas
- Unitarias del motor: casos dorados por tipo (límites de rango, tope, aforo
justo arriba/abajo, pisos de notaría, toggle de gastos).
- Smoke API: `POST` cotización con cada tipo → verificar montos y `viable`.
- Regresión: cotizaciones históricas existentes siguen leyéndose.
### 3.5 Entrega
- Migración + build de `api-gateway` y `apps/cotizador`.
- Actualización de guía de usuario del cotizador (sección reglas).
- Resumen de cambios para el cliente con los puntos 2 resueltos.