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

@@ -157,16 +157,86 @@ model LeadOportunidad {
// Registro local de qué usuario del ecosistema capturó cada lead (alta de
// afluencia). Monday no guarda al capturista (el creador del item siempre es
// el token del API); esta tabla alimenta "Mis capturas" del panel Mi día.
// Además es la entidad local del embudo en modo local (Monday caído): guarda
// el board/etapa actuales y el perfil de la captura hasta que el sync worker
// la replique en Monday.
model LeadCaptura {
id String @id @default(uuid())
leadId String @unique // item del tablero Leads (Monday)
leadId String @unique // item del tablero Leads (Monday); en modo local: id local loc_<uuid>
oportunidadId String? // item del tablero Oportunidades ligado al capturar
usuarioId String // Usuario.id del capturista
nombreLead String
telefono String?
// Board del embudo donde vive el registro: LEADS | OPORTUNIDADES | APARTADO
board String @default("LEADS")
// Etapa/estado local actual (NUEVO al capturar)
etapa String @default("NUEVO")
origen String?
desarrollo String?
modelo String?
email String?
// Motivo / submotivo del descarte
motivo String?
submotivo String?
// Perfilamiento completo de afluencia / payload extra (JSON serializado)
datosJson String?
// Item de Monday una vez sincronizado (rows viejas: = leadId)
mondayItemId String?
// true = falta replicar a Monday (lo drena el sync worker)
pendienteSync Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
@@index([usuarioId])
@@index([mondayItemId])
}
// Espejo local del inventario (tablero Unidades de Monday). Lo siembra
// scripts/seed-unidades-locales.js desde el INVENTARIO_SEED del cotizador y lo
// mantiene fresco el write-through de monday.service.getUnidades(); es el
// fallback de lectura del cotizador en modo local.
model Unidad {
id String @id @default(uuid())
codigo String @unique // 'U-101', 'ATELIER-10', 'SENDA-411-1-7', ...
desarrollo String // DIAGONAL | ATELIER | ESTANZA | SENDA | SOLANA
modelo String @default("")
manzana String @default("")
lote String @default("")
supConstruccion Float @default(0)
supExcedente Float @default(0)
supEstacionamiento Float @default(0)
precioLista Float @default(0)
precioAcabados Float @default(0)
status String @default("DISPONIBLE") // DISPONIBLE | APARTADO | VENDIDO | ESCRITURADO | BLOQUEADO | ...
etapa String @default("") // etapa del pipeline de venta ('' si no aplica)
updatedAt DateTime @updatedAt @default(now())
@@index([desarrollo])
@@index([status])
}
// Cola de sincronización a Monday (modo local). Cada operación hecha contra la
// base local mientras Monday está caído se encola aquí; sync-monday.service la
// drena FIFO (createdAt asc) cuando Monday vuelve. Estados: PENDIENTE |
// PROCESANDO | HECHO | ERROR. Idempotencia vía AutomatizacionState 'sync:<id>'.
model SyncPendiente {
id String @id @default(uuid())
// CREAR_LEAD | CREAR_AFLUENCIA | ACTUALIZAR_AFLUENCIA | CAMBIO_ETAPA |
// DESCARTE | CONVERSION | CREAR_CONTACTO | ACTUALIZAR_CONTACTO |
// CREAR_APARTADO | COTIZACION_OPORTUNIDAD | CHECKBOX_DOC |
// APARTO_OPORTUNIDAD | LIQUIDADO_ESCRITURAR | ASIGNAR_PERSONA |
// DISPONIBILIDAD_UNIDAD
tipo String
payloadJson String
// Referencia local (leadId / clienteId / cotizacionId) para actualizar ids
referenciaLocal String?
estado String @default("PENDIENTE")
intentos Int @default(0)
ultimoError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([estado, createdAt])
}
// Estado persistente de las automatizaciones de Monday (round-robin, recordatorios).