- GET /api/contactos/:id/detalle: todas las columnas del contacto, la propiedad comprada/en proceso (apartado relacionado por teléfono/email, enriquecido con el desarrollo de la unidad), quién atiende cada item relacionado (leads/oportunidades/apartados, columna people) y el historial completo de atención (activity log de los 4 tableros + capturas y alta local con nombre del capturista) - monday.service: getContactoDetalle (columnas por título), getHistorialItems (todos los eventos del activity log por items) y ApartadoMonday enriquecido (teléfono, email, precio final, fecha compra) - Landing: la lista de Contactos abre un modal por cliente con datos, propiedad, relaciones CRM (con quién atiende) y timeline de historial - Tests: 465 API + 12 landing, verde
122 lines
4.8 KiB
TypeScript
122 lines
4.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { claveTelefono, detalleContacto } from './contactos-detalle.service.js'
|
|
import type { LeadMonday, OportunidadMonday, ApartadoMonday } from './monday.service.js'
|
|
|
|
const MI_ID = 106736727
|
|
|
|
const leadBase: LeadMonday = {
|
|
id: 'l1', nombre: 'Ana Demo', createdAt: '', updatedAt: '', estado: 'Nuevo', origen: 'PASE',
|
|
telefono: '+52 6641234567', email: 'ana@correo.com', vendedorIds: [MI_ID],
|
|
}
|
|
|
|
const opBase: OportunidadMonday = {
|
|
id: 'o1', nombre: 'Ana Demo', createdAt: '', updatedAt: '', etapa: 'Cotizador', unidadCodigo: '',
|
|
precioFinal: 1500000, asesorIds: [MI_ID], telefono: '6641234567', email: '', origen: 'PASE',
|
|
desarrollo: 'SENDA', modeloInteres: 'SOHO',
|
|
}
|
|
|
|
const apBase: ApartadoMonday = {
|
|
id: 'a1', nombre: 'Ana Demo', unidadCodigo: 'SENDA-409-1-2', fechaLimite: '2026-09-10',
|
|
estado: 'Pago Apartado', vendedorIds: [MI_ID], telefono: '6641234567', email: '',
|
|
precioFinal: 2646000, fechaCompra: '2026-09-01',
|
|
}
|
|
|
|
function mondayMock(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
async getContactoDetalle() {
|
|
return { nombre: 'Ana Demo', Teléfono: '+52 6641234567', Email: 'ana@correo.com', Estado: 'CLIENTE', CURP: 'XAXX010101XXXXXXX0' }
|
|
},
|
|
async getLeads() { return [leadBase] },
|
|
async getOportunidades() { return [opBase] },
|
|
async getApartados() { return [apBase] },
|
|
async getHistorialItems(_b: string, ids: string[]) {
|
|
return ids.map((id, i) => ({
|
|
itemId: id,
|
|
fechaISO: `2026-09-0${(i % 9) + 1}T10:00:00.000Z`,
|
|
usuarioId: MI_ID,
|
|
evento: 'update_column_value',
|
|
}))
|
|
},
|
|
async getUnidad() { return { desarrollo: 'SENDA' } },
|
|
...overrides,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any
|
|
}
|
|
|
|
const nombres = new Map([[MI_ID, 'Martin Zavala']])
|
|
|
|
describe('claveTelefono', () => {
|
|
it('últimos 10 dígitos, tolerante a lada, espacios y símbolos', () => {
|
|
expect(claveTelefono('+52 664 123 4567')).toBe('6641234567')
|
|
expect(claveTelefono('+16195550134')).toBe('6195550134')
|
|
expect(claveTelefono('6641234567')).toBe('6641234567')
|
|
expect(claveTelefono('')).toBe('')
|
|
expect(claveTelefono('123')).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('detalleContacto (GET /api/contactos/:id/detalle)', () => {
|
|
it('arma contacto + propiedad + relaciones + historial con nombres resueltos', async () => {
|
|
const d = await detalleContacto(
|
|
mondayMock(),
|
|
'c1',
|
|
nombres,
|
|
[{ leadId: 'l1', nombreLead: 'Ana Demo', createdAt: new Date('2026-09-02T08:00:00Z'), usuario: { nombre: 'Martin Zavala' } }],
|
|
{
|
|
id: 'cl1', nombreCompleto: 'Ana Demo', proyecto: 'SENDA', unidadCodigo: 'SENDA-409-1-2',
|
|
createdAt: new Date('2026-09-03T08:00:00Z'), capturadoPor: { nombre: 'Martin Zavala' },
|
|
},
|
|
)
|
|
expect(d).not.toBeNull()
|
|
expect(d!.contacto['CURP']).toBe('XAXX010101XXXXXXX0')
|
|
// Propiedad comprada (apartado relacionado, enriquecido con el desarrollo)
|
|
expect(d!.propiedad).toMatchObject({
|
|
unidadCodigo: 'SENDA-409-1-2',
|
|
desarrollo: 'SENDA',
|
|
estado: 'Pago Apartado',
|
|
precioFinal: 2646000,
|
|
atendidoPor: 'Martin Zavala',
|
|
})
|
|
// Relaciones por teléfono/email con quién atiende
|
|
expect(d!.relaciones.leads[0]).toMatchObject({ id: 'l1', atendidoPor: 'Martin Zavala' })
|
|
expect(d!.relaciones.oportunidades[0]).toMatchObject({ id: 'o1', atendidoPor: 'Martin Zavala' })
|
|
expect(d!.relaciones.apartados[0]).toMatchObject({ id: 'a1', atendidoPor: 'Martin Zavala' })
|
|
// Historial: activity log + captura + alta local, ordenado desc
|
|
expect(d!.historial.some((h) => h.fuente === 'App' && h.detalle.includes('Captura'))).toBe(true)
|
|
expect(d!.historial.some((h) => h.detalle.includes('Alta de cliente'))).toBe(true)
|
|
expect(d!.historial.every((h) => h.usuario === 'Martin Zavala')).toBe(true)
|
|
const fechas = d!.historial.map((h) => h.fechaISO)
|
|
expect(fechas).toEqual([...fechas].sort((a, b) => b.localeCompare(a)))
|
|
})
|
|
|
|
it('sin relaciones ni registros locales: listas vacías y propiedad null', async () => {
|
|
const d = await detalleContacto(
|
|
mondayMock({
|
|
async getLeads() { return [] },
|
|
async getOportunidades() { return [] },
|
|
async getApartados() { return [] },
|
|
}),
|
|
'c1',
|
|
new Map(),
|
|
[],
|
|
null,
|
|
)
|
|
expect(d!.propiedad).toBeNull()
|
|
expect(d!.relaciones).toEqual({ leads: [], oportunidades: [], apartados: [] })
|
|
// El historial al menos tiene los eventos del propio contacto
|
|
expect(d!.historial).toHaveLength(1)
|
|
expect(d!.historial[0]?.fuente).toBe('Contactos')
|
|
})
|
|
|
|
it('contacto inexistente devuelve null', async () => {
|
|
const d = await detalleContacto(
|
|
mondayMock({ async getContactoDetalle() { return null } }),
|
|
'no-existe',
|
|
new Map(),
|
|
[],
|
|
null,
|
|
)
|
|
expect(d).toBeNull()
|
|
})
|
|
})
|