feat(apartado): ficha completa en Monday + estado visible en la app

- 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).
This commit is contained in:
2026-09-04 07:34:57 +00:00
parent d81797c610
commit 42cfe937cf
13 changed files with 428 additions and 12 deletions

View File

@@ -743,4 +743,99 @@ describe('MondayService', () => {
expect(Object.keys(parsed).sort()).toEqual(['color_estado', 'date_fecha'])
})
})
describe('crearApartado (ficha completa del tablero Apartado)', () => {
// Columnas reales del board (verificadas en Monday 2026-09-04)
const COLUMNAS_APARTADO = [
{ id: 'text_u', title: 'Unidad', type: 'text' },
{ id: 'numeric_ma', title: 'Monto apartado', type: 'numbers' },
{ id: 'date_fa', title: 'Fecha apartado', type: 'date' },
{ id: 'date_fl', title: 'Fecha límite', type: 'date' },
{ id: 'color_e', title: 'Estado', type: 'status' },
{ id: 'phone_t', title: 'Teléfono', type: 'phone' },
{ id: 'email_c', title: 'Correo electrónico', type: 'email' },
{ id: 'text_curp', title: 'curp', type: 'text' }, // minúscula en el board
{ id: 'numeric_pf', title: 'Precio Final', type: 'numbers' },
{ id: 'numeric_en', title: 'Enganche', type: 'numbers' },
{ id: 'date_fc', title: 'Fecha Compra', type: 'date' },
{ id: 'people_v', title: 'Vendedor', type: 'people' },
{ id: 'dropdown_fp', title: 'Forma de Pago', type: 'dropdown' },
{ id: 'long_obs', title: 'Observaciones', type: 'long_text' },
]
it('serializa cada columna según su tipo (phone, email, numbers, date, people, dropdown, long_text)', async () => {
const llamadas = mockearFetch((body) => {
if (body.query.includes('settings_str')) {
return respuestaOk({
boards: [
{
columns: [
{ id: 'color_e', title: 'Estado', settings_str: '{"labels":{"1":"Pago Apartado"}}' },
{ id: 'dropdown_fp', title: 'Forma de Pago', settings_str: '{"labels":[{"name":"Infonavit"},{"name":"Contado"}]}' },
],
},
],
})
}
if (body.query.includes('columns')) return respuestaOk({ boards: [{ columns: COLUMNAS_APARTADO }] })
return respuestaOk({ create_item: { id: 'ap-1' } })
})
const id = await crearServicio().crearApartado({
clienteNombre: 'Cliente Demo',
unidadCodigo: 'U-101',
montoApartado: 20000,
fechaApartado: '2026-09-04',
fechaLimite: '2026-09-19',
estadoLabel: 'Pago Apartado',
telefono: '6641112233',
email: 'cli@correo.com',
curp: 'CURP123',
precioFinal: 1500000,
enganche: 20000,
fechaCompra: '2026-09-04',
vendedorId: 12345,
formaPagoLabel: 'INFONAVIT', // el resolver lo ajusta al label real
observaciones: 'Venta con descuento autorizado',
})
expect(id).toBe('ap-1')
const mutacion = llamadas.find((l) => l.body.query.includes('create_item'))
expect(mutacion?.body.variables['boardId']).toBe('666') // tablero Apartado
const parsed = JSON.parse(String(mutacion?.body.variables['columnValues'])) as Record<string, unknown>
expect(parsed['phone_t']).toEqual({ phone: '6641112233', countryShortName: 'MX' })
expect(parsed['email_c']).toEqual({ email: 'cli@correo.com', text: 'cli@correo.com' })
expect(parsed['text_curp']).toBe('CURP123')
expect(parsed['numeric_pf']).toBe(1500000)
expect(parsed['numeric_en']).toBe(20000)
expect(parsed['date_fc']).toEqual({ date: '2026-09-04' })
expect(parsed['people_v']).toEqual({ personsAndTeams: [{ id: 12345, kind: 'person' }] })
expect(parsed['dropdown_fp']).toEqual({ labels: ['Infonavit'] }) // dropdown resuelto case-insensitive
expect(parsed['long_obs']).toEqual({ text: 'Venta con descuento autorizado' })
})
it('omite las columnas opcionales no capturadas', async () => {
const llamadas = mockearFetch((body) => {
if (body.query.includes('settings_str')) {
return respuestaOk({
boards: [{ columns: [{ id: 'color_e', title: 'Estado', settings_str: '{"labels":{"1":"Pago Apartado"}}' }] }],
})
}
if (body.query.includes('columns')) return respuestaOk({ boards: [{ columns: COLUMNAS_APARTADO }] })
return respuestaOk({ create_item: { id: 'ap-2' } })
})
await crearServicio().crearApartado({
clienteNombre: 'Cliente Demo',
unidadCodigo: 'U-101',
fechaApartado: '2026-09-04',
fechaLimite: '2026-09-19',
estadoLabel: 'Pago Apartado',
})
const mutacion = llamadas.find((l) => l.body.query.includes('create_item'))
const parsed = JSON.parse(String(mutacion?.body.variables['columnValues'])) as Record<string, unknown>
expect(Object.keys(parsed).sort()).toEqual(['color_e', 'date_fa', 'date_fl', 'text_u'])
})
})
})