- El alta (directa y progresiva) llena el Apartado con telefono, correo, curp, Precio Final, Enganche, Fecha Compra, Vendedor (el capturista con Monday vinculado) y Forma de Pago; el path webhook/cron hereda contacto, precio y asesor de la oportunidad. - Bidireccional: Mis clientes en /hoy muestra el estado actual del Apartado leyendolo de Monday (si lo mueven en Monday, la app lo refleja en el siguiente refresh). - getApartados con la correccion value-null al leer Estado. - Test del mapa de Tipo propuesta del cotizador (los 502 del log eran de un proceso previo al fix 4f31d1c; queda cubierto).
357 lines
14 KiB
TypeScript
357 lines
14 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { Request, Response } from 'express'
|
|
import { contactoDesdeDatos, textoDireccion } from './clientes.controller.js'
|
|
|
|
// ---------- Mocks de módulo (los tests no tocan BD ni red) ----------
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
cliente: {
|
|
create: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
usuario: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
const mondayMock = vi.hoisted(() => ({
|
|
crearContacto: vi.fn(),
|
|
actualizarContacto: vi.fn(),
|
|
crearApartado: vi.fn(),
|
|
}))
|
|
|
|
const funnelMock = vi.hoisted(() => ({
|
|
moverAApartoConDedup: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
|
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
|
|
vi.mock('../services/funnel.service.js', () => ({ funnelService: funnelMock }))
|
|
|
|
const reqFake = (body: unknown, params: Record<string, string> = {}): Request =>
|
|
({ body, params }) as unknown as Request
|
|
|
|
const resFake = () => {
|
|
const json = vi.fn()
|
|
const status = vi.fn().mockReturnValue({ json })
|
|
return { res: { json, status } as unknown as Response, status, json }
|
|
}
|
|
|
|
const datosWizard = {
|
|
datosGenerales: {
|
|
nombreCompleto: 'Ana Demo Progreso',
|
|
curp: 'AEDA900101MBCXXX00',
|
|
rfc: 'AEDA900101XX0',
|
|
email: 'ana@correo.com',
|
|
celular: '6649990003',
|
|
estadoCivil: 'SOLTERO',
|
|
tipoVivienda: 'RENTADA',
|
|
tipoCompra: 'VIVIR_AHI',
|
|
fechaNacimiento: '1990-01-01',
|
|
},
|
|
empleo: { tipoEmpleo: 'ASALARIADO' },
|
|
viviendaCredito: {
|
|
proyecto: 'DIAGONAL',
|
|
domicilioFiscal: {
|
|
calle: 'Av. Siempre Viva',
|
|
numExt: '123',
|
|
numInt: '4',
|
|
colonia: 'Col. Centro',
|
|
cp: '22000',
|
|
municipio: 'Tijuana',
|
|
estado: 'Baja California',
|
|
},
|
|
},
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
prismaMock.cliente.create.mockImplementation(async ({ data }) => ({ id: 'cli-1', ...data }))
|
|
prismaMock.cliente.update.mockImplementation(async ({ data }) => ({ id: 'cli-1', ...data }))
|
|
prismaMock.usuario.findUnique.mockResolvedValue(null) // default: capturista sin Monday vinculado
|
|
mondayMock.crearContacto.mockResolvedValue('cont-1')
|
|
mondayMock.actualizarContacto.mockResolvedValue(undefined)
|
|
mondayMock.crearApartado.mockResolvedValue('ap-1')
|
|
funnelMock.moverAApartoConDedup.mockResolvedValue('op-1')
|
|
})
|
|
|
|
describe('textoDireccion (domicilio del wizard → columna Dirección)', () => {
|
|
it('arma calle con ext/int, colonia, CP, municipio y estado', () => {
|
|
expect(
|
|
textoDireccion({
|
|
calle: 'Av. Siempre Viva',
|
|
numExt: '123',
|
|
numInt: '4',
|
|
colonia: 'Col. Centro',
|
|
cp: '22000',
|
|
municipio: 'Tijuana',
|
|
estado: 'Baja California',
|
|
}),
|
|
).toBe('Av. Siempre Viva 123, Int. 4, Col. Centro, CP 22000, Tijuana, Baja California')
|
|
})
|
|
|
|
it('omite las partes vacías y devuelve undefined si no hay nada', () => {
|
|
expect(textoDireccion({ calle: 'Calle Uno', municipio: 'Tijuana' })).toBe('Calle Uno, Tijuana')
|
|
expect(textoDireccion({})).toBeUndefined()
|
|
expect(textoDireccion({ calle: ' ' })).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('contactoDesdeDatos (estado del wizard → campos del Contacto)', () => {
|
|
it('deriva todos los campos mapeables presentes', () => {
|
|
const c = contactoDesdeDatos(datosWizard)
|
|
expect(c).toMatchObject({
|
|
nombre: 'Ana Demo Progreso',
|
|
curp: 'AEDA900101MBCXXX00',
|
|
rfc: 'AEDA900101XX0',
|
|
email: 'ana@correo.com',
|
|
telefono: '6649990003',
|
|
direccion: 'Av. Siempre Viva 123, Int. 4, Col. Centro, CP 22000, Tijuana, Baja California',
|
|
estadoCivilLabel: 'Soltero',
|
|
viviendaActualLabel: 'Rentada',
|
|
propositoCompraLabel: 'Habitarla',
|
|
rangoEdadLabel: 'Entre 24 y 40',
|
|
fechaNacimiento: '1990-01-01',
|
|
actividadEconomicaLabel: 'Empleado',
|
|
})
|
|
})
|
|
|
|
it('solo incluye campos presentes (no inventa datos)', () => {
|
|
const c = contactoDesdeDatos({ datosGenerales: { nombreCompleto: 'Solo Nombre' } })
|
|
expect(c).toEqual({ nombre: 'Solo Nombre' })
|
|
})
|
|
|
|
it('sin domicilioFiscal usa el texto libre datosGenerales.direccion', () => {
|
|
const c = contactoDesdeDatos({
|
|
datosGenerales: { nombreCompleto: 'Ana', direccion: 'Calle Libre 1' },
|
|
})
|
|
expect(c.direccion).toBe('Calle Libre 1')
|
|
})
|
|
})
|
|
|
|
describe('iniciarProgreso (POST /api/clientes/progreso/iniciar)', () => {
|
|
it('crea el Cliente local y el Contacto en Monday con Estado PROSPECTO', async () => {
|
|
const { iniciarProgreso } = await import('./clientes.controller.js')
|
|
const { res, status, json } = resFake()
|
|
await iniciarProgreso(
|
|
reqFake({
|
|
datosGenerales: {
|
|
nombreCompleto: 'Ana Demo Progreso',
|
|
curp: 'AEDA900101MBCXXX00',
|
|
rfc: 'AEDA900101XX0',
|
|
celular: '6649990003',
|
|
},
|
|
}),
|
|
res,
|
|
)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
expect(json.mock.calls[0]?.[0]).toEqual({ id: 'cli-1', mondayContactoId: 'cont-1' })
|
|
expect(mondayMock.crearContacto).toHaveBeenCalledWith({
|
|
nombre: 'Ana Demo Progreso',
|
|
estadoLabel: 'PROSPECTO',
|
|
curp: 'AEDA900101MBCXXX00',
|
|
rfc: 'AEDA900101XX0',
|
|
telefono: '6649990003',
|
|
})
|
|
expect(prismaMock.cliente.update).toHaveBeenCalledWith({
|
|
where: { id: 'cli-1' },
|
|
data: { mondayContactoId: 'cont-1' },
|
|
})
|
|
})
|
|
|
|
it('requiere solo datosGenerales.nombreCompleto', async () => {
|
|
const { iniciarProgreso } = await import('./clientes.controller.js')
|
|
const { res, status } = resFake()
|
|
await iniciarProgreso(reqFake({ datosGenerales: { nombreCompleto: 'Solo Nombre' } }), res)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
expect(mondayMock.crearContacto).toHaveBeenCalledWith({
|
|
nombre: 'Solo Nombre',
|
|
estadoLabel: 'PROSPECTO',
|
|
})
|
|
await expect(iniciarProgreso(reqFake({ datosGenerales: {} }), resFake().res)).rejects.toThrow()
|
|
})
|
|
|
|
it('fallo de Monday: crea el local igual y devuelve mondayContactoId null', async () => {
|
|
const { iniciarProgreso } = await import('./clientes.controller.js')
|
|
mondayMock.crearContacto.mockRejectedValue(new Error('Monday caído'))
|
|
const { res, status, json } = resFake()
|
|
await iniciarProgreso(reqFake({ datosGenerales: { nombreCompleto: 'Ana Demo' } }), res)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
expect(json.mock.calls[0]?.[0]).toEqual({ id: 'cli-1', mondayContactoId: null })
|
|
})
|
|
|
|
it('guarda capturadoPorId con el sub del JWT (null sin token)', async () => {
|
|
const { iniciarProgreso } = await import('./clientes.controller.js')
|
|
const req = reqFake({ datosGenerales: { nombreCompleto: 'Ana Demo' } })
|
|
;(req as unknown as { usuarioToken: { sub: string } }).usuarioToken = { sub: 'usr-1' }
|
|
await iniciarProgreso(req, resFake().res)
|
|
expect(prismaMock.cliente.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({ data: expect.objectContaining({ capturadoPorId: 'usr-1' }) }),
|
|
)
|
|
await iniciarProgreso(reqFake({ datosGenerales: { nombreCompleto: 'Ana Demo' } }), resFake().res)
|
|
expect(prismaMock.cliente.create).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({ data: expect.objectContaining({ capturadoPorId: null }) }),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('actualizarProgreso (PATCH /api/clientes/progreso/:id)', () => {
|
|
it('deriva los campos presentes y actualiza el Contacto (incl. Dirección armada)', async () => {
|
|
const { actualizarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({ id: 'cli-1', mondayContactoId: 'cont-1' })
|
|
const { res, json } = resFake()
|
|
await actualizarProgreso(reqFake(datosWizard, { id: 'cli-1' }), res)
|
|
expect(prismaMock.cliente.update).toHaveBeenCalledWith({
|
|
where: { id: 'cli-1' },
|
|
data: { payloadJson: JSON.stringify(datosWizard) },
|
|
})
|
|
expect(mondayMock.actualizarContacto).toHaveBeenCalledWith(
|
|
'cont-1',
|
|
expect.objectContaining({
|
|
nombre: 'Ana Demo Progreso',
|
|
direccion: 'Av. Siempre Viva 123, Int. 4, Col. Centro, CP 22000, Tijuana, Baja California',
|
|
actividadEconomicaLabel: 'Empleado',
|
|
telefono: '6649990003',
|
|
}),
|
|
)
|
|
expect(json.mock.calls[0]?.[0]).toEqual({ id: 'cli-1', mondaySync: true })
|
|
})
|
|
|
|
it('sin mondayContactoId responde mondaySync: false y no toca Monday', async () => {
|
|
const { actualizarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({ id: 'cli-1', mondayContactoId: null })
|
|
const { res, json } = resFake()
|
|
await actualizarProgreso(reqFake(datosWizard, { id: 'cli-1' }), res)
|
|
expect(mondayMock.actualizarContacto).not.toHaveBeenCalled()
|
|
expect(json.mock.calls[0]?.[0]).toEqual({ id: 'cli-1', mondaySync: false })
|
|
})
|
|
|
|
it('fallo de Monday: 200 igual con mondaySync: false', async () => {
|
|
const { actualizarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({ id: 'cli-1', mondayContactoId: 'cont-1' })
|
|
mondayMock.actualizarContacto.mockRejectedValue(new Error('Monday caído'))
|
|
const { res, json } = resFake()
|
|
await actualizarProgreso(reqFake(datosWizard, { id: 'cli-1' }), res)
|
|
expect(json.mock.calls[0]?.[0]).toEqual({ id: 'cli-1', mondaySync: false })
|
|
})
|
|
|
|
it('rechaza un body que no sea objeto', async () => {
|
|
const { actualizarProgreso } = await import('./clientes.controller.js')
|
|
await expect(actualizarProgreso(reqFake('no-objeto', { id: 'cli-1' }), resFake().res)).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('completarProgreso (POST /api/clientes/progreso/:id/completar)', () => {
|
|
const altaCompleta = {
|
|
...datosWizard,
|
|
viviendaCredito: {
|
|
...datosWizard.viviendaCredito,
|
|
ubicacion: 'U-999-DEMO2',
|
|
valorVenta: 1500000,
|
|
enganche: 20000,
|
|
},
|
|
firma: { firmaDataUrl: 'data:image/png;base64,AAAA' },
|
|
leadMondayId: 'lead-9',
|
|
}
|
|
|
|
it('pasa el Contacto a CLIENTE, crea el Apartado y mueve el lead a Aparto (dedup)', async () => {
|
|
const { completarProgreso } = await import('./clientes.controller.js')
|
|
const { prismaStateStore } = await import('../services/automatizaciones.service.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({
|
|
id: 'cli-1',
|
|
mondayContactoId: 'cont-1',
|
|
mondayApartadoId: null,
|
|
})
|
|
const { res, status, json } = resFake()
|
|
await completarProgreso(reqFake(altaCompleta, { id: 'cli-1' }), res)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
expect(mondayMock.actualizarContacto).toHaveBeenCalledWith(
|
|
'cont-1',
|
|
expect.objectContaining({ nombre: 'Ana Demo Progreso', estadoLabel: 'CLIENTE' }),
|
|
)
|
|
expect(mondayMock.crearApartado).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
clienteNombre: 'Ana Demo Progreso',
|
|
unidadCodigo: 'U-999-DEMO2',
|
|
montoApartado: 20000,
|
|
estadoLabel: 'Pago Apartado',
|
|
}),
|
|
)
|
|
expect(funnelMock.moverAApartoConDedup).toHaveBeenCalledWith('lead-9', prismaStateStore)
|
|
expect(json.mock.calls[0]?.[0]).toMatchObject({
|
|
id: 'cli-1',
|
|
mondayContactoId: 'cont-1',
|
|
mondayApartadoId: 'ap-1',
|
|
})
|
|
})
|
|
|
|
it('el Apartado lleva la ficha completa: contacto, precio, Forma de Pago y Vendedor del capturista original', async () => {
|
|
const { completarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({
|
|
id: 'cli-1',
|
|
mondayContactoId: 'cont-1',
|
|
mondayApartadoId: null,
|
|
capturadoPorId: 'usr-9', // quien inició el alta progresiva
|
|
})
|
|
prismaMock.usuario.findUnique.mockResolvedValue({ mondayUserId: 12345 })
|
|
const altaConCredito = {
|
|
...altaCompleta,
|
|
viviendaCredito: { ...altaCompleta.viviendaCredito, tiposCredito: ['COFI_FOVISSSTE'] },
|
|
}
|
|
const { res, status } = resFake()
|
|
await completarProgreso(reqFake(altaConCredito, { id: 'cli-1' }), res)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
expect(mondayMock.crearApartado).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
clienteNombre: 'Ana Demo Progreso',
|
|
unidadCodigo: 'U-999-DEMO2',
|
|
telefono: '6649990003',
|
|
email: 'ana@correo.com',
|
|
curp: 'AEDA900101MBCXXX00',
|
|
precioFinal: 1500000,
|
|
enganche: 20000,
|
|
vendedorId: 12345,
|
|
formaPagoLabel: 'Cofi-Fovissste',
|
|
fechaCompra: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('sin Contacto previo lo crea directo como CLIENTE (alta sin Monday al iniciar)', async () => {
|
|
const { completarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({
|
|
id: 'cli-1',
|
|
mondayContactoId: null,
|
|
mondayApartadoId: null,
|
|
})
|
|
const { res, status } = resFake()
|
|
await completarProgreso(reqFake(altaCompleta, { id: 'cli-1' }), res)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
expect(mondayMock.actualizarContacto).not.toHaveBeenCalled()
|
|
expect(mondayMock.crearContacto).toHaveBeenCalledWith(
|
|
expect.objectContaining({ nombre: 'Ana Demo Progreso', estadoLabel: 'CLIENTE' }),
|
|
)
|
|
})
|
|
|
|
it('rechaza un payload incompleto (misma validación que POST /clientes)', async () => {
|
|
const { completarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue({ id: 'cli-1', mondayContactoId: 'cont-1' })
|
|
await expect(
|
|
completarProgreso(
|
|
reqFake({ datosGenerales: { nombreCompleto: 'Sin curp ni proyecto' } }, { id: 'cli-1' }),
|
|
resFake().res,
|
|
),
|
|
).rejects.toThrow()
|
|
expect(prismaMock.cliente.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('404 si el progreso no existe', async () => {
|
|
const { completarProgreso } = await import('./clientes.controller.js')
|
|
prismaMock.cliente.findUnique.mockResolvedValue(null)
|
|
await expect(
|
|
completarProgreso(reqFake(altaCompleta, { id: 'no-existe' }), resFake().res),
|
|
).rejects.toThrow('Cliente no encontrado')
|
|
})
|
|
})
|