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:
141
api-gateway/src/services/unidades-local.service.test.ts
Normal file
141
api-gateway/src/services/unidades-local.service.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AppError } from '../utils/errors.js'
|
||||
|
||||
// ---------- Prisma y Monday mockeados (los tests no tocan BD ni red) ----------
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
unidad: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mondayMock = vi.hoisted(() => ({
|
||||
getUnidades: vi.fn(),
|
||||
getUnidad: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
|
||||
|
||||
import {
|
||||
getUnidadesLocal,
|
||||
getUnidadLocal,
|
||||
listarUnidadesConFallback,
|
||||
obtenerUnidadConFallback,
|
||||
unidadLocalADto,
|
||||
type FilaUnidad,
|
||||
} from './unidades-local.service.js'
|
||||
import { saludMonday } from './salud-monday.service.js'
|
||||
|
||||
function fila(parcial: Partial<FilaUnidad> & { codigo: string }): FilaUnidad {
|
||||
return {
|
||||
desarrollo: 'SENDA',
|
||||
modelo: '1R',
|
||||
supConstruccion: 69.5,
|
||||
supExcedente: 0,
|
||||
supEstacionamiento: 12.5,
|
||||
precioLista: 4_415_000,
|
||||
precioAcabados: 4_579_000,
|
||||
status: 'DISPONIBLE',
|
||||
etapa: 'Entrega inmediata',
|
||||
...parcial,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.restoreAllMocks() // restaura los spyOn(saludMonday, 'estaDisponible')
|
||||
saludMonday.restablecer()
|
||||
prismaMock.unidad.findMany.mockResolvedValue([])
|
||||
prismaMock.unidad.findUnique.mockResolvedValue(null)
|
||||
})
|
||||
|
||||
describe('unidadLocalADto', () => {
|
||||
it('mapea la fila local al DTO Unidad (misma forma que Monday)', () => {
|
||||
const dto = unidadLocalADto(fila({ codigo: 'SENDA-101' }))
|
||||
expect(dto).toEqual({
|
||||
codigo: 'SENDA-101',
|
||||
modelo: '1R',
|
||||
fechaEntrega: null,
|
||||
supEstacionamiento: 12.5,
|
||||
supConstruccion: 69.5,
|
||||
supExcedente: 0,
|
||||
precioSinAcabados: 4_415_000,
|
||||
precioConAcabados: 4_579_000,
|
||||
status: 'DISPONIBLE',
|
||||
desarrollo: 'SENDA',
|
||||
mesesParaFirma: null,
|
||||
etapa: 'Entrega inmediata',
|
||||
})
|
||||
})
|
||||
|
||||
it('etapa vacía → null (como el mapeo Monday)', () => {
|
||||
expect(unidadLocalADto(fila({ codigo: 'U-1', etapa: '' })).etapa).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getUnidadesLocal / getUnidadLocal', () => {
|
||||
it('devuelve las filas mapeadas; soloDisponibles y desarrollo filtran', async () => {
|
||||
prismaMock.unidad.findMany.mockResolvedValue([
|
||||
fila({ codigo: 'U-1', desarrollo: 'DIAGONAL' }),
|
||||
fila({ codigo: 'U-2', desarrollo: 'SENDA', status: 'APARTADO' }),
|
||||
fila({ codigo: 'U-3', desarrollo: 'senda' }),
|
||||
])
|
||||
const todas = await getUnidadesLocal()
|
||||
expect(todas.map((u) => u.codigo)).toEqual(['U-1', 'U-2', 'U-3'])
|
||||
|
||||
const disponibles = await getUnidadesLocal({ soloDisponibles: true })
|
||||
expect(disponibles.map((u) => u.codigo)).toEqual(['U-1', 'U-3'])
|
||||
|
||||
const senda = await getUnidadesLocal({ desarrollo: 'SENDA' })
|
||||
expect(senda.map((u) => u.codigo)).toEqual(['U-2', 'U-3'])
|
||||
})
|
||||
|
||||
it('getUnidadLocal: null si no existe', async () => {
|
||||
prismaMock.unidad.findUnique.mockImplementation(async ({ where }) =>
|
||||
where.codigo === 'U-9' ? fila({ codigo: 'U-9' }) : null,
|
||||
)
|
||||
expect((await getUnidadLocal('U-9'))?.codigo).toBe('U-9')
|
||||
expect(await getUnidadLocal('U-X')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fallbacks con circuit breaker', () => {
|
||||
it('Monday caído (salud) → lee local sin llamar a Monday', async () => {
|
||||
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
|
||||
prismaMock.unidad.findMany.mockResolvedValue([fila({ codigo: 'U-1' })])
|
||||
prismaMock.unidad.findUnique.mockResolvedValue(fila({ codigo: 'U-1' }))
|
||||
|
||||
const lista = await listarUnidadesConFallback()
|
||||
expect(lista.map((u) => u.codigo)).toEqual(['U-1'])
|
||||
expect(mondayMock.getUnidades).not.toHaveBeenCalled()
|
||||
|
||||
const unidad = await obtenerUnidadConFallback('U-1')
|
||||
expect(unidad?.codigo).toBe('U-1')
|
||||
expect(mondayMock.getUnidad).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Monday vivo → camino normal (sin tocar local)', async () => {
|
||||
mondayMock.getUnidades.mockResolvedValue([{ codigo: 'M-1' }])
|
||||
mondayMock.getUnidad.mockResolvedValue({ codigo: 'M-1' })
|
||||
expect(await listarUnidadesConFallback()).toEqual([{ codigo: 'M-1' }])
|
||||
expect(await obtenerUnidadConFallback('M-1')).toEqual({ codigo: 'M-1' })
|
||||
expect(prismaMock.unidad.findMany).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Monday lanza AppError 502 → fallback local', async () => {
|
||||
mondayMock.getUnidades.mockRejectedValue(new AppError(502, 'Monday no respondió'))
|
||||
mondayMock.getUnidad.mockRejectedValue(new AppError(502, 'Monday no respondió'))
|
||||
prismaMock.unidad.findMany.mockResolvedValue([fila({ codigo: 'U-1' })])
|
||||
prismaMock.unidad.findUnique.mockResolvedValue(fila({ codigo: 'U-1' }))
|
||||
|
||||
expect((await listarUnidadesConFallback()).map((u) => u.codigo)).toEqual(['U-1'])
|
||||
expect((await obtenerUnidadConFallback('U-1'))?.codigo).toBe('U-1')
|
||||
})
|
||||
|
||||
it('otro error de Monday se propaga (no se traga nada)', async () => {
|
||||
mondayMock.getUnidades.mockRejectedValue(new AppError(500, 'bug'))
|
||||
await expect(listarUnidadesConFallback()).rejects.toThrow('bug')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user