feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol

This commit is contained in:
2026-09-21 08:34:53 +00:00
parent b5a20aed4e
commit 2fc80e7f9e
66 changed files with 3792 additions and 436 deletions

View File

@@ -118,6 +118,99 @@ describe('listarContactosLocal', () => {
expect(lista[0]?.id).toBe('c1')
})
it('deduplica por nombre a los registros sin teléfono ni email (gana la ficha con más datos)', async () => {
// Alta fallida: Cliente hueco (payload vacío, sin teléfono ni email) con el
// mismo nombre que su ficha real
prismaMock.cliente.findMany.mockResolvedValue([
clienteFake({
id: 'hueco',
nombreCompleto: 'LUIS ENRIQUE SALGADO ZAVALETA',
payloadJson: '{}',
createdAt: new Date('2026-09-11T10:00:00'),
}),
clienteFake({
id: 'completo',
nombreCompleto: 'Luis Enrique Salgado Zavaleta',
celular: '+52 5586880444',
email: 'luisenriquesalgadozavaleta@gmail.com',
curp: 'SAZL990504HDFLVS06',
rfc: 'SAZL990504KY0',
proyecto: 'SOLANA',
unidadCodigo: '9',
payloadJson: '{"datosGenerales":{"estadoCivil":"SOLTERO"}}',
}),
])
const lista = (await listarContactosLocal()).contactos
expect(lista).toHaveLength(1)
expect(lista[0]).toMatchObject({
id: 'completo',
nombre: 'Luis Enrique Salgado Zavaleta',
telefono: '+52 5586880444',
email: 'luisenriquesalgadozavaleta@gmail.com',
estado: 'Cliente',
})
})
it('fusiona el registro hueco aunque llegue después de la ficha completa', async () => {
prismaMock.cliente.findMany.mockResolvedValue([
clienteFake({
id: 'completo',
nombreCompleto: 'GRACIELA BAUTISTA VELASCO',
celular: '+52 6641159092',
email: 'gbautista1304@gmail.com',
}),
clienteFake({ id: 'hueco', nombreCompleto: 'Graciela Bautista Velasco', payloadJson: '{}' }),
])
const lista = (await listarContactosLocal()).contactos
expect(lista).toHaveLength(1)
expect(lista[0]?.id).toBe('completo')
})
it('no fusiona a dos personas distintas con el mismo nombre si tienen teléfonos distintos', async () => {
prismaMock.cliente.findMany.mockResolvedValue([
clienteFake({ id: 'c1', nombreCompleto: 'JUAN PÉREZ', celular: '4771234567' }),
clienteFake({ id: 'c2', nombreCompleto: 'JUAN PÉREZ', celular: '6649998877' }),
])
const lista = (await listarContactosLocal()).contactos
expect(lista.map((c) => c.id).sort()).toEqual(['c1', 'c2'])
})
it('un Cliente hueco se fusiona con la captura del mismo nombre (gana la que trae más datos)', async () => {
prismaMock.cliente.findMany.mockResolvedValue([
clienteFake({
id: 'hueco',
nombreCompleto: 'Guadalupe',
curp: '',
rfc: '',
payloadJson: '{}',
createdAt: new Date('2026-09-15T10:00:00'),
}),
])
prismaMock.leadCaptura.findMany.mockResolvedValue([
capturaFake({
leadId: 'loc_g',
nombreLead: 'Guadalupe',
telefono: '+52 664 222 6090',
email: 'lupisintj1@hotmail.com',
etapa: 'Contactado',
}),
])
const lista = (await listarContactosLocal()).contactos
expect(lista).toHaveLength(1)
expect(lista[0]).toMatchObject({
id: 'loc_g',
telefono: '+52 664 222 6090',
email: 'lupisintj1@hotmail.com',
estado: 'Contactado',
})
})
it('pagina de 100 en 100 con hayMas (la página 2 trae el siguiente bloque)', async () => {
// 101 clientes: la página 1 llena 100 y detecta que hay más
prismaMock.cliente.findMany.mockResolvedValue(
@@ -233,4 +326,110 @@ describe('detalleContactoLocal', () => {
expect(detalle.relaciones.oportunidades).toHaveLength(1)
expect(detalle.historial.length).toBeGreaterThan(0)
})
it('completa el perfil con el payloadJson del alta (estado civil, nacimiento, actividad, dirección)', async () => {
prismaMock.cliente.findFirst.mockResolvedValue(
clienteFake({
id: 'c1',
nombreCompleto: 'LUIS ENRIQUE SALGADO ZAVALETA',
celular: '5586880444',
email: 'luis@correo.mx',
curp: '',
rfc: '',
payloadJson: JSON.stringify({
datosGenerales: {
estadoCivil: 'SOLTERO',
fechaNacimiento: '1999-05-04',
actividadOcupacion: 'PILOTO AVIADOR',
direccion: 'C.SAN FELIPE EJIDO FRANCISCO VILLA',
},
empleo: { compania: 'FRASA', direccion: 'AV COLINA DE SAN PABLO' },
// El co-acreditado repite los campos vacíos: no debe pisar los reales
coacreditado: { tieneCoacreditado: false, generales: { estadoCivil: '', fechaNacimiento: '', actividadOcupacion: '', direccion: '' } },
}),
}),
)
const detalle = await detalleContactoLocal('c1')
expect(detalle?.contacto).toMatchObject({
estadoCivil: 'SOLTERO',
fechaNacimiento: '1999-05-04',
actividadEconomica: 'PILOTO AVIADOR',
direccion: 'C.SAN FELIPE EJIDO FRANCISCO VILLA',
})
})
it('toma el perfilamiento del datosJson de la captura cuando no hay Cliente', async () => {
const captura = capturaFake({
leadId: 'loc_1',
nombreLead: 'RAMON MORALES',
telefono: '6643535353',
email: 'ramon.morales@gmail.com',
datosJson: JSON.stringify({
edad: 41,
estadoCivil: 'Casado(a)',
ocupacion: 'empleado',
rangoSalarial: '50000',
}),
})
prismaMock.leadCaptura.findUnique.mockResolvedValue(captura)
prismaMock.leadCaptura.findMany.mockResolvedValue([captura])
const detalle = await detalleContactoLocal('loc_1')
expect(detalle?.contacto).toMatchObject({
nombre: 'RAMON MORALES',
estadoCivil: 'Casado(a)',
actividadEconomica: 'empleado',
})
// No se inventa fecha de nacimiento a partir de la edad
expect(detalle?.contacto['fechaNacimiento']).toBe('')
expect(detalle?.contacto['direccion']).toBe('')
})
it('incluye los documentos del registro hueco de la misma persona (no se pierden al fusionar)', async () => {
prismaMock.cliente.findFirst.mockResolvedValue(
clienteFake({
id: 'c1',
nombreCompleto: 'DEYSI LORENA ALEMAN FIGUEROA',
celular: '6643998451',
email: 'daleman@frasa.com.mx',
}),
)
prismaMock.cliente.findMany.mockResolvedValue([
{ id: 'hueco', nombreCompleto: 'DEYSI LORENA ALEMAN FIGUEROA', celular: null, email: null },
{ id: 'otro', nombreCompleto: 'OTRA PERSONA', celular: null, email: null },
])
prismaMock.documento.findMany.mockResolvedValue([
{ id: 'd1', tipo: 'INE', createdAt: new Date('2026-09-01T12:00:00') },
])
const detalle = await detalleContactoLocal('c1')
expect(prismaMock.documento.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { clienteId: { in: ['c1', 'hueco'] } } }),
)
expect(detalle?.historial.some((h) => h.detalle === 'Documento subido (INE)')).toBe(true)
})
it('si la ficha visible es la captura, también se muestran los documentos de su Cliente hueco', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue(
capturaFake({ leadId: 'loc_g', nombreLead: 'Guadalupe', telefono: '6642226090', etapa: 'Contactado' }),
)
prismaMock.leadCaptura.findMany.mockResolvedValue([
capturaFake({ leadId: 'loc_g', nombreLead: 'Guadalupe', telefono: '6642226090', etapa: 'Contactado' }),
])
prismaMock.cliente.findMany.mockResolvedValue([
{ id: 'hueco', nombreCompleto: 'Guadalupe', celular: null, email: null },
])
prismaMock.documento.findMany.mockResolvedValue([
{ id: 'd1', tipo: 'INE', createdAt: new Date('2026-09-01T12:00:00') },
])
const detalle = await detalleContactoLocal('loc_g')
expect(prismaMock.documento.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { clienteId: { in: ['hueco'] } } }),
)
expect(detalle?.historial.some((h) => h.detalle === 'Documento subido (INE)')).toBe(true)
})
})