feat(contactos): detalle completo del cliente con propiedad e historial
- 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
This commit is contained in:
121
api-gateway/src/services/contactos-detalle.service.test.ts
Normal file
121
api-gateway/src/services/contactos-detalle.service.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user