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.
This commit is contained in:
2026-09-11 11:47:17 +00:00
parent 64b01246d6
commit 89d95eb53b
130 changed files with 2018 additions and 14747 deletions

View File

@@ -1,38 +1,29 @@
import { prisma } from './prisma.js'
import { calcularScore } from './lead-scoring.service.js'
import { config } from '../config/index.js'
import { detalleEventoUltimoCambio, esEstadoFinalApartado } from './mi-dia.service.js'
import type { MondayService } from './monday.service.js'
/**
* Detalle del módulo Contactos: toda la información del contacto, la propiedad
* comprada (apartado relacionado), quién lo atiende (asesor/vendedor actual de
* cada item relacionado) y el historial completo de atención.
*
* Las relaciones se arman por teléfono (últimos 10 dígitos) o email a través de
* los tableros Leads, Oportunidades y Apartado; el historial mezcla el activity
* log de Monday de esos items + el registro local (quién capturó qué en las apps).
* 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 (columna people del tablero); null si sin asignar */
/** 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 // nombre resuelto; null si fue automatización o desconocido
usuario: string | null
detalle: string
fuente: string // 'Contactos' | 'Leads' | 'Oportunidades' | 'Apartado' | 'App'
fuente: string
}
export interface DetalleContacto {
contacto: Record<string, string> // campos nombrados del modelo local (nombre, telefono, email, …)
/** Apartado relacionado más reciente (la propiedad comprada o en proceso) */
contacto: Record<string, string>
propiedad: {
id: string
unidadCodigo: string
@@ -57,173 +48,6 @@ export function claveTelefono(telefono: string): string {
return digitos.length >= 10 ? digitos.slice(-10) : ''
}
/**
* Mapeo título de columna del board Contactos (Monday) → campo nombrado del
* modelo local. Los títulos sin entrada se conservan tal cual.
*/
const CAMPOS_CONTACTO: Record<string, string> = {
nombre: 'nombre',
'Teléfono': 'telefono',
Email: 'email',
Estado: 'estado',
CURP: 'curp',
RFC: 'rfc',
'Estado Civil': 'estadoCivil',
'Fecha Nacimiento': 'fechaNacimiento',
'Actividad Economica': 'actividadEconomica',
'Dirección': 'direccion',
}
/** Normaliza un record por título de columna a campos nombrados del modelo local */
export function normalizarCamposContacto(contacto: Record<string, string>): Record<string, string> {
const normalizado: Record<string, string> = {}
for (const [titulo, valor] of Object.entries(contacto)) {
normalizado[CAMPOS_CONTACTO[titulo] ?? titulo] = valor
}
return normalizado
}
type MondayDetalle = Pick<
MondayService,
| 'getContactoDetalle'
| 'getLeads'
| 'getOportunidades'
| 'getApartados'
| 'getHistorialItems'
| 'getUnidad'
>
export interface CapturaLocal {
leadId: string
nombreLead: string
createdAt: Date
usuario: { nombre: string } | null
}
export interface ClienteLocal {
id: string
nombreCompleto: string
proyecto: string
unidadCodigo: string | null
createdAt: Date
capturadoPor: { nombre: string } | null
_count?: { documentos: number }
}
export async function detalleContacto(
monday: MondayDetalle,
contactoId: string,
nombresPorMondayId: Map<number, string>,
capturas: CapturaLocal[],
clienteLocal: ClienteLocal | null,
): Promise<DetalleContacto | null> {
const contacto = await monday.getContactoDetalle(contactoId)
if (contacto === null) return null
const nombre = (id: number | null): string | null =>
id !== null ? (nombresPorMondayId.get(id) ?? null) : null
const tel = claveTelefono(contacto['Teléfono'] ?? '')
const email = (contacto['Email'] ?? '').trim().toLowerCase()
const coincide = (t: string, e: string): boolean =>
(tel !== '' && claveTelefono(t) === tel) || (email !== '' && e.trim().toLowerCase() === email)
const [leads, oportunidades, apartados] = await Promise.all([
monday.getLeads(),
monday.getOportunidades(),
monday.getApartados(),
])
const relLeads = leads.filter((l) => coincide(l.telefono, l.email))
const relOportunidades = oportunidades.filter((o) => coincide(o.telefono, o.email))
const relApartados = apartados.filter((a) => coincide(a.telefono ?? '', a.email ?? ''))
// Propiedad: el apartado relacionado "vivo" (no final) o el último del board;
// se enriquece con el desarrollo de la unidad (inventario)
let propiedad: DetalleContacto['propiedad'] = null
const aptoReciente = relApartados.find((a) => !esEstadoFinalApartado(a.estado)) ?? relApartados[relApartados.length - 1]
if (aptoReciente) {
let desarrollo = ''
if (aptoReciente.unidadCodigo !== '') {
try {
desarrollo = (await monday.getUnidad(aptoReciente.unidadCodigo))?.desarrollo ?? ''
} catch {
desarrollo = ''
}
}
propiedad = {
id: aptoReciente.id,
unidadCodigo: aptoReciente.unidadCodigo,
desarrollo,
estado: aptoReciente.estado,
precioFinal: aptoReciente.precioFinal ?? 0,
fechaCompra: aptoReciente.fechaCompra ?? '',
fechaLimite: aptoReciente.fechaLimite,
atendidoPor: nombre(aptoReciente.vendedorIds[0] ?? null),
}
}
// Historial: activity log de los items relacionados + del propio contacto,
// más los registros locales de captura/alta (Monday no guarda quién capturó)
const [evContactos, evLeads, evOportunidades, evApartado] = await Promise.all([
monday.getHistorialItems(config.monday.boards.contactos, [contactoId]),
monday.getHistorialItems(config.monday.boards.leads, relLeads.map((l) => l.id)),
monday.getHistorialItems(config.monday.boards.oportunidades, relOportunidades.map((o) => o.id)),
monday.getHistorialItems(config.monday.boards.apartado, relApartados.map((a) => a.id)),
])
const historial: EventoHistorialContacto[] = [
...evContactos.map((e) => ({ fechaISO: e.fechaISO, usuario: nombre(e.usuarioId), detalle: detalleEventoUltimoCambio(e.evento), fuente: 'Contactos' })),
...evLeads.map((e) => ({ fechaISO: e.fechaISO, usuario: nombre(e.usuarioId), detalle: detalleEventoUltimoCambio(e.evento), fuente: 'Leads' })),
...evOportunidades.map((e) => ({ fechaISO: e.fechaISO, usuario: nombre(e.usuarioId), detalle: detalleEventoUltimoCambio(e.evento), fuente: 'Oportunidades' })),
...evApartado.map((e) => ({ fechaISO: e.fechaISO, usuario: nombre(e.usuarioId), detalle: detalleEventoUltimoCambio(e.evento), fuente: 'Apartado' })),
...capturas.map((c) => ({
fechaISO: c.createdAt.toISOString(),
usuario: c.usuario?.nombre ?? null,
detalle: `Captura en la app (${c.nombreLead})`,
fuente: 'App',
})),
...(clienteLocal
? [{
fechaISO: clienteLocal.createdAt.toISOString(),
usuario: clienteLocal.capturadoPor?.nombre ?? null,
detalle: `Alta de cliente registrada${clienteLocal.unidadCodigo ? ` (${clienteLocal.unidadCodigo})` : ''}`,
fuente: 'App',
}]
: []),
].sort((a, b) => b.fechaISO.localeCompare(a.fechaISO))
return {
contacto: normalizarCamposContacto(contacto),
propiedad,
relaciones: {
leads: relLeads.map((l) => ({
id: l.id,
nombre: l.nombre,
estado: l.estado,
atendidoPor: nombre(l.vendedorIds[0] ?? null),
detalle: l.origen,
})),
oportunidades: relOportunidades.map((o) => ({
id: o.id,
nombre: o.nombre,
estado: o.etapa,
atendidoPor: nombre(o.asesorIds[0] ?? null),
detalle: [o.desarrollo, o.modeloInteres].filter((p) => p).join(' · '),
})),
apartados: relApartados.map((a) => ({
id: a.id,
nombre: a.nombre,
estado: a.estado,
atendidoPor: nombre(a.vendedorIds[0] ?? null),
detalle: a.unidadCodigo,
})),
},
historial,
}
}
// ── Perfil Completo del Contacto (modo local) ─────────────────────────
export interface PerfilCompletoContacto {
@@ -267,7 +91,7 @@ export interface PerfilCompletoContacto {
export async function perfilCompleto(contactoId: string): Promise<PerfilCompletoContacto | null> {
// Buscar el cliente
const cliente = await prisma.cliente.findFirst({
where: { OR: [{ id: contactoId }, { mondayContactoId: contactoId }] },
where: { OR: [{ id: contactoId }, { mondayContactoId: contactoId }] }, // mondayContactoId: compatibilidad de datos
})
// Buscar la captura del lead