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:
@@ -1,9 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Request, Response } from 'express'
|
||||
import {
|
||||
cambiarEstadoApartado,
|
||||
cambiarEstadoApartadoEndpoint,
|
||||
FASES_APARTADO_PERMITIDAS,
|
||||
subirDocumentosApartado,
|
||||
TIPOS_DOCUMENTO_APARTADO,
|
||||
} from './apartados.controller.js'
|
||||
import { saludMonday } from '../services/salud-monday.service.js'
|
||||
|
||||
// ---------- Prisma mockeado (los tests no tocan la BD) ----------
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
leadCaptura: {
|
||||
create: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
cliente: {
|
||||
create: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
documento: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
syncPendiente: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||
|
||||
/** Monday mockeado: registra cambios de columna y movimientos de grupo */
|
||||
function mondayMock(grupoActual = 'topics') {
|
||||
@@ -71,3 +99,85 @@ describe('cambiarEstadoApartado (POST /api/apartados/:id/estado)', () => {
|
||||
expect(TIPOS_DOCUMENTO_APARTADO).toEqual(['CAPTURA_COTIZADOR', 'CAPTURA_PAGO_APARTADO', 'OTRO'])
|
||||
})
|
||||
})
|
||||
|
||||
// ---------- Modo local (saludMonday caído / ids loc_*) ----------
|
||||
|
||||
describe('ruteo modo local (apartados)', () => {
|
||||
function resFake() {
|
||||
const json = vi.fn()
|
||||
const status = vi.fn().mockReturnValue({ json })
|
||||
return { res: { json, status } as unknown as Response, status, json }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prismaMock.leadCaptura.findUnique.mockResolvedValue(null)
|
||||
prismaMock.leadCaptura.findFirst.mockResolvedValue(null)
|
||||
prismaMock.leadCaptura.create.mockImplementation(async ({ data }) => ({ ...data }))
|
||||
prismaMock.leadCaptura.update.mockImplementation(async ({ data }) => ({ ...data }))
|
||||
prismaMock.cliente.findFirst.mockResolvedValue(null)
|
||||
prismaMock.cliente.create.mockImplementation(async ({ data }) => ({ id: 'cli-local', ...data }))
|
||||
prismaMock.documento.create.mockImplementation(async ({ data }) => ({ id: 'doc-1', ...data }))
|
||||
prismaMock.syncPendiente.create.mockResolvedValue({})
|
||||
prismaMock.$transaction.mockImplementation(async (ops: Promise<unknown>[]) => Promise.all(ops))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('cambiarEstadoApartadoEndpoint con id loc_*: fase local + CAMBIO_ETAPA encolado', async () => {
|
||||
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(true) // el id loc_* manda aunque Monday viva
|
||||
prismaMock.leadCaptura.findUnique.mockResolvedValue({ leadId: 'loc_apt', board: 'APARTADO' })
|
||||
const { res, json } = resFake()
|
||||
|
||||
await cambiarEstadoApartadoEndpoint(
|
||||
{ params: { itemId: 'loc_apt' }, body: { estado: 'Alta Enkontrol' } } as unknown as Request,
|
||||
res,
|
||||
)
|
||||
|
||||
expect(prismaMock.leadCaptura.update).toHaveBeenCalledWith({
|
||||
where: { leadId: 'loc_apt' },
|
||||
data: { etapa: 'Alta Enkontrol', pendienteSync: true },
|
||||
})
|
||||
expect(prismaMock.syncPendiente.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ tipo: 'CAMBIO_ETAPA', referenciaLocal: 'loc_apt' }),
|
||||
})
|
||||
expect(json).toHaveBeenCalledWith({ itemId: 'loc_apt', estado: 'Alta Enkontrol' })
|
||||
})
|
||||
|
||||
it('subirDocumentosApartado con Monday caído: nombre desde LeadCaptura local y checkbox encolado (CHECKBOX_DOC)', async () => {
|
||||
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
|
||||
// El apartado local nació del "Aparto" de la oportunidad: su fila tiene el nombre
|
||||
prismaMock.leadCaptura.findFirst.mockResolvedValue({ leadId: 'loc_apt', nombreLead: 'Luis Demo' })
|
||||
const archivo = {
|
||||
originalname: 'curp.pdf',
|
||||
path: '/tmp/uploads/apartados/loc_apt/CURP_1.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
size: 1234,
|
||||
} as Express.Multer.File
|
||||
const { res, status } = resFake()
|
||||
|
||||
await subirDocumentosApartado(
|
||||
{ params: { itemId: 'loc_apt' }, body: { tipo: 'CURP' }, files: [archivo], usuarioToken: { sub: 'u-1' } } as unknown as Request,
|
||||
res,
|
||||
)
|
||||
|
||||
// Cliente mínimo creado con el nombre de la fila local (no truena por obtenerNombreItem)
|
||||
expect(prismaMock.cliente.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ nombreCompleto: 'Luis Demo', mondayApartadoId: 'loc_apt' }),
|
||||
})
|
||||
expect(prismaMock.documento.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ clienteId: 'cli-local', tipo: 'CURP' }),
|
||||
})
|
||||
// El checkbox no se intenta en Monday: queda encolado para el worker
|
||||
expect(prismaMock.syncPendiente.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
tipo: 'CHECKBOX_DOC',
|
||||
payloadJson: JSON.stringify({ checkbox: 'CURP', itemId: 'loc_apt' }),
|
||||
referenciaLocal: 'loc_apt',
|
||||
}),
|
||||
})
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user