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:
100
api-gateway/src/services/unidades-local.service.ts
Normal file
100
api-gateway/src/services/unidades-local.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { mondayService, type Unidad } from './monday.service.js'
|
||||
import { prisma } from './prisma.js'
|
||||
import { saludMonday } from './salud-monday.service.js'
|
||||
import { AppError } from '../utils/errors.js'
|
||||
|
||||
/**
|
||||
* Fallback de LECTURA del inventario (modo local): la tabla `Unidad` es el
|
||||
* espejo local del tablero Unidades de Monday (la siembra
|
||||
* scripts/seed-unidades-locales.js y la mantiene fresco el write-through de
|
||||
* monday.service.getUnidades). Cuando Monday está caído (saludMonday) o lanza
|
||||
* AppError 502, el cotizador lee de aquí con la MISMA forma de DTO.
|
||||
*
|
||||
* La tabla local no guarda fechaEntrega ni mesesParaFirma (no están en el
|
||||
* seed curado): salen en null, que el cotizador ya tolera (plazo desconocido).
|
||||
*/
|
||||
|
||||
/** Fila de la tabla Unidad tal como la devuelve Prisma (estructural) */
|
||||
export interface FilaUnidad {
|
||||
codigo: string
|
||||
desarrollo: string
|
||||
modelo: string
|
||||
supConstruccion: number
|
||||
supExcedente: number
|
||||
supEstacionamiento: number
|
||||
precioLista: number
|
||||
precioAcabados: number
|
||||
status: string
|
||||
etapa: string
|
||||
}
|
||||
|
||||
/** Fila local → DTO `Unidad` (UnidadCotizable) que consumen controllers y fronts */
|
||||
export function unidadLocalADto(u: FilaUnidad): Unidad {
|
||||
return {
|
||||
codigo: u.codigo,
|
||||
modelo: u.modelo,
|
||||
fechaEntrega: null, // no está en el espejo local: plazo desconocido
|
||||
supEstacionamiento: u.supEstacionamiento,
|
||||
supConstruccion: u.supConstruccion,
|
||||
supExcedente: u.supExcedente,
|
||||
precioSinAcabados: u.precioLista,
|
||||
precioConAcabados: u.precioAcabados,
|
||||
status: u.status,
|
||||
desarrollo: u.desarrollo,
|
||||
mesesParaFirma: null,
|
||||
etapa: u.etapa === '' ? null : u.etapa,
|
||||
}
|
||||
}
|
||||
|
||||
/** true si el error es un fallo de Monday (AppError 502) que amerita fallback local */
|
||||
function esFalloMonday502(err: unknown): boolean {
|
||||
return err instanceof AppError && err.status === 502
|
||||
}
|
||||
|
||||
/** Unidades del espejo local; filtros equivalentes a los del controller */
|
||||
export async function getUnidadesLocal(
|
||||
filtro: { desarrollo?: string | undefined; soloDisponibles?: boolean | undefined } = {},
|
||||
): Promise<Unidad[]> {
|
||||
const rows = await prisma.unidad.findMany({ orderBy: { codigo: 'asc' } })
|
||||
let lista = rows.map(unidadLocalADto)
|
||||
if (filtro.soloDisponibles === true) {
|
||||
lista = lista.filter((u) => u.status.trim().toUpperCase() === 'DISPONIBLE')
|
||||
}
|
||||
if (filtro.desarrollo !== undefined && filtro.desarrollo !== '') {
|
||||
const desarrollo = filtro.desarrollo.toUpperCase()
|
||||
lista = lista.filter((u) => (u.desarrollo || 'DIAGONAL').toUpperCase() === desarrollo)
|
||||
}
|
||||
return lista
|
||||
}
|
||||
|
||||
/** Una unidad local por código; null si no existe en el espejo */
|
||||
export async function getUnidadLocal(codigo: string): Promise<Unidad | null> {
|
||||
const row = await prisma.unidad.findUnique({ where: { codigo } })
|
||||
return row === null ? null : unidadLocalADto(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lectura con fallback: Monday si está disponible; espejo local si el circuit
|
||||
* breaker lo da por caído o si la llamada lanza AppError 502. Cualquier otro
|
||||
* error se propaga intacto (no se traga nada del camino feliz).
|
||||
*/
|
||||
export async function listarUnidadesConFallback(): Promise<Unidad[]> {
|
||||
if (!saludMonday.estaDisponible()) return getUnidadesLocal()
|
||||
try {
|
||||
return await mondayService.getUnidades()
|
||||
} catch (err) {
|
||||
if (esFalloMonday502(err)) return getUnidadesLocal()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/** Igual que listarUnidadesConFallback pero para una unidad por código */
|
||||
export async function obtenerUnidadConFallback(codigo: string): Promise<Unidad | null> {
|
||||
if (!saludMonday.estaDisponible()) return getUnidadLocal(codigo)
|
||||
try {
|
||||
return await mondayService.getUnidad(codigo)
|
||||
} catch (err) {
|
||||
if (esFalloMonday502(err)) return getUnidadLocal(codigo)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user