Files
FRASA-CAS/api-gateway/src/services/contactos-detalle.service.ts
Consult AS Technology 89d95eb53b feat(purga): Monday eliminado completamente del sistema
14 servicios eliminados, 7 controllers local-only, crons locales,
config sin MONDAY_*, frontend sin banners/avisos Monday.
552 tests API + 7 apps construidas.
2026-09-11 11:47:17 +00:00

214 lines
5.8 KiB
TypeScript

import { prisma } from './prisma.js'
import { calcularScore } from './lead-scoring.service.js'
/**
* Perfil Completo del Contacto: datos locales del cliente/lead, historial de
* pagos, cotizaciones, documentos, lead score y última actividad.
*/
export interface RelacionCrm {
id: string
nombre: string
estado: string // estado/etapa actual
/** Quién lo atiende ahora; null si sin asignar */
atendidoPor: string | null
detalle: string // origen/desarrollo/unidad según el tablero ('' si no aplica)
}
export interface EventoHistorialContacto {
fechaISO: string
usuario: string | null
detalle: string
fuente: string
}
export interface DetalleContacto {
contacto: Record<string, string>
propiedad: {
id: string
unidadCodigo: string
desarrollo: string
estado: string
precioFinal: number
fechaCompra: string
fechaLimite: string
atendidoPor: string | null
} | null
relaciones: {
leads: RelacionCrm[]
oportunidades: RelacionCrm[]
apartados: RelacionCrm[]
}
historial: EventoHistorialContacto[]
}
/** Últimos 10 dígitos del teléfono (dedup/relación tolerante a lada y formato) */
export function claveTelefono(telefono: string): string {
const digitos = telefono.replace(/\D/g, '')
return digitos.length >= 10 ? digitos.slice(-10) : ''
}
// ── Perfil Completo del Contacto (modo local) ─────────────────────────
export interface PerfilCompletoContacto {
contacto: Record<string, string>
historialPagos: Array<{
planId: string
clienteNombre: string
unidadCodigo: string
proyecto: string
numPago: number
fechaVencimiento: string
monto: number
montoPagado: number
estado: string
}>
cotizaciones: Array<{
id: string
unidadCodigo: string
tipoPropuesta: string
precioFinal: number
createdAt: string
}>
documentos: Array<{
id: string
tipo: string
nombreArchivo: string
createdAt: string
}>
leadScore: number | null
ultimaActividad: {
titulo: string
fecha: string
tipo: string
} | null
}
/**
* Perfil completo de un contacto: combina datos del cliente/lead, historial
* de pagos, cotizaciones, documentos, lead score y última actividad local.
*/
export async function perfilCompleto(contactoId: string): Promise<PerfilCompletoContacto | null> {
// Buscar el cliente
const cliente = await prisma.cliente.findFirst({
where: { OR: [{ id: contactoId }, { mondayContactoId: contactoId }] }, // mondayContactoId: compatibilidad de datos
})
// Buscar la captura del lead
const captura = await prisma.leadCaptura.findUnique({ where: { leadId: contactoId } })
if (!cliente && !captura) return null
const telefono = cliente?.celular ?? captura?.telefono ?? ''
const email = (cliente?.email ?? captura?.email ?? '').trim().toLowerCase()
const nombre = cliente?.nombreCompleto ?? captura?.nombreLead ?? 'Sin nombre'
// Contacto fields
const contacto: Record<string, string> = {
nombre,
Teléfono: telefono,
Email: email,
}
if (cliente) {
contacto['CURP'] = cliente.curp
contacto['RFC'] = cliente.rfc
contacto['Proyecto'] = cliente.proyecto
}
if (captura) {
contacto['Etapa'] = captura.etapa
contacto['Origen'] = captura.origen ?? ''
}
// Historial de pagos (por teléfono o nombre del cliente)
const historialPagos: PerfilCompletoContacto['historialPagos'] = []
if (cliente) {
const planes = await prisma.planPago.findMany({
where: { clienteNombre: cliente.nombreCompleto },
include: { pagos: true },
})
for (const plan of planes) {
for (const pago of plan.pagos) {
const restante = pago.monto - pago.montoPagado
const estado = restante <= 0.01 ? 'pagado' : 'pendiente'
historialPagos.push({
planId: plan.id,
clienteNombre: plan.clienteNombre,
unidadCodigo: plan.unidadCodigo,
proyecto: plan.proyecto,
numPago: pago.numPago,
fechaVencimiento: pago.fechaVencimiento.toISOString().slice(0, 10),
monto: pago.monto,
montoPagado: pago.montoPagado,
estado,
})
}
}
}
// Cotizaciones (por teléfono)
const clave = cliente?.celular?.replace(/\D/g, '').slice(-10) ?? ''
const cotizacionesRaw = clave !== ''
? await prisma.cotizacion.findMany({
where: { clienteTelefono: { contains: clave } },
orderBy: { createdAt: 'desc' },
})
: []
// Documentos
const documentosRaw = cliente
? await prisma.documento.findMany({
where: { clienteId: cliente.id },
orderBy: { createdAt: 'desc' },
})
: []
// Lead score
let leadScore: number | null = null
if (captura) {
leadScore = calcularScore({
etapa: captura.etapa,
telefono: captura.telefono,
email: captura.email,
origen: captura.origen,
createdAt: captura.createdAt,
updatedAt: captura.updatedAt,
})
}
// Última actividad
let ultimaActividad: PerfilCompletoContacto['ultimaActividad'] = null
if (captura) {
const ultima = await prisma.actividadLocal.findFirst({
where: { leadCapturaId: captura.leadId },
orderBy: { createdAt: 'desc' },
})
if (ultima) {
ultimaActividad = {
titulo: ultima.titulo,
fecha: ultima.createdAt.toISOString(),
tipo: ultima.tipo,
}
}
}
return {
contacto,
historialPagos,
cotizaciones: cotizacionesRaw.map((c) => ({
id: c.id,
unidadCodigo: c.unidadCodigo,
tipoPropuesta: c.tipoPropuesta,
precioFinal: c.precioFinal,
createdAt: c.createdAt.toISOString(),
})),
documentos: documentosRaw.map((d) => ({
id: d.id,
tipo: d.tipo,
nombreArchivo: d.nombreArchivo,
createdAt: d.createdAt.toISOString(),
})),
leadScore,
ultimaActividad,
}
}