feat(afluencia): captura completa de perfil en alta de afluencia

Agrega al formulario ligero y a POST /api/leads: tipo de propiedad,
medio de contacto, edad, estado civil, tipo de empleo, ocupacion,
rango salarial, credito, tipo de economia, califica y lo quiere para.
Selects obligatorios (estado civil, tipo de empleo, tipo de economia,
califica) con las etiquetas sembradas como labels status en el tablero
Leads de Monday.
This commit is contained in:
2026-09-02 01:38:13 +00:00
parent f7070c9c35
commit 52667df041
7 changed files with 388 additions and 27 deletions

View File

@@ -37,7 +37,7 @@ describe('AltaAfluenciaForm', () => {
it('con email inválido no llama al API', () => {
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/^Email/), { target: { value: 'correo-malo' } })
fireEvent.change(screen.getByLabelText(/^Correo/), { target: { value: 'correo-malo' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect(fetchMock).not.toHaveBeenCalled()
@@ -48,7 +48,7 @@ describe('AltaAfluenciaForm', () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/^Origen/), { target: { value: 'Web' } })
fireEvent.change(screen.getByLabelText(/¿Cómo se enteró de nosotros\?/), { target: { value: 'Web' } })
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
@@ -65,6 +65,76 @@ describe('AltaAfluenciaForm', () => {
})
})
it('renderiza los selects de calificación con las opciones de los catálogos', () => {
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
const opcionesDe = (label: RegExp) =>
Array.from((screen.getByLabelText(label) as HTMLSelectElement).options).map((o) => o.value)
expect(opcionesDe(/Departamento o casa de interés/)).toEqual(['', 'Casa', 'Departamento'])
expect(opcionesDe(/^Estado civil/)).toEqual(['', 'Soltero(a)', 'Casado(a)', 'Unión libre', 'Divorciado(a)', 'Viudo(a)'])
expect(opcionesDe(/Tipo de empleo/)).toEqual(['', 'Asalariado', 'Independiente'])
expect(opcionesDe(/^Crédito/)).toEqual([
'',
'Infonavit',
'Apoyo Infonavit',
'Cofinavit',
'Segundo crédito',
'Contado',
'Fovissste',
'Cofi-Fovissste',
'Bancario',
])
expect(opcionesDe(/Tipo de economía/)).toEqual(['', 'Economía mexicana', 'Economía americana', 'Mixta'])
expect(opcionesDe(/^Califica/)).toEqual(['', 'Sí', 'No', 'Por confirmar'])
})
it('el payload incluye los campos de calificación capturados', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/Departamento o casa de interés/), { target: { value: 'Casa' } })
fireEvent.change(screen.getByLabelText(/Medio de contacto/), { target: { value: 'Llamada' } })
fireEvent.change(screen.getByLabelText(/^Edad/), { target: { value: '35' } })
fireEvent.change(screen.getByLabelText(/^Estado civil/), { target: { value: 'Casado(a)' } })
fireEvent.change(screen.getByLabelText(/Tipo de empleo/), { target: { value: 'Asalariado' } })
fireEvent.change(screen.getByLabelText(/^Ocupación/), { target: { value: 'Ingeniero' } })
fireEvent.change(screen.getByLabelText(/Rango salarial/), { target: { value: '$20,000 - $30,000' } })
fireEvent.change(screen.getByLabelText(/^Crédito/), { target: { value: 'Infonavit' } })
fireEvent.change(screen.getByLabelText(/Tipo de economía/), { target: { value: 'Economía mexicana' } })
fireEvent.change(screen.getByLabelText(/^Califica/), { target: { value: 'Sí' } })
fireEvent.change(screen.getByLabelText(/Lo quiere para/), { target: { value: 'Vivir ahí' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
await screen.findByRole('status')
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(JSON.parse(String(init.body))).toMatchObject({
tipoPropiedad: 'Casa',
medioContacto: 'Llamada',
edad: '35',
estadoCivil: 'Casado(a)',
tipoEmpleo: 'Asalariado',
ocupacion: 'Ingeniero',
rangoSalarial: '$20,000 - $30,000',
credito: 'Infonavit',
tipoEconomia: 'Economía mexicana',
califica: 'Sí',
loQuierePara: 'Vivir ahí',
})
})
it('sin campos de calificación el payload no los incluye', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
await screen.findByRole('status')
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
const cuerpo = JSON.parse(String(init.body)) as Record<string, unknown>
expect(cuerpo).toEqual({ nombre: 'Ana Demo', telefono: '6641234567' })
})
it('con 409 muestra el duplicado con el nombre del lead existente', async () => {
fetchMock.mockResolvedValueOnce(
respuesta({ error: 'Ya existe un lead con ese teléfono', leadId: 'l-9', nombre: 'ANA DEMO' }, 409),
@@ -114,7 +184,7 @@ describe('AltaAfluenciaForm', () => {
const onPasar = vi.fn()
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={onPasar} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/^Email/), { target: { value: 'ana@correo.com' } })
fireEvent.change(screen.getByLabelText(/^Correo/), { target: { value: 'ana@correo.com' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
fireEvent.click(await screen.findByRole('button', { name: /Pasar a Alta de Cliente/ }))