feat(monday): alinear Frasa-CAS con CRM real del cliente (7 mejoras)
- Vocabulario funnel: labels Intento de contacto/Abandono en Leads, etapas completas en Oportunidades (Cotizador, Ganado, Perdido...) - Apartados: pipeline Enkontrol (8 estados) + checkboxes CURP/RFC/Comprobante que la app de alta prende al subir documentos - Motivos No Compra: board catálogo + dropdown + endpoint /oportunidades/:id/perder - Perfil demográfico en Contactos mapeado desde el alta (estado civil, vivienda, propósito, fecha nac., rango de edad calculado) - Desarrollos: catálogo maestro sincronizado (precios, m2, tipo, entrega) - Actividades: board nuevo + creación automática en round-robin e inactividad - Cobranza: plan liquidado mueve el apartado a 'X Escriturar' en Monday - Scripts: setup:crm y sync:desarrollos (idempotentes) - Tests 119/119 verdes
This commit is contained in:
@@ -43,11 +43,19 @@ function stateEnMemoria(): StateStore & { mapa: Map<string, string> } {
|
||||
}
|
||||
}
|
||||
|
||||
/** Monday mockeado: leads fijos, registra asignaciones y updates */
|
||||
/** Monday mockeado: leads fijos, registra asignaciones, updates y actividades */
|
||||
function mondayMock(leads: LeadMonday[]) {
|
||||
return {
|
||||
asignaciones: [] as { itemId: string; userId: number }[],
|
||||
updates: [] as { itemId: string; body: string }[],
|
||||
actividades: [] as {
|
||||
nombre: string
|
||||
tipoLabel: string
|
||||
responsableId: number
|
||||
fecha: string
|
||||
estadoLabel: string
|
||||
leadItemId: string
|
||||
}[],
|
||||
async getLeads() {
|
||||
return leads
|
||||
},
|
||||
@@ -58,6 +66,17 @@ function mondayMock(leads: LeadMonday[]) {
|
||||
this.updates.push({ itemId, body })
|
||||
return 'upd-1'
|
||||
},
|
||||
async crearActividad(datos: {
|
||||
nombre: string
|
||||
tipoLabel: string
|
||||
responsableId: number
|
||||
fecha: string
|
||||
estadoLabel: string
|
||||
leadItemId: string
|
||||
}) {
|
||||
this.actividades.push(datos)
|
||||
return 'act-1'
|
||||
},
|
||||
async getUsuariosWorkspace() {
|
||||
return VENDEDORES
|
||||
},
|
||||
@@ -238,3 +257,85 @@ describe('AutomatizacionesService.escaneoInactividad', () => {
|
||||
expect(monday.updates).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------- Actividades automáticas (tablero Actividades del CRM) ----------
|
||||
|
||||
describe('Actividades automáticas (ACTIVIDADES_ENABLED)', () => {
|
||||
const ahora = new Date('2026-08-22T09:00:00Z')
|
||||
|
||||
it('el round-robin crea "Contactar a {lead}" tipo Llamada, Abierto, ligada al lead', async () => {
|
||||
const monday = mondayMock([lead('1'), lead('2')])
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: true })
|
||||
|
||||
const r = await svc.runRoundRobin()
|
||||
expect(r.asignados).toBe(2)
|
||||
expect(monday.actividades).toHaveLength(2)
|
||||
expect(monday.actividades[0]).toMatchObject({
|
||||
nombre: 'Contactar a Lead 1',
|
||||
tipoLabel: 'Llamada',
|
||||
responsableId: 101,
|
||||
estadoLabel: 'Abierto',
|
||||
leadItemId: '1',
|
||||
})
|
||||
// Fecha compromiso: hoy + 1 día (fecha local, como la construye el servicio)
|
||||
const esperada = new Date()
|
||||
esperada.setDate(esperada.getDate() + 1)
|
||||
const m = String(esperada.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(esperada.getDate()).padStart(2, '0')
|
||||
expect(monday.actividades[0]?.fecha).toBe(`${esperada.getFullYear()}-${m}-${d}`)
|
||||
expect(monday.actividades[1]?.responsableId).toBe(102)
|
||||
})
|
||||
|
||||
it('con actividadesEnabled=false el round-robin no crea actividades', async () => {
|
||||
const monday = mondayMock([lead('1')])
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: false })
|
||||
await svc.runRoundRobin()
|
||||
expect(monday.actividades).toEqual([])
|
||||
})
|
||||
|
||||
it('inactividad: crea "Retomar contacto" al vendedor asignado; sin vendedor no crea nada', async () => {
|
||||
const monday = mondayMock([
|
||||
lead('1', { updatedAt: '2026-08-01T00:00:00Z', vendedorIds: [103] }), // inactivo con vendedor
|
||||
lead('2', { updatedAt: '2026-08-01T00:00:00Z' }), // inactivo sin vendedor
|
||||
])
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: true })
|
||||
|
||||
const r = await svc.escaneoInactividad({ preview: false, ahora })
|
||||
expect(r.recordatoriosEnviados).toBe(2)
|
||||
expect(monday.actividades).toHaveLength(1)
|
||||
expect(monday.actividades[0]).toMatchObject({
|
||||
nombre: 'Retomar contacto con Lead 1',
|
||||
tipoLabel: 'Llamada',
|
||||
responsableId: 103,
|
||||
estadoLabel: 'Abierto',
|
||||
leadItemId: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('preview de inactividad no crea actividades', async () => {
|
||||
const monday = mondayMock([lead('1', { updatedAt: '2026-08-01T00:00:00Z', vendedorIds: [101] })])
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: true })
|
||||
await svc.escaneoInactividad({ preview: true, ahora })
|
||||
expect(monday.actividades).toEqual([])
|
||||
})
|
||||
|
||||
it('un fallo al crear la actividad no rompe la asignación', async () => {
|
||||
const monday = mondayMock([lead('1')])
|
||||
vi.spyOn(monday, 'crearActividad').mockRejectedValueOnce(new Error('Monday caído'))
|
||||
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: true })
|
||||
|
||||
const r = await svc.runRoundRobin()
|
||||
expect(r.asignados).toBe(1) // la asignación se hizo igual
|
||||
expect(monday.asignaciones).toEqual([{ itemId: '1', userId: 101 }])
|
||||
spyError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ESTADOS_TERMINALES_LEAD con vocabulario CRM', () => {
|
||||
it('ABANDONO también es terminal', () => {
|
||||
expect(esEstadoTerminal('ABANDONO')).toBe(true)
|
||||
expect(esEstadoTerminal('Abandono')).toBe(true)
|
||||
expect(esEstadoTerminal('Intento de contacto')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user