Files
FRASA-CAS/api-gateway/src/services/reportes-local.test.ts
Consult AS Technology 89d95eb53b feat(purga): Monday eliminado completamente del sistema
14 servicios eliminados, 7 controllers local-only, crons locales,
config sin MONDAY_*, frontend sin banners/avisos Monday.
552 tests API + 7 apps construidas.
2026-09-11 11:47:17 +00:00

253 lines
7.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { PlanPago } from './cobranza.service.js'
import type { LeadFila, OportunidadFila, Unidad } from './reportes.service.js'
// ---------- Prisma mockeado (los tests no tocan la BD) ----------
const prismaMock = vi.hoisted(() => ({
leadCaptura: {
findMany: vi.fn(),
},
unidad: {
findMany: vi.fn(),
},
actividadLocal: {
count: vi.fn(),
},
}))
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
import { cargarDatosLocalesPrisma, ReportesService, type DatosLocalesReportes } from './reportes.service.js'
/**
* Reportes 100% locales: el resumen se calcula desde tablas locales
* (LeadCaptura por board, Unidad por status, PlanPago como siempre).
* `parcial: false` porque los datos locales son completos.
*/
function pago(id: string, fechaVencimiento: string, monto: number, montoPagado = 0): PlanPago['pagos'][number] {
return { id, numPago: 1, fechaVencimiento, monto, montoPagado, aplicaciones: [] }
}
function plan(id: string, overrides: Partial<PlanPago> = {}): PlanPago {
return {
id,
clienteNombre: `Cliente ${id}`,
unidadCodigo: 'U-101',
proyecto: 'ATELIER',
fechaInicio: '2026-07-01',
engancheTotal: 100000,
numPagos: 2,
mensualidad: 50000,
status: 'ACTIVO',
pagos: [pago(`p-${id}`, '2027-01-01', 50000)], // pendiente y al corriente: ACTIVO
notas: [],
createdAt: '2026-07-01T00:00:00.000Z',
...overrides,
}
}
function leadFila(id: string, estado = 'Nuevo'): LeadFila {
return {
id,
nombre: `Lead ${id}`,
createdAt: '2026-09-04T10:00:00.000Z',
updatedAt: '2026-09-04T10:00:00.000Z',
estado,
origen: 'Web',
telefono: '',
email: '',
vendedorIds: [],
}
}
function unidadFila(codigo: string, status = 'DISPONIBLE'): Unidad {
return {
codigo,
modelo: '1R',
fechaEntrega: null,
supEstacionamiento: 0,
supConstruccion: 60,
supExcedente: 0,
precioSinAcabados: 1_000_000,
precioConAcabados: 1_100_000,
status,
desarrollo: 'SENDA',
mesesParaFirma: null,
etapa: null,
}
}
const datosLocales: DatosLocalesReportes = {
leads: [leadFila('loc_l1'), leadFila('loc_l2', 'Descartado')],
oportunidades: [
{
id: 'loc_o1',
nombre: 'Op Local',
createdAt: '2026-09-04T10:00:00.000Z',
updatedAt: '2026-09-04T10:00:00.000Z',
etapa: 'Afluencia',
unidadCodigo: '',
precioFinal: 0,
asesorIds: [],
telefono: '',
email: '',
origen: 'WhatsApp',
desarrollo: 'SENDA',
modeloInteres: '1R',
credito: 'Infonavit',
},
],
unidades: [unidadFila('U-1'), unidadFila('U-2', 'APARTADO')],
}
beforeEach(() => {
vi.clearAllMocks()
prismaMock.leadCaptura.findMany.mockResolvedValue([])
prismaMock.unidad.findMany.mockResolvedValue([])
prismaMock.actividadLocal.count.mockResolvedValue(0)
})
describe('ReportesService (resumen desde tablas locales)', () => {
it('resumen desde datos locales: leads, oportunidades, unidades y planes en una respuesta', async () => {
const svc = new ReportesService(async () => [plan('a')], {
cargarDatosLocales: async () => datosLocales,
})
const r = await svc.resumen()
expect(r.parcial).toBe(false)
expect(r.leads.total).toBe(2)
expect(r.leads.porEstado).toEqual({ Nuevo: 1, Descartado: 1 })
expect(r.oportunidades.total).toBe(1)
expect(r.oportunidades.porEtapa).toEqual({ Afluencia: 1 })
expect(r.unidades.total).toBe(2)
expect(r.unidades.disponibles).toBe(1)
expect(r.cobranza.planes.activos).toBe(1)
expect(r.actividadesAbiertas).toBe(0) // no hay actividades en el fixture
})
it('cuenta actividades abiertas locales cuando el fixture las trae', async () => {
const svc = new ReportesService(async () => [], {
cargarDatosLocales: async () => ({ ...datosLocales, actividadesAbiertas: 3 }),
})
const r = await svc.resumen()
expect(r.actividadesAbiertas).toBe(3)
})
it('usa el cargador local por defecto (cargarDatosLocalesPrisma) cuando no se inyecta', async () => {
const svc = new ReportesService(async () => [])
const r = await svc.resumen()
// Con las tablas vacías el resumen sale en ceros pero completo
expect(r.parcial).toBe(false)
expect(r.leads.total).toBe(0)
expect(r.oportunidades.total).toBe(0)
expect(r.unidades.total).toBe(0)
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalled()
expect(prismaMock.unidad.findMany).toHaveBeenCalled()
expect(prismaMock.actividadLocal.count).toHaveBeenCalled()
})
it('un error del cargador local se propaga (no se traga nada)', async () => {
const svc = new ReportesService(async () => [], {
cargarDatosLocales: async () => {
throw new Error('bug')
},
})
await expect(svc.resumen()).rejects.toThrow('bug')
})
})
describe('cargarDatosLocalesPrisma (mapeo LeadCaptura/Unidad → DTOs del embudo)', () => {
it('mapea boards a leads/oportunidades con el perfil de datosJson y unidades al DTO', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
{
leadId: 'loc_l1',
nombreLead: 'Lead Local',
telefono: '4771234567',
email: null,
origen: 'Web',
desarrollo: null,
modelo: null,
board: 'LEADS',
etapa: 'NUEVO',
datosJson: null,
createdAt: new Date('2026-09-04T10:00:00'),
updatedAt: new Date('2026-09-04T11:00:00'),
},
{
leadId: 'loc_o1',
nombreLead: 'Op Local',
telefono: null,
email: 'op@correo.mx',
origen: 'WhatsApp',
desarrollo: 'SENDA',
modelo: '1R',
board: 'OPORTUNIDADES',
etapa: 'Afluencia',
datosJson: JSON.stringify({ estadoCivil: 'Soltero(a)', tipoEmpleo: 'Empleado', credito: 'Infonavit', edad: 35, califica: 'Si' }),
createdAt: new Date('2026-09-03T10:00:00'),
updatedAt: new Date('2026-09-03T11:00:00'),
},
{
leadId: 'loc_a1',
nombreLead: 'Apt Local',
board: 'APARTADO', // el board APARTADO no alimenta leads ni oportunidades
etapa: 'Pago Apartado',
telefono: null,
email: null,
origen: null,
desarrollo: null,
modelo: null,
datosJson: null,
createdAt: new Date('2026-09-02T10:00:00'),
updatedAt: new Date('2026-09-02T11:00:00'),
},
])
prismaMock.unidad.findMany.mockResolvedValue([
{
codigo: 'SENDA-101',
desarrollo: 'SENDA',
modelo: '1R',
supConstruccion: 69.5,
supExcedente: 0,
supEstacionamiento: 12.5,
precioLista: 4_415_000,
precioAcabados: 4_579_000,
status: 'DISPONIBLE',
etapa: '',
},
])
const datos = await cargarDatosLocalesPrisma()
expect(datos.leads).toHaveLength(1)
expect(datos.leads[0]).toMatchObject({ id: 'loc_l1', estado: 'NUEVO', origen: 'Web', telefono: '4771234567' })
expect(datos.oportunidades).toHaveLength(1)
expect(datos.oportunidades[0]).toMatchObject({
id: 'loc_o1',
etapa: 'Afluencia',
desarrollo: 'SENDA',
modeloInteres: '1R',
estadoCivil: 'Soltero(a)',
tipoEmpleo: 'Empleado',
credito: 'Infonavit',
edad: 35,
califica: 'Si',
})
expect(datos.unidades).toHaveLength(1)
expect(datos.unidades[0]).toMatchObject({
codigo: 'SENDA-101',
precioSinAcabados: 4_415_000,
precioConAcabados: 4_579_000,
status: 'DISPONIBLE',
etapa: null,
})
})
})