Files
FRASA-CAS/api-gateway/src/services/buscar.service.test.ts
Consult AS Technology 8a2584438d fix(auditoria general): 25 errores encontrados en la revision modulo por modulo
SEGURIDAD (agente IA):
- Conversaciones: GET /ai/conversations/:id y el reanudar chat verifican
  propiedad (antes cualquier usuario leia/continuaba la conversacion de otro).
- /ai/documents pasa a solo-admin (antes cualquien podia inyectar o borrar
  documentos de la base de conocimiento).

DATOS / CORRUPCION:
- Cotizador: elegir un cliente del maestro en el buscador ya no crea un lead
  fantasma "Registro previo (CRM anterior)" en el embudo.
- Afluencia con oportunidadId inexistente: 404 en vez de crear sombra fantasma.
- Cobranza: los 103 planes importados quedaron con vendedor asignado (7 con su
  asesor real segun el Excel, el resto al admin) y el motor de recordatorios
  cae al admin cuando un plan no tiene vendedor: antes los 411 recordatorios
  se registraban 'sin-destinatario' y no los veia nadie.
- Backups: script reescrito para el host real (sqlite .backup + uploads + env
  en /root/backups) y cron instalado (antes: script que asumia docker y sin
  cron programado = cero respaldos).

REPORTES / METRICAS:
- Mi tablero: el embudo ya excluye cancelaciones reales (Cancelo/Cancelacion
  Apartado) y cuenta como vendidos Firma Cliente/Escriturado/Titulado (antes
  41 "apartados" donde habia 13 activos y vendidos buscaba una etapa que no
  existe); tasa de descarte ya compara el mismo periodo.
- Scoring: etapa real 'Agendo Cita' (antes 'AGENDO' nunca aplicaba) y los
  descartados/perdidos ya no reciben score.
- ver_cobranza (IA): ignora planes cancelados y no cuenta como vencido el pago
  que vence hoy.

UI / FLUJOS:
- mi-dia: typo CANCO -> CANCELO (los cancelados reaparecen en Escrituracion);
  confirmar cita ya no retrocede la etapa de Cotizador/Aparto (boton y kanban).
- alta-clientes: la firma ya no se borra al navegar fuera del paso de firmas;
  toast de exito solo cuando el API confirma; prefill de apartado trae
  direccion, estado civil y fecha de nacimiento desde el alta.
- Auditoria: el filtro por usuario funciona (el front enviaba 'usuario' y el
  API esperaba 'usuarioId') y 'hasta' incluye todo el dia.
- Contactos: paginacion aplicada despues de fusionar fuentes (antes saltaban o
  se repetian contactos entre paginas y 'Siguiente' se deshabilitaba antes de
  tiempo).
- Buscador global: leads -> /hoy/, contactos -> /; el cotizador entiende
  ?cotizacion=<id> para retomar desde la liga.
- Export mi-dia: columnas Desarrollo/Origen rellenas y sin duplicados.
- Recibo de cobranza: "Recibio" es FRASA (el cliente paga, no recibe).
- Alta de plan de cobranza: proyecto libre con sugerencias del catalogo real
  (antes limitado a 3 proyectos que no cubrian la cartera).

Verificado en vivo: autorizacion IA (3x 403), filtro de auditoria, 404 de
afluencia. Suites: 717 API + 124 alta-clientes + 115 cotizador + 92 cobranza +
89 landing + 32 mi-dia en verde. Backup de hoy generado en /root/backups.
2026-09-24 10:54:09 +00:00

164 lines
7.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
// ---------- Prisma mockeado ----------
const prismaMock = vi.hoisted(() => ({
leadCaptura: { findMany: vi.fn() },
cliente: { findMany: vi.fn() },
cotizacion: { findMany: vi.fn() },
unidad: { findMany: vi.fn() },
}))
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
import { buscar } from './buscar.service.js'
beforeEach(() => {
vi.clearAllMocks()
prismaMock.leadCaptura.findMany.mockResolvedValue([])
prismaMock.cliente.findMany.mockResolvedValue([])
prismaMock.cotizacion.findMany.mockResolvedValue([])
prismaMock.unidad.findMany.mockResolvedValue([])
})
describe('buscar (GET /api/buscar)', () => {
it('término vacío retorna [] sin consultar la base', async () => {
const r = await buscar('')
expect(r).toEqual([])
expect(prismaMock.leadCaptura.findMany).not.toHaveBeenCalled()
})
it('término con solo espacios también retorna []', async () => {
const r = await buscar(' ')
expect(r).toEqual([])
expect(prismaMock.leadCaptura.findMany).not.toHaveBeenCalled()
})
it('consulta los 4 modelos con el patrón contains', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
{ leadId: 'l1', nombreLead: 'María López', telefono: '4771112233', email: null, origen: 'Facebook', board: 'LEADS', updatedAt: new Date() },
])
prismaMock.cliente.findMany.mockResolvedValue([
{ id: 'c1', nombreCompleto: 'María López', curp: '', rfc: '', email: 'maria@x.com', celular: '4771112233', proyecto: 'DIAGONAL', createdAt: new Date() },
])
prismaMock.cotizacion.findMany.mockResolvedValue([])
prismaMock.unidad.findMany.mockResolvedValue([])
const r = await buscar('María')
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalledTimes(1)
expect(prismaMock.cliente.findMany).toHaveBeenCalledTimes(1)
expect(prismaMock.cotizacion.findMany).toHaveBeenCalledTimes(1)
expect(prismaMock.unidad.findMany).toHaveBeenCalledTimes(1)
expect(r.length).toBe(2)
expect(r[0]?.tipo).toBe('lead')
expect(r[1]?.tipo).toBe('contacto')
})
it('aplica take: limite por modelo', async () => {
await buscar('test', 5)
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalledWith(
expect.objectContaining({ take: 5 }),
)
})
it('prioriza leads sobre contactos, contactos sobre oportunidades, oportunidades sobre unidades', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
{ leadId: 'l1', nombreLead: 'X', telefono: null, email: null, origen: null, board: 'LEADS', updatedAt: new Date() },
])
prismaMock.cliente.findMany.mockResolvedValue([
{ id: 'c1', nombreCompleto: 'X', curp: '', rfc: '', email: null, celular: null, proyecto: '', createdAt: new Date() },
])
prismaMock.cotizacion.findMany.mockResolvedValue([
{ id: 'ct1', clienteNombre: 'X', clienteEmail: null, unidadCodigo: null, desarrollo: null, tipoPropuesta: null, createdAt: new Date() },
])
prismaMock.unidad.findMany.mockResolvedValue([
{ id: 'u1', codigo: 'X', desarrollo: 'SENDA', modelo: null, status: null, updatedAt: new Date() },
])
const r = await buscar('X')
expect(r.map((i) => i.tipo)).toEqual(['lead', 'contacto', 'oportunidad', 'unidad'])
})
it('mezcla los resultados y limita al total indicado', async () => {
const leads = Array.from({ length: 10 }, (_, i) => ({
leadId: `l${i}`, nombreLead: `L${i}`, telefono: null, email: null, origen: null, board: 'LEADS', updatedAt: new Date(),
}))
const clientes = Array.from({ length: 10 }, (_, i) => ({
id: `c${i}`, nombreCompleto: `C${i}`, curp: '', rfc: '', email: null, celular: null, proyecto: '', createdAt: new Date(),
}))
prismaMock.leadCaptura.findMany.mockResolvedValue(leads)
prismaMock.cliente.findMany.mockResolvedValue(clientes)
const r = await buscar('x', 15)
expect(r).toHaveLength(15)
// Primeros 10 son leads (prioridad), siguientes 5 son contactos
expect(r.filter((i) => i.tipo === 'lead')).toHaveLength(10)
expect(r.filter((i) => i.tipo === 'contacto')).toHaveLength(5)
})
it('lead: detalle muestra teléfono · email, o origen como fallback', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
{ leadId: 'l1', nombreLead: 'Ana', telefono: '4771112233', email: 'ana@x.com', origen: null, board: 'LEADS', updatedAt: new Date() },
])
const r = await buscar('Ana')
expect(r[0]?.detalle).toBe('4771112233 · ana@x.com')
prismaMock.leadCaptura.findMany.mockResolvedValue([
{ leadId: 'l2', nombreLead: 'Bob', telefono: null, email: null, origen: 'Facebook', board: 'LEADS', updatedAt: new Date() },
])
const r2 = await buscar('Bob')
expect(r2[0]?.detalle).toBe('Facebook')
})
it('contacto: detalle muestra email · celular · proyecto', async () => {
prismaMock.cliente.findMany.mockResolvedValue([
{ id: 'c1', nombreCompleto: 'Luis', curp: '', rfc: '', email: 'luis@x.com', celular: '4772223333', proyecto: 'SENDA', createdAt: new Date() },
])
const r = await buscar('Luis')
expect(r[0]?.detalle).toBe('luis@x.com · 4772223333 · SENDA')
})
it('oportunidad: detalle muestra unidadCodigo · desarrollo, o tipoPropuesta como fallback', async () => {
prismaMock.cotizacion.findMany.mockResolvedValue([
{ id: 'ct1', clienteNombre: 'Carlos', clienteEmail: null, unidadCodigo: 'U-101', desarrollo: 'DIAGONAL', tipoPropuesta: null, createdAt: new Date() },
])
const r = await buscar('Carlos')
expect(r[0]?.detalle).toBe('U-101 · DIAGONAL')
prismaMock.cotizacion.findMany.mockResolvedValue([
{ id: 'ct2', clienteNombre: 'Diana', clienteEmail: null, unidadCodigo: null, desarrollo: null, tipoPropuesta: 'CONTADO', createdAt: new Date() },
])
const r2 = await buscar('Diana')
expect(r2[0]?.detalle).toBe('CONTADO')
})
it('unidad: detalle muestra desarrollo · modelo · status', async () => {
prismaMock.unidad.findMany.mockResolvedValue([
{ id: 'u1', codigo: 'U-202', desarrollo: 'ESTANZA', modelo: 'SOHO', status: 'Disponible', updatedAt: new Date() },
])
const r = await buscar('U-202')
expect(r[0]?.detalle).toBe('ESTANZA · SOHO · Disponible')
})
it('urls de navegación son correctas por tipo', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
{ leadId: 'l1', nombreLead: 'X', telefono: null, email: null, origen: null, board: 'LEADS', updatedAt: new Date() },
])
prismaMock.cliente.findMany.mockResolvedValue([
{ id: 'c1', nombreCompleto: 'X', curp: '', rfc: '', email: null, celular: null, proyecto: '', createdAt: new Date() },
])
prismaMock.cotizacion.findMany.mockResolvedValue([
{ id: 'ct1', clienteNombre: 'X', clienteEmail: null, unidadCodigo: null, desarrollo: null, tipoPropuesta: null, createdAt: new Date() },
])
prismaMock.unidad.findMany.mockResolvedValue([
{ id: 'u1', codigo: 'X', desarrollo: null, modelo: null, status: null, updatedAt: new Date() },
])
const r = await buscar('X')
expect(r.find((i) => i.tipo === 'lead')?.url).toBe('/hoy/')
expect(r.find((i) => i.tipo === 'contacto')?.url).toBe('/')
expect(r.find((i) => i.tipo === 'oportunidad')?.url).toBe('/quote/?cotizacion=ct1')
expect(r.find((i) => i.tipo === 'unidad')?.url).toBe('/quote/?unidad=X')
})
})