Files
FRASA-CAS/api-gateway/src/services/reporte-mi-tablero.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

182 lines
6.8 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest'
import { construirMiTablero } from './reporte-mi-tablero.service.js'
// ---------- Fixtures ----------
function captura(
overrides: Partial<{
leadId: string
board: string
etapa: string
origen: string | null
desarrollo: string | null
createdAt: Date
updatedAt: Date
datosJson: string | null
}> = {},
) {
return {
leadId: overrides.leadId ?? `cap-${Math.random().toString(36).slice(2, 8)}`,
board: overrides.board ?? 'LEADS',
etapa: overrides.etapa ?? 'NUEVO',
origen: overrides.origen ?? 'Facebook',
desarrollo: overrides.desarrollo ?? 'DIAGONAL',
createdAt: overrides.createdAt ?? new Date('2026-09-05T10:00:00'),
updatedAt: overrides.updatedAt ?? overrides.createdAt ?? new Date('2026-09-05T10:00:00'),
datosJson: overrides.datosJson ?? null,
}
}
function cotizacion(
overrides: Partial<{ precioFinal: number; vendedorId: string | null; createdAt: Date }> = {},
) {
return {
precioFinal: overrides.precioFinal ?? 850000,
vendedorId: overrides.vendedorId ?? 'u-1',
createdAt: overrides.createdAt ?? new Date('2026-09-01T12:00:00'),
}
}
function actividad(
overrides: Partial<{ usuarioId: string; estado: string; fechaVencimiento: Date | null }> = {},
) {
return {
usuarioId: overrides.usuarioId ?? 'u-1',
estado: overrides.estado ?? 'ABIERTA',
fechaVencimiento: overrides.fechaVencimiento ?? null,
}
}
const AHORA = new Date('2026-09-07T12:00:00')
// ---------- construirMiTablero (puro) ----------
describe('construirMiTablero', () => {
it('genera 30 entradas en leadsPorDia con ceros para días sin leads', () => {
const tablero = construirMiTablero([], [], [], AHORA)
expect(tablero.leadsPorDia.length).toBe(30)
// Todos los días deben tener cantidad 0
expect(tablero.leadsPorDia.every((d) => d.cantidad === 0)).toBe(true)
})
it('cuenta leads por día correctamente', () => {
const hace2Dias = new Date(AHORA.getTime() - 2 * 24 * 60 * 60 * 1000)
const capturas = [
captura({ createdAt: hace2Dias }),
captura({ createdAt: hace2Dias }),
captura({ createdAt: hace2Dias }),
]
const tablero = construirMiTablero(capturas, [], [], AHORA)
const entrada = tablero.leadsPorDia.find((d) => d.cantidad > 0)
expect(entrada).toBeDefined()
expect(entrada!.cantidad).toBe(3)
})
it('cuenta conversión correctamente', () => {
const capturas = [
captura({ board: 'LEADS' }),
captura({ board: 'LEADS' }),
captura({ board: 'OPORTUNIDADES' }),
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE' }),
captura({ board: 'APARTADO', etapa: 'CANCELO' }),
captura({ board: 'APARTADO', etapa: 'ESCRITURADO' }),
captura({ board: 'LEADS', etapa: 'DESCARTADO' }),
]
const tablero = construirMiTablero(capturas, [], [], AHORA)
expect(tablero.conversion.leads).toBe(3) // 2 activos + 1 descartado
expect(tablero.conversion.oportunidades).toBe(1)
// Cancelo no cuenta; Firma Cliente y Escriturado ya son ventas
expect(tablero.conversion.apartados).toBe(0)
expect(tablero.conversion.vendidos).toBe(2)
expect(tablero.conversion.descartados).toBe(1) // etapa terminal DESCARTADO
})
it('calcula ventas del mes con ventas CERRADAS (no cotizaciones)', () => {
// Las cotizaciones ya no cuentan como ventas: se cuentan los apartados con
// firma de cliente o posteriores, sin repetir la misma unidad.
const cots = [
cotizacion({ precioFinal: 850000, createdAt: new Date('2026-09-01T12:00:00') }),
]
const capturas = [
captura({
leadId: 'loc_apt_1',
board: 'APARTADO',
etapa: 'Firma Cliente',
updatedAt: new Date('2026-09-10T12:00:00'),
datosJson: JSON.stringify({ precioFinal: 1_500_000 }),
}),
captura({
leadId: 'loc_apt_2',
board: 'APARTADO',
etapa: 'Venta',
updatedAt: new Date('2026-09-12T12:00:00'),
datosJson: JSON.stringify({ precioFinal: 2_000_000 }),
}),
// Mismo apartado recotizado: no debe duplicarse (mismo leadId)
captura({
leadId: 'loc_apt_1',
board: 'APARTADO',
etapa: 'Firma Cliente',
updatedAt: new Date('2026-09-15T12:00:00'),
datosJson: JSON.stringify({ precioFinal: 1_500_000 }),
}),
captura({ board: 'APARTADO', etapa: 'Pago Apartado', updatedAt: new Date('2026-09-11T12:00:00') }),
captura({ board: 'APARTADO', etapa: 'Cancelacion Apartado', updatedAt: new Date('2026-09-11T12:00:00') }),
]
const tablero = construirMiTablero(capturas, cots, [], AHORA)
expect(tablero.ventasMes.unidades).toBe(2)
expect(tablero.ventasMes.monto).toBe(3_500_000)
})
it('agrupa por origen con conteo de convertidos', () => {
const capturas = [
captura({ board: 'LEADS', origen: 'Facebook' }),
captura({ board: 'LEADS', origen: 'Facebook' }),
captura({ board: 'OPORTUNIDADES', origen: 'Facebook' }),
captura({ board: 'LEADS', origen: 'Referido' }),
]
const tablero = construirMiTablero(capturas, [], [], AHORA)
const fb = tablero.porOrigen.find((o) => o.origen === 'Facebook')
expect(fb).toBeDefined()
expect(fb!.total).toBe(3)
expect(fb!.convertidos).toBe(1)
const ref = tablero.porOrigen.find((o) => o.origen === 'Referido')
expect(ref).toBeDefined()
expect(ref!.total).toBe(1)
expect(ref!.convertidos).toBe(0)
})
it('agrupa por desarrollo solo con vendidos', () => {
const capturas = [
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE', desarrollo: 'DIAGONAL' }),
captura({ board: 'APARTADO', etapa: 'ESCRITURADO', desarrollo: 'DIAGONAL' }),
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE', desarrollo: 'ATELIER' }),
captura({ board: 'APARTADO', etapa: 'NUEVO', desarrollo: 'SENDA' }), // no vendido
]
const tablero = construirMiTablero(capturas, [], [], AHORA)
expect(tablero.porDesarrollo).toEqual([
{ desarrollo: 'DIAGONAL', ventas: 2 },
{ desarrollo: 'ATELIER', ventas: 1 },
])
})
it('cuenta actividades hoy y pendientes', () => {
const actividades = [
actividad({ estado: 'ABIERTA', fechaVencimiento: new Date('2026-09-07T10:00:00') }),
actividad({ estado: 'ABIERTA', fechaVencimiento: new Date('2026-09-08T10:00:00') }),
actividad({ estado: 'ABIERTA' }),
actividad({ estado: 'CERRADA' }),
]
const tablero = construirMiTablero([], [], actividades, AHORA)
expect(tablero.actividadesHoy).toBe(2) // 1 vencida + 1 sin fecha (hoy)
expect(tablero.actividadesPendientes).toBe(3) // 3 ABIERTA
})
it('normaliza orígenes vacíos a (sin origen)', () => {
const capturas = [captura({ board: 'LEADS', origen: '' })]
const tablero = construirMiTablero(capturas, [], [], AHORA)
expect(tablero.porOrigen.length).toBe(1)
expect(tablero.porOrigen[0].origen).toBe('(sin origen)')
})
})