14 servicios eliminados, 7 controllers local-only, crons locales, config sin MONDAY_*, frontend sin banners/avisos Monday. 552 tests API + 7 apps construidas.
253 lines
7.5 KiB
TypeScript
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,
|
|
})
|
|
})
|
|
})
|