feat(modo-local): fallback completo sin Monday + cola de sincronización
- Circuit breaker saludMonday (3 fallos → modo local; ping cada 60s)
- Endpoint GET /api/sistema/estado {mondayOk, pendientesSync}
- LeadCaptura extendida (board, etapa, origen, motivo, pendienteSync)
- Tablas Unidad (espejo inventario, 288 seed) + SyncPendiente
- crmLocal: crearLead, afluencia, cambio etapa, descarte, conversión, apartar
- Ruteo en controllers: fallback local ante 502 o Monday caído
- Mi día local: resumen completo desde SQLite + unión con Monday
- Fallbacks: unidades, contactos, reportes, prefill, buscador
- Sync worker cada 2min + handlers 15 tipos (con DependenciaPendienteError)
- Gate automatizaciones: skip si !mondayOk
- Banner 'Modo local' en 5 apps + badge pendiente en mi-dia
- 597 API tests + 263 front tests verdes
Env MODO_LOCAL_FORZADO=true para testing sin Monday.
This commit is contained in:
@@ -8,6 +8,9 @@ const prismaMock = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
syncPendiente: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mondayMock = vi.hoisted(() => ({
|
||||
@@ -103,13 +106,38 @@ describe('crearCotizacion (POST /api/cotizaciones) — match con el funnel', ()
|
||||
expect(mondayMock.crearOportunidad).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('si Monday falla, la cotización igual se crea (201 con mondayItemId null)', async () => {
|
||||
it('si Monday falla, la cotización igual se crea (201 con mondayItemId null) y la vinculación queda encolada', async () => {
|
||||
mondayMock.crearOportunidad.mockRejectedValue(new Error('Monday caído'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
prismaMock.syncPendiente.create.mockResolvedValue({})
|
||||
const { res, status, json } = resFake()
|
||||
await crearCotizacion(reqFake(bodyBase), res)
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(json.mock.calls[0]?.[0]).toMatchObject({ mondayItemId: null })
|
||||
expect(prismaMock.cotizacion.create).toHaveBeenCalledOnce()
|
||||
// COTIZACION_OPORTUNIDAD con los datos completos + teléfono para el match del worker
|
||||
expect(prismaMock.syncPendiente.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
tipo: 'COTIZACION_OPORTUNIDAD',
|
||||
referenciaLocal: 'cot-1',
|
||||
}),
|
||||
})
|
||||
const llamada = prismaMock.syncPendiente.create.mock.calls[0]?.[0] as { data: { payloadJson: string } }
|
||||
expect(JSON.parse(llamada.data.payloadJson)).toMatchObject({
|
||||
clienteTelefono: '4771234567',
|
||||
datos: { unidadCodigo: 'U-101', estadoLabel: 'Cotizador' },
|
||||
})
|
||||
})
|
||||
|
||||
it('si falla la actualización con oportunidadId explícito, se encola con ESE id', async () => {
|
||||
mondayMock.actualizarOportunidadCotizacion.mockRejectedValue(new Error('Monday caído'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
prismaMock.syncPendiente.create.mockResolvedValue({})
|
||||
const { res, status } = resFake()
|
||||
await crearCotizacion(reqFake({ ...bodyBase, oportunidadId: 'opp-midia-1' }), res)
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
const llamada = prismaMock.syncPendiente.create.mock.calls[0]?.[0] as { data: { payloadJson: string } }
|
||||
expect(JSON.parse(llamada.data.payloadJson)).toMatchObject({ oportunidadId: 'opp-midia-1' })
|
||||
})
|
||||
|
||||
it('con oportunidadId explícito (botón "Cotizar" de Mi día): actualiza ESE item y no consulta el funnel', async () => {
|
||||
|
||||
Reference in New Issue
Block a user