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 @@ import { config } from '../config/index.js'
|
||||
import { mondayService, type MondayService } from '../services/monday.service.js'
|
||||
import { moverAGrupoSegunEstado } from '../services/grupos-estado.service.js'
|
||||
import { prisma } from '../services/prisma.js'
|
||||
import { saludMonday } from '../services/salud-monday.service.js'
|
||||
import { crmLocal, encolarSyncSeguro, esIdLocal } from '../services/crm-local.service.js'
|
||||
import { esFalloMonday } from '../services/sync-monday.service.js'
|
||||
import { checkboxParaTipoDocumento } from './clientes.controller.js'
|
||||
|
||||
/**
|
||||
@@ -54,7 +57,24 @@ export async function cambiarEstadoApartado(
|
||||
export async function cambiarEstadoApartadoEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const body = cambiarEstadoSchema.parse(req.body)
|
||||
const itemId = String(req.params['itemId'])
|
||||
await cambiarEstadoApartado(mondayService, itemId, body.estado)
|
||||
// Modo local (id loc_* o Monday caído / fallback 502): fase en la fila local
|
||||
// + CAMBIO_ETAPA encolado; ids Monday viejos sin fila crean sombra
|
||||
if (esIdLocal(itemId) || !saludMonday.estaDisponible()) {
|
||||
await crmLocal.cambiarEstado(itemId, body.estado, 'APARTADO', {
|
||||
usuarioId: req.usuarioToken?.sub,
|
||||
sombra: !esIdLocal(itemId),
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
await cambiarEstadoApartado(mondayService, itemId, body.estado)
|
||||
} catch (err) {
|
||||
if (!esFalloMonday(err)) throw err
|
||||
await crmLocal.cambiarEstado(itemId, body.estado, 'APARTADO', {
|
||||
usuarioId: req.usuarioToken?.sub,
|
||||
sombra: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
res.json({ itemId, estado: body.estado })
|
||||
}
|
||||
|
||||
@@ -104,12 +124,28 @@ export const uploadDocumentosApartado = multer({
|
||||
/**
|
||||
* Cliente local ligado al apartado de Monday; si aún no existe (apartado
|
||||
* creado sin pasar por el alta progresiva), se crea la ficha mínima para que
|
||||
* los documentos tengan dónde vivir.
|
||||
* los documentos tengan dónde vivir. El nombre se toma de Monday; si Monday
|
||||
* está caído o el id es local (loc_*), se usa el de la fila LeadCaptura del
|
||||
* apartado (board APARTADO, creada por el "Aparto" local) como fallback.
|
||||
*/
|
||||
async function clienteDeApartado(itemId: string, capturadoPorId?: string) {
|
||||
const existente = await prisma.cliente.findFirst({ where: { mondayApartadoId: itemId } })
|
||||
if (existente) return existente
|
||||
const nombre = await mondayService.obtenerNombreItem(itemId)
|
||||
let nombre: string | null = null
|
||||
if (!esIdLocal(itemId) && saludMonday.estaDisponible()) {
|
||||
try {
|
||||
nombre = await mondayService.obtenerNombreItem(itemId)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`No se pudo leer el nombre del apartado ${itemId} en Monday; se usa el nombre local:`, err)
|
||||
}
|
||||
}
|
||||
if (nombre === null) {
|
||||
const captura = await prisma.leadCaptura.findFirst({
|
||||
where: { OR: [{ leadId: itemId }, { mondayItemId: itemId }] },
|
||||
})
|
||||
nombre = captura?.nombreLead ?? 'Cliente'
|
||||
}
|
||||
return prisma.cliente.create({
|
||||
data: {
|
||||
nombreCompleto: nombre,
|
||||
@@ -151,14 +187,20 @@ export async function subirDocumentosApartado(req: Request, res: Response): Prom
|
||||
)
|
||||
|
||||
// Prende el checkbox del documento en el item de Apartado (si el tipo tiene
|
||||
// checkbox); un fallo de Monday no rompe la subida.
|
||||
// checkbox); un fallo de Monday no rompe la subida: queda encolado
|
||||
// (CHECKBOX_DOC) y el worker lo aplica al sincronizar.
|
||||
const checkbox = checkboxParaTipoDocumento(tipo)
|
||||
if (checkbox !== null) {
|
||||
try {
|
||||
await mondayService.cambiarValorColumna(config.monday.boards.apartado, itemId, checkbox, true)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`No se pudo marcar el checkbox "${checkbox}" del apartado ${itemId}:`, err)
|
||||
if (esIdLocal(itemId) || !saludMonday.estaDisponible()) {
|
||||
await encolarSyncSeguro('CHECKBOX_DOC', { checkbox, itemId }, itemId)
|
||||
} else {
|
||||
try {
|
||||
await mondayService.cambiarValorColumna(config.monday.boards.apartado, itemId, checkbox, true)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`No se pudo marcar el checkbox "${checkbox}" del apartado ${itemId}:`, err)
|
||||
await encolarSyncSeguro('CHECKBOX_DOC', { checkbox, itemId }, itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
res.status(201).json(documentos)
|
||||
|
||||
Reference in New Issue
Block a user