Files
FRASA-CAS/api-gateway/scripts/descartar-duplicados.ts
Consult AS Technology ea03e6eeba chore: limpieza de 26 registros duplicados (motivo Duplicado)
Post-fix del dedup telefonoNorm: descarte de los leads/oportunidades/
apartados duplicados acumulados antes del 26/09. Regla: conservar la
cadena mas avanzada por telefono; el resto queda con etapa terminal y
motivo 'Duplicado' (sin borrar, con entrada de auditoria). Script
idempotente con validacion de board+etapa esperados.
2026-09-26 21:37:07 +00:00

131 lines
7.0 KiB
TypeScript

/**
* Limpieza de leads/oportunidades/apartados duplicados (septiembre 2026).
*
* Los duplicados se originaron ANTES del fix del dedup normalizado
* (telefonoNorm, 2026-09-26): la búsqueda `telefono contains` no encontraba
* teléfonos guardados con espacios y dejaba pasar capturas repetidas.
*
* Regla aplicada: conservar la cadena MÁS AVANZADA de cada teléfono
* (APARTADO > OPORTUNIDADES > LEADS; dentro del mismo tablero, la etapa más
* avanzada / la de la cadena ligada) y descartar el resto SIN borrarlo:
* - LEADS → etapa 'Descartado', motivo 'Duplicado'
* - OPORTUNIDADES→ etapa 'Perdido', motivo 'Duplicado'
* - APARTADO → etapa 'Cancelacion Apartado', motivo 'Duplicado'
* Cada descarte queda en AuditoriaLog (usuario admin del sistema).
*
* Seguridad: sólo toca la fila si el board y la etapa ACTUALES coinciden con
* lo esperado; si ya avanzó o ya está cerrada, la omite y avisa. Idempotente.
*
* Uso: npx tsx scripts/descartar-duplicados.ts
*/
import dotenv from 'dotenv'
import { PrismaClient } from '@prisma/client'
dotenv.config()
const prisma = new PrismaClient()
const ADMIN_ID = process.env.ADMIN_USUARIO_ID ?? 'c882ed06-7891-439a-9cd9-f33dd36378b3'
const MOTIVO = 'Duplicado'
/** Etapa terminal por board (mismo criterio que CrmLocalService.descartar) */
const ETAPA_TERMINAL: Record<string, string> = {
LEADS: 'Descartado',
OPORTUNIDADES: 'Perdido',
APARTADO: 'Cancelacion Apartado',
}
interface Descarte {
leadId: string
nombre: string
board: string
etapaEsperada: string
}
const LEADS: Descarte[] = [
{ leadId: 'loc_9a9b444a-6d1b-495f-8a6f-a10c4ae260c5', nombre: 'PaoSan', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_c8eac00b-f7ef-4842-81c8-6d0011aa9531', nombre: 'Nayeli Mota Perez (25/09)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_b190d1cd-96aa-4abb-8d44-606840bf3ae5', nombre: 'Xenya Dominguez (25/09)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_758b736f-d186-476f-996f-c95cce0463ea', nombre: 'Gustavo Fernandez de León (jose gregorio)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_80ed24b0-5c7d-4df5-8273-0e1c6b64e2d8', nombre: 'GUADALUPE (25/09)', board: 'LEADS', etapaEsperada: 'Calificado' },
{ leadId: 'loc_9e63629d-988f-485c-8b23-47b38115357a', nombre: 'Blanca Monter (17:49)', board: 'LEADS', etapaEsperada: 'Calificado' },
{ leadId: 'loc_e2e85982-5a3e-469e-9dc7-57c7225671d8', nombre: 'Wilberth Almanza (00:38)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_6a5f2678-8c5f-468c-b9e4-67a6b845e095', nombre: 'Maytte Lyon (00:48)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_d6d996d9-1ed3-48c7-8eb7-bb1e4890c37f', nombre: 'eduardo velarde gutierrez (tel de Daniel)', board: 'LEADS', etapaEsperada: 'NUEVO' },
{ leadId: 'loc_44fe422c-9746-47c8-a8c7-2b3e495630db', nombre: 'Stefano Sandoval (tel compartido con Blanca)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_c26e53c0-5c33-472c-8b66-0d84a7a34b7b', nombre: 'Miguel Ángel Rodríguez (25/09)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_beeec35a-aa09-44af-ab5a-c7edd522e80e', nombre: 'Joel Benitez (00:36)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_347d7a43-b9e9-4f5c-8319-965353e256a2', nombre: 'Abigail Melendez (00:29)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_237f8b3d-23d7-4206-97de-d69ffab5ce00', nombre: 'Mildred (00:25)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_eab9b179-0883-4ecf-a380-7dba8ae2d256', nombre: 'Eva Vargas Arias (00:31)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_5f1b3b9b-cd37-4353-8b32-d8a49cb2992c', nombre: 'Julio (NUEVO 18:58)', board: 'LEADS', etapaEsperada: 'NUEVO' },
{ leadId: 'loc_7c98bdbf-00fb-40ba-be1f-19d78efb8df7', nombre: 'EDGAR MORALES (20:57)', board: 'LEADS', etapaEsperada: 'Contactado' },
{ leadId: 'loc_cfacf7dd-eaa7-43c8-aab7-1f388c8477fe', nombre: 'Ivan (21/09)', board: 'LEADS', etapaEsperada: 'Contactado' },
]
const OPORTUNIDADES: Descarte[] = [
{ leadId: 'loc_07cbd367-9537-4f8f-9a12-6af840dc548f', nombre: 'EMANUEL GARCIA HERNANDEZ (19/09)', board: 'OPORTUNIDADES', etapaEsperada: 'Aparto' },
{ leadId: 'loc_6635cc1e-d160-4a93-bb58-e52a7d704546', nombre: 'GUADALUPE Agendo Cita (25/09)', board: 'OPORTUNIDADES', etapaEsperada: 'Agendo Cita' },
{ leadId: 'loc_661acb75-2601-46dc-a314-e903b2d9ad02', nombre: 'GUADALUPE AVILA Afluencia (24/09)', board: 'OPORTUNIDADES', etapaEsperada: 'Afluencia' },
{ leadId: 'loc_d234e717-b849-4a09-ab06-3cacc8afcd7b', nombre: 'Blanca Monter Agendo Cita (17:50)', board: 'OPORTUNIDADES', etapaEsperada: 'Agendo Cita' },
{ leadId: 'loc_2c18e0ce-c1a7-47e0-b9fc-18562a83b9b7', nombre: 'Rocio Gtz Afluencia (17:50)', board: 'OPORTUNIDADES', etapaEsperada: 'Afluencia' },
{ leadId: 'loc_d5f3ec01-cf7e-47f2-9840-a77aada459e7', nombre: 'Nereyda Lopez Afluencia (17:53)', board: 'OPORTUNIDADES', etapaEsperada: 'Afluencia' },
{ leadId: 'loc_8873f6e0-7440-4061-aa2e-cc31be47f8f0', nombre: 'Guadalupe Avila Jimenez Cotizador', board: 'OPORTUNIDADES', etapaEsperada: 'Cotizador' },
]
const APARTADOS: Descarte[] = [
{ leadId: 'loc_e0751165-95e8-4e1c-b21c-f99e417a6c54', nombre: 'EMANUEL GARCIA HERNANDEZ (03:29)', board: 'APARTADO', etapaEsperada: 'Pago Apartado' },
]
const ETAPAS_CERRADAS = ['Descartado', 'Perdido', 'Cancelo', 'Cancelacion Apartado']
async function main() {
const todos = [...LEADS, ...OPORTUNIDADES, ...APARTADOS]
let descartados = 0
let omitidos = 0
for (const d of todos) {
const row = await prisma.leadCaptura.findUnique({ where: { leadId: d.leadId } })
if (!row) {
console.log(`OMITIDO (no existe): ${d.leadId} ${d.nombre}`)
omitidos++
continue
}
if (row.board !== d.board || ETAPAS_CERRADAS.includes(row.etapa)) {
console.log(`OMITIDO (board=${row.board} etapa=${row.etapa}): ${d.nombre}`)
omitidos++
continue
}
if (row.etapa !== d.etapaEsperada) {
console.log(`OMITIDO (etapa actual "${row.etapa}" != esperada "${d.etapaEsperada}"): ${d.nombre}`)
omitidos++
continue
}
await prisma.$transaction([
prisma.leadCaptura.update({
where: { leadId: d.leadId },
data: { etapa: ETAPA_TERMINAL[d.board], motivo: MOTIVO, submotivo: null, pendienteSync: true },
}),
prisma.auditoriaLog.create({
data: {
usuarioId: ADMIN_ID,
accion: 'LIMPIEZA_DUPLICADO',
entidad: 'LeadCaptura',
entidadId: d.leadId,
detalle: `Descartado como duplicado (${d.board} "${row.nombreLead}" etapa ${row.etapa}). Limpieza post-fix dedup telefonoNorm.`,
},
}),
])
console.log(`DESCARTADO [${d.board} → ${ETAPA_TERMINAL[d.board]}]: ${row.nombreLead} (${d.nombre})`)
descartados++
}
console.log(`\nListo: ${descartados} descartados, ${omitidos} omitidos de ${todos.length}.`)
}
main()
.catch((e) => {
console.error(e)
process.exitCode = 1
})
.finally(() => prisma.$disconnect())