From ea03e6eeba831a5478caeadc02390ca7c6415b38 Mon Sep 17 00:00:00 2001 From: Consult AS Technology Date: Sat, 26 Sep 2026 21:37:07 +0000 Subject: [PATCH] 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. --- api-gateway/scripts/descartar-duplicados.ts | 130 ++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 api-gateway/scripts/descartar-duplicados.ts diff --git a/api-gateway/scripts/descartar-duplicados.ts b/api-gateway/scripts/descartar-duplicados.ts new file mode 100644 index 0000000..05dfa83 --- /dev/null +++ b/api-gateway/scripts/descartar-duplicados.ts @@ -0,0 +1,130 @@ +/** + * 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 = { + 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())