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

View File

@@ -0,0 +1,67 @@
/**
* Endpoints del módulo WhatsApp (WA CRM): bandeja del vendedor, estado del
* round-robin para administración, configuración y reasignación manual.
*/
import type { NextFunction, Request, Response } from 'express'
import { z } from 'zod'
import { AppError } from '../utils/errors.js'
import {
configurarWacrm,
estadoWacrm,
ejecutarRoundRobinWacrm,
miBandeja,
reasignarConversacion,
} from '../services/wacrm.service.js'
import * as auditoria from '../services/auditoria.service.js'
/** Admin/manager: estado, configuración y reasignación. */
export function requireRolWacrmAdmin(req: Request, _res: Response, next: NextFunction): void {
const rol = req.usuarioToken?.rol
if (rol !== 'admin' && rol !== 'manager') {
throw new AppError(403, 'Requiere rol de administrador')
}
next()
}
/** Bandeja de WhatsApp del vendedor logueado (vínculo vía wacrmUserId). */
export async function miBandejaEndpoint(req: Request, res: Response): Promise<void> {
const usuarioId = req.usuarioToken?.sub
if (!usuarioId) throw new AppError(401, 'Sesión requerida')
const resultado = await miBandeja(usuarioId)
res.json(resultado)
}
export async function estadoWacrmEndpoint(_req: Request, res: Response): Promise<void> {
res.json(await estadoWacrm())
}
const configSchema = z
.object({
cuentaId: z.string().max(64).optional(),
activo: z.boolean().optional(),
})
.refine((o) => Object.keys(o).length > 0, { message: 'Nada que actualizar' })
export async function configurarWacrmEndpoint(req: Request, res: Response): Promise<void> {
const body = configSchema.parse(req.body)
const resultado = await configurarWacrm(body)
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'CONFIG_WACRM', 'configuracion', 'wacrm', JSON.stringify(body))
res.json(resultado)
}
const reasignarSchema = z.object({
conversacionId: z.string().min(10),
vendedorId: z.string().min(10),
})
export async function reasignarWacrmEndpoint(req: Request, res: Response): Promise<void> {
const body = reasignarSchema.parse(req.body)
await reasignarConversacion(body)
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'REASIGNAR_WACRM', 'conversacion', body.conversacionId, body.vendedorId)
res.json({ ok: true })
}
/** Dispara el round-robin a demanda (admin) — útil para pruebas. */
export async function roundRobinWacrmEndpoint(_req: Request, res: Response): Promise<void> {
res.json(await ejecutarRoundRobinWacrm())
}