Files
FRASA-CAS/apps/dashboards/src/lib/reportes.test.ts
Consult AS Technology 9e04b76616 feat(unidades): pipeline de 10 etapas por unidad + sync desde Reporte_EstatusVenta
- Columna Etapa en Monday Unidades (10 labels oficiales), 155 unidades pobladas
- sync-etapas.ts idempotente (--archivo/--dry-run/--semilla) con traspasos
  Vendido→Escrituración y Escriturado→SAC como actividades (sin retroactivas)
- API: etapa en unidades + porEtapa en /api/reportes/resumen
- Dashboard: gráfica pipeline por etapa + KPI en notaría; cotizador muestra etapa
- 28 cambios aplicados desde Reporte_EstatusVenta (8 avances, 20 nuevas);
  contradicciones retenidas para revisión humana (documentadas)
- Tests: api 168, dashboards 9, cotizador 71
2026-08-28 04:43:02 +00:00

134 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import {
datosCobranza,
datosFunnel,
datosPipeline,
formatoMoneda,
formatoPorcentaje,
unidadesApiladas,
unidadesEnEtapa,
type ResumenReportes,
} from './reportes'
function resumen(overrides: Partial<ResumenReportes> = {}): ResumenReportes {
return {
generadoEn: '2026-08-22T12:00:00.000Z',
leads: { total: 40, nuevosHoy: 3, nuevosSemana: 12, porEstado: {}, porOrigen: {} },
oportunidades: {
total: 20,
activas: 15,
porEtapa: { Afluencia: 8, Cotizador: 5, Aparto: 4, Ganado: 2, Perdido: 1 },
tasaAparto: 0.3,
tasaGanado: 0.1,
},
unidades: {
total: 5,
disponibles: 2,
porDesarrollo: [
{ desarrollo: 'ATELIER', total: 3, porStatus: { DISPONIBLE: 1, VENDIDO: 2 } },
{ desarrollo: 'SOLANA', total: 2, porStatus: { DISPONIBLE: 1, ESCRITURADO: 1 } },
],
porEtapa: [
{ etapa: 'Apartado', total: 1 },
{ etapa: 'Reserva', total: 0 },
{ etapa: 'Vendido', total: 0 },
{ etapa: 'Integración', total: 0 },
{ etapa: 'En Análisis', total: 0 },
{ etapa: 'Autorizado', total: 0 },
{ etapa: 'Notaría', total: 2 },
{ etapa: 'Escriturado', total: 1 },
{ etapa: 'Entrega', total: 0 },
{ etapa: 'PostVenta', total: 0 },
],
},
cobranza: {
planes: { activos: 6, liquidados: 2, cancelados: 1 },
planesAlCorriente: 4,
planesConVencidos: 2,
pagosVencidos: 3,
montoVencido: 125000,
},
actividadesAbiertas: 7,
...overrides,
}
}
describe('unidadesApiladas', () => {
it('mapea desarrollo × status con columnas fijas y agrupa lo desconocido en OTROS', () => {
expect(unidadesApiladas(resumen())).toEqual([
{ desarrollo: 'ATELIER', DISPONIBLE: 1, APARTADO: 0, VENDIDO: 2, BLOQUEADO: 0, OTROS: 0 },
{ desarrollo: 'SOLANA', DISPONIBLE: 1, APARTADO: 0, VENDIDO: 0, BLOQUEADO: 0, OTROS: 1 },
])
})
it('con inventario vacío devuelve arreglo vacío', () => {
expect(unidadesApiladas(resumen({ unidades: { total: 0, disponibles: 0, porDesarrollo: [], porEtapa: [] } }))).toEqual([])
})
})
describe('datosPipeline', () => {
it('devuelve las 10 etapas en el orden oficial con sus totales', () => {
const pipeline = datosPipeline(resumen())
expect(pipeline.map((e) => e.etapa)).toEqual([
'Apartado',
'Reserva',
'Vendido',
'Integración',
'En Análisis',
'Autorizado',
'Notaría',
'Escriturado',
'Entrega',
'PostVenta',
])
expect(pipeline.find((e) => e.etapa === 'Notaría')?.valor).toBe(2)
expect(pipeline.find((e) => e.etapa === 'Escriturado')?.valor).toBe(1)
})
})
describe('unidadesEnEtapa', () => {
it('devuelve el total de la etapa pedida y 0 si no aparece', () => {
expect(unidadesEnEtapa(resumen(), 'Notaría')).toBe(2)
expect(unidadesEnEtapa(resumen(), 'Reserva')).toBe(0)
expect(unidadesEnEtapa(resumen({ unidades: { total: 0, disponibles: 0, porDesarrollo: [], porEtapa: [] } }), 'Notaría')).toBe(0)
})
})
describe('datosFunnel', () => {
it('ordena Leads → Oportunidades → Aparto o más → Ganado sin subir', () => {
const funnel = datosFunnel(resumen())
expect(funnel.map((e) => e.etapa)).toEqual(['Leads', 'Oportunidades', 'Aparto o más', 'Ganado'])
expect(funnel.map((e) => e.valor)).toEqual([40, 20, 6, 2]) // Aparto(4) + Ganado(2) acumulados
const valores = funnel.map((e) => e.valor)
for (let i = 1; i < valores.length; i++) expect(valores[i]!).toBeLessThanOrEqual(valores[i - 1]!)
})
it('tolera etiquetas con otra capitalización', () => {
const r = resumen()
r.oportunidades.porEtapa = { aparto: 1, ' GANADO ': 3 }
const funnel = datosFunnel(r)
expect(funnel[2]?.valor).toBe(4)
expect(funnel[3]?.valor).toBe(3)
})
})
describe('datosCobranza', () => {
it('devuelve al corriente vs con vencidos', () => {
expect(datosCobranza(resumen())).toEqual([
{ nombre: 'Al corriente', valor: 4 },
{ nombre: 'Con vencidos', valor: 2 },
])
})
})
describe('formatos', () => {
it('moneda MXN sin centavos', () => {
expect(formatoMoneda(125000)).toBe('$125,000')
})
it('porcentaje desde fracción', () => {
expect(formatoPorcentaje(0.4)).toBe('40%')
expect(formatoPorcentaje(0.333)).toBe('33.3%')
})
})