feat: conversión lead→oportunidad (Agendo) y alta ligada a apartado

- Mis leads: botón 'Agendo →' convierte el lead en oportunidad (etapa
  Agendo Cita, con sus datos, asesor = vendedor del lead o quien
  convierte), marca el lead Calificado y guarda el mapping
  LeadOportunidad; 409 si ya fue convertido
- Mis oportunidades: se quitan No Asistio y Abandono (redundantes con
  Descartar); quedan Cotizar, Afluencia, Agendo Cita y Aparto
- Mis apartados: 'Firma cliente (venta) →' abre el alta de cliente con
  TODO lo recolectado prellenado (ficha del apartado + contacto +
  oportunidad + cotización vinculada vía GET /altas/prefill-apartado/:id)
- El alta con apartadoMondayId se LIGA al apartado existente y lo
  enriquece (curp, teléfono, email, precio final, enganche, forma de
  pago, unidad) en vez de crear un duplicado
- monday.service: actualizarApartado + ApartadoMonday con curp, forma de
  pago, enganche y monto apartado
- Tests: 470 API + 120 alta-clientes + 16 mi-dia, verde
This commit is contained in:
2026-09-05 00:33:53 +00:00
parent a4575bb948
commit 5b348d21e5
14 changed files with 654 additions and 17 deletions

View File

@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest'
import { apartadoAPrellenado, type PrefillApartado } from './apartadoAlta'
const base: PrefillApartado = {
apartado: {
id: 'a1',
nombre: 'Ana Demo',
telefono: '+52 6641234567',
email: 'ana@correo.com',
curp: '',
unidadCodigo: 'SENDA-409-1-2',
precioFinal: 2646000,
enganche: 132300,
formaPago: 'Cofinavit',
},
contacto: {
nombre: 'Ana Demo',
CURP: 'AAXX010101XXXXXXX0',
RFC: 'AAXX010101XXX',
Dirección: 'Calle 1, Col. Centro, CP 38000',
'Estado Civil': 'Casado(a)',
'Fecha Nacimiento': '1990-05-12',
},
oportunidad: { id: 'op-1', desarrollo: 'SENDA', modeloInteres: 'SOHO', credito: 'Cofinavit' },
cotizacion: { tipoPropuesta: 'INFONAVIT', acabados: true, precioFinal: 2646000, enganche: 132300 },
}
describe('apartadoAPrellenado (alta desde apartado de Monday)', () => {
it('prellena datos generales (contacto manda) y vivienda/crédito (apartado > oportunidad > cotización)', () => {
const p = apartadoAPrellenado(base)
expect(p.datosGenerales).toMatchObject({
nombreCompleto: 'ANA DEMO',
email: 'ana@correo.com',
celular: '+52 6641234567',
curp: 'AAXX010101XXXXXXX0',
rfc: 'AAXX010101XXX',
direccion: 'CALLE 1, COL. CENTRO, CP 38000',
fechaNacimiento: '1990-05-12',
estadoCivil: 'CASADO',
})
expect(p.viviendaCredito).toMatchObject({
proyecto: 'SENDA',
modelo: 'SOHO',
ubicacion: 'SENDA-409-1-2',
valorVenta: '2646000',
enganche: '132300',
tiposCredito: ['COFINAVIT'], // formaPago del apartado manda sobre la cotización
equipamientoAdicional: 'COCINA Y CLOSET', // cotización con acabados
})
expect(p.leadMondayId).toBe('op-1')
})
it('desarrollo desconocido cae en OTRO con el nombre en texto libre', () => {
const p = apartadoAPrellenado({ ...base, oportunidad: { id: 'op-1', desarrollo: 'MONTEBELLO' } })
expect(p.viviendaCredito.proyecto).toBe('OTRO')
expect(p.viviendaCredito.proyectoOtro).toBe('MONTEBELLO')
})
it('sin contacto usa los datos del apartado; sin créditos no prellena tiposCredito', () => {
const p = apartadoAPrellenado({
...base,
contacto: null,
apartado: { ...base.apartado, formaPago: '' },
oportunidad: { id: 'op-1', desarrollo: 'SENDA' },
cotizacion: null,
})
expect(p.datosGenerales).toMatchObject({ nombreCompleto: 'ANA DEMO', email: 'ana@correo.com', celular: '+52 6641234567' })
expect(p.datosGenerales.curp).toBeUndefined()
expect(p.datosGenerales.estadoCivil).toBeUndefined()
expect(p.viviendaCredito.tiposCredito).toBeUndefined()
expect(p.viviendaCredito.equipamientoAdicional).toBeUndefined()
expect(p.leadMondayId).toBe('op-1')
})
it('sin oportunidad relacionada el alta queda sin lead ligado', () => {
const p = apartadoAPrellenado({ ...base, oportunidad: null })
expect(p.leadMondayId).toBeNull()
})
})