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

@@ -15,9 +15,6 @@ const prismaMock = vi.hoisted(() => ({
create: vi.fn(),
findUnique: vi.fn(),
},
syncPendiente: {
create: vi.fn(),
},
automatizacionState: {
findUnique: vi.fn(),
upsert: vi.fn(),
@@ -34,19 +31,6 @@ import { CrmLocalService, esIdLocal } from './crm-local.service.js'
const svc = new CrmLocalService()
/** Tipos de sync encolados, en orden, a partir de las llamadas al mock */
function tiposEncolados(): string[] {
return prismaMock.syncPendiente.create.mock.calls.map((c) => (c[0] as { data: { tipo: string } }).data.tipo)
}
function payloadEncolado(tipo: string): Record<string, unknown> {
const llamada = prismaMock.syncPendiente.create.mock.calls.find(
(c) => (c[0] as { data: { tipo: string } }).data.tipo === tipo,
)
if (!llamada) throw new Error(`No se encoló ${tipo}`)
return JSON.parse((llamada[0] as { data: { payloadJson: string } }).data.payloadJson) as Record<string, unknown>
}
beforeEach(() => {
vi.clearAllMocks()
prismaMock.$transaction.mockImplementation(async (ops: Promise<unknown>[]) => Promise.all(ops))
@@ -55,20 +39,19 @@ beforeEach(() => {
prismaMock.leadCaptura.findUnique.mockResolvedValue(null)
prismaMock.leadCaptura.update.mockImplementation(async ({ data }) => data)
prismaMock.leadOportunidad.create.mockResolvedValue({})
prismaMock.syncPendiente.create.mockResolvedValue({})
prismaMock.automatizacionState.findUnique.mockResolvedValue(null)
prismaMock.automatizacionState.upsert.mockResolvedValue({})
})
describe('esIdLocal', () => {
it('distingue ids locales de ids de Monday', () => {
it('distingue ids locales de ids heredados', () => {
expect(esIdLocal('loc_123e4567-e89b-12d3-a456-426614174000')).toBe(true)
expect(esIdLocal('1234567890')).toBe(false)
})
})
describe('CrmLocalService.crearLead', () => {
it('crea la captura board=LEADS etapa NUEVO pendienteSync y encola CREAR_LEAD + ASIGNAR_PERSONA', async () => {
it('crea la captura board=LEADS etapa NUEVO pendienteSync', async () => {
const r = await svc.crearLead(
{ nombre: 'Ana Prueba', telefono: '4771234567', origen: 'WhatsApp', desarrollo: 'SENDA', notas: 'Quiere 2R' },
'u-1',
@@ -90,12 +73,6 @@ describe('CrmLocalService.crearLead', () => {
pendienteSync: true,
}),
})
expect(tiposEncolados()).toEqual(['CREAR_LEAD', 'ASIGNAR_PERSONA'])
const payload = payloadEncolado('CREAR_LEAD')
expect(payload['datos']).toMatchObject({ nombre: 'Ana Prueba', telefono: '4771234567', origenLabel: 'WhatsApp' })
expect(payloadEncolado('ASIGNAR_PERSONA')).toEqual({ board: 'leads', usuarioId: 'u-1' })
// Todo quedó en una sola transacción (cambio local + cola atómicos)
expect(prismaMock.$transaction).toHaveBeenCalledOnce()
})
it('sin teléfono repetido no hay duplicado; con teléfono existente devuelve { duplicado }', async () => {
@@ -103,12 +80,11 @@ describe('CrmLocalService.crearLead', () => {
const r = await svc.crearLead({ nombre: 'Ana Prueba', telefono: '4771234567' })
expect(r).toEqual({ duplicado: { id: 'loc_viejo', nombre: 'Ana Prueba' } })
expect(prismaMock.leadCaptura.create).not.toHaveBeenCalled()
expect(prismaMock.syncPendiente.create).not.toHaveBeenCalled()
})
})
describe('CrmLocalService.crearAfluencia', () => {
it('crea la oportunidad local (board=OPORTUNIDADES, etapa Afluencia) con perfil en datosJson y encola CREAR_AFLUENCIA', async () => {
it('crea la oportunidad local (board=OPORTUNIDADES, etapa Afluencia) con perfil en datosJson', async () => {
const r = await svc.crearAfluencia(
{
nombre: 'Luis Demo',
@@ -135,18 +111,11 @@ describe('CrmLocalService.crearAfluencia', () => {
})
// Perfilamiento completo en datosJson
expect(JSON.parse(creada.data['datosJson'] as string)).toMatchObject({ edad: 35, credito: 'Infonavit' })
// Auto-mapping leadId = oportunidadId (como el flujo Monday)
// Auto-mapping leadId = oportunidadId
expect(prismaMock.leadOportunidad.create).toHaveBeenCalledWith({ data: { leadId: r.id, oportunidadId: r.id } })
expect(tiposEncolados()).toEqual(['CREAR_AFLUENCIA', 'ASIGNAR_PERSONA'])
expect(payloadEncolado('CREAR_AFLUENCIA')['datos']).toMatchObject({
nombre: 'Luis Demo',
modeloLabel: 'SOHO',
creditoLabel: 'Infonavit',
loQuierePara: 'Habitarla',
})
})
it('con oportunidadId ACTUALIZA la fila local y encola ACTUALIZAR_AFLUENCIA (no crea duplicado)', async () => {
it('con oportunidadId ACTUALIZA la fila local (no crea duplicado)', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue({ leadId: 'op-55', nombreLead: 'Luis Demo' })
const r = await svc.crearAfluencia({ nombre: 'Luis Demo', oportunidadId: 'op-55', califica: 'Si' }, 'u-2')
expect(r).toEqual({ id: 'op-55', oportunidadId: 'op-55', actualizada: true })
@@ -155,8 +124,6 @@ describe('CrmLocalService.crearAfluencia', () => {
data: expect.objectContaining({ etapa: 'Afluencia', pendienteSync: true }),
})
expect(prismaMock.leadCaptura.create).not.toHaveBeenCalled()
expect(tiposEncolados()).toEqual(['ACTUALIZAR_AFLUENCIA'])
expect(payloadEncolado('ACTUALIZAR_AFLUENCIA')['datos']).toMatchObject({ nombre: 'Luis Demo', calificaLabel: 'Si' })
})
it('con teléfono duplicado en OPORTUNIDADES devuelve { duplicado }', async () => {
@@ -167,44 +134,35 @@ describe('CrmLocalService.crearAfluencia', () => {
})
describe('CrmLocalService.cambiarEstado', () => {
it('actualiza la etapa local y encola CAMBIO_ETAPA con board y label', async () => {
it('actualiza la etapa local y guarda la fecha de la cita en datosJson', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue({ leadId: 'loc_1', board: 'OPORTUNIDADES' })
await svc.cambiarEstado('loc_1', 'Agendo Cita', 'OPORTUNIDADES', { fecha: '2026-09-10' })
expect(prismaMock.leadCaptura.update).toHaveBeenCalledWith({
where: { leadId: 'loc_1' },
data: { etapa: 'Agendo Cita', pendienteSync: true },
data: { etapa: 'Agendo Cita', pendienteSync: true, datosJson: JSON.stringify({ fechaCita: '2026-09-10' }) },
})
expect(payloadEncolado('CAMBIO_ETAPA')).toEqual({ board: 'oportunidades', label: 'Agendo Cita', fecha: '2026-09-10' })
})
it('sin fila local y sin sombra → 404', async () => {
await expect(svc.cambiarEstado('loc_inexistente', 'Contactado', 'LEADS')).rejects.toMatchObject({ status: 404 })
})
it('con sombra crea la fila mínima (id Monday viejo) y encola igual', async () => {
it('con sombra crea la fila mínima (id heredado)', async () => {
await svc.cambiarEstado('998877', 'Contactado', 'LEADS', { sombra: true, usuarioId: 'u-1' })
expect(prismaMock.leadCaptura.create).toHaveBeenCalledWith({
data: expect.objectContaining({ leadId: '998877', board: 'LEADS', mondayItemId: '998877', pendienteSync: true }),
})
expect(tiposEncolados()).toEqual(['CAMBIO_ETAPA'])
})
})
describe('CrmLocalService.descartar', () => {
it('marca etapa terminal Perdido con motivo/submotivo y encola DESCARTE', async () => {
it('marca etapa terminal Perdido con motivo/submotivo', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue({ leadId: 'loc_2', nombreLead: 'Luis Demo' })
await svc.descartar('loc_2', 'Precio', { subcategoria: 'Enganche', comentario: 'Se le hizo caro' })
expect(prismaMock.leadCaptura.update).toHaveBeenCalledWith({
where: { leadId: 'loc_2' },
data: { etapa: 'Perdido', motivo: 'Precio', submotivo: 'Enganche', pendienteSync: true },
})
expect(payloadEncolado('DESCARTE')).toEqual({
fuente: 'oportunidad',
motivo: 'Precio',
nombreItem: 'Luis Demo',
subcategoria: 'Enganche',
comentario: 'Se le hizo caro',
})
})
it('fuente lead: etapa terminal Descartado', async () => {
@@ -231,7 +189,7 @@ describe('CrmLocalService.convertirLead', () => {
datosJson: '{"notas":null}',
}
it('marca el lead Calificado, crea la oportunidad en Agendo Cita, liga y encola CONVERSION', async () => {
it('marca el lead Calificado, crea la oportunidad en Agendo Cita y la liga', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue(filaLead)
const op = await svc.convertirLead('loc_lead', 'u-1')
expect(op.leadId).toMatch(/^loc_/)
@@ -251,12 +209,9 @@ describe('CrmLocalService.convertirLead', () => {
expect(prismaMock.leadOportunidad.create).toHaveBeenCalledWith({
data: { leadId: 'loc_lead', oportunidadId: op.leadId },
})
const payload = payloadEncolado('CONVERSION')
expect(payload['oportunidadIdLocal']).toBe(op.leadId)
expect(payload['datos']).toMatchObject({ nombre: 'Ana Prueba', estadoLabel: 'Agendo Cita', telefono: '4771234567' })
})
it('si el lead ya fue convertido → 409 como el flujo Monday', async () => {
it('si el lead ya fue convertido → 409', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue({ ...filaLead, oportunidadId: 'loc_op_previa' })
await expect(svc.convertirLead('loc_lead')).rejects.toMatchObject({ status: 409 })
expect(prismaMock.leadCaptura.create).not.toHaveBeenCalled()
@@ -279,7 +234,7 @@ describe('CrmLocalService.apartar', () => {
datosJson: '{"edad":35}',
}
it('pone la oportunidad en Aparto, crea el apartado local (Pago Apartado) y encola CAMBIO_ETAPA + CREAR_APARTADO', async () => {
it('pone la oportunidad en Aparto y crea el apartado local (Pago Apartado)', async () => {
prismaMock.leadCaptura.findUnique.mockResolvedValue(filaOportunidad)
const apartado = await svc.apartar('loc_op', 'u-1')
expect(apartado.leadId).toMatch(/^loc_/)
@@ -299,13 +254,6 @@ describe('CrmLocalService.apartar', () => {
expect(prismaMock.automatizacionState.upsert).toHaveBeenCalledWith(
expect.objectContaining({ where: { clave: 'apartado-creado:loc_op' } }),
)
expect(tiposEncolados()).toEqual(['CAMBIO_ETAPA', 'CREAR_APARTADO'])
expect(payloadEncolado('CREAR_APARTADO')['datos']).toMatchObject({
clienteNombre: 'Luis Demo',
estadoLabel: 'Pago Apartado',
telefono: '6641112233',
email: 'luis@correo.com',
})
})
it('idempotente: con el dedup escrito devuelve el apartado existente sin encolar de nuevo', async () => {
@@ -316,7 +264,6 @@ describe('CrmLocalService.apartar', () => {
const apartado = await svc.apartar('loc_op', 'u-1')
expect(apartado.leadId).toBe('loc_ap_previo')
expect(prismaMock.leadCaptura.create).not.toHaveBeenCalled()
expect(prismaMock.syncPendiente.create).not.toHaveBeenCalled()
})
})