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:
2026-09-06 08:08:56 +00:00
parent af2fb10956
commit 60649890eb
70 changed files with 8763 additions and 240 deletions

View File

@@ -1,6 +1,16 @@
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { notificarEscrituracionSiLiquidado, resolverFiltroMios } from './planes.controller.js'
// ---------- Prisma mockeado: el encolado de la réplica no toca la BD ----------
const prismaMock = vi.hoisted(() => ({
syncPendiente: {
create: vi.fn(),
},
}))
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
/** Monday mockeado: registra cambios de columna */
function mondayMock() {
return {
@@ -12,6 +22,11 @@ function mondayMock() {
}
describe('notificarEscrituracionSiLiquidado (puente cobranza → postventa)', () => {
beforeEach(() => {
vi.clearAllMocks()
prismaMock.syncPendiente.create.mockResolvedValue({})
})
it('plan LIQUIDADO con mondayItemId: mueve el apartado a "X Escriturar"', async () => {
const monday = mondayMock()
const ok = await notificarEscrituracionSiLiquidado({ mondayItemId: 'apt-55' }, true, monday)
@@ -30,13 +45,21 @@ describe('notificarEscrituracionSiLiquidado (puente cobranza → postventa)', ()
expect(monday.cambios).toEqual([])
})
it('un fallo de Monday no rompe el abono: loguea y devuelve false', async () => {
it('un fallo de Monday no rompe el abono: loguea, encola LIQUIDADO_ESCRITURAR y devuelve false', async () => {
const monday = mondayMock()
vi.spyOn(monday, 'cambiarValorColumna').mockRejectedValueOnce(new Error('Monday caído'))
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {})
const ok = await notificarEscrituracionSiLiquidado({ mondayItemId: 'apt-55' }, true, monday)
expect(ok).toBe(false)
expect(spyError).toHaveBeenCalled()
// La escrituración queda encolada para que el worker la aplique al volver Monday
expect(prismaMock.syncPendiente.create).toHaveBeenCalledWith({
data: expect.objectContaining({
tipo: 'LIQUIDADO_ESCRITURAR',
payloadJson: JSON.stringify({ mondayItemId: 'apt-55' }),
referenciaLocal: 'apt-55',
}),
})
spyError.mockRestore()
})
})