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:
@@ -0,0 +1,72 @@
|
||||
-- Modo local (degradado) con sincronización a Monday:
|
||||
-- - LeadCaptura pasa a ser la entidad local del embudo (board/etapa/perfil +
|
||||
-- bandera pendienteSync).
|
||||
-- - Unidad: espejo local del inventario (fallback del cotizador).
|
||||
-- - SyncPendiente: cola FIFO de réplicas a Monday.
|
||||
-- Patrón manual (drift histórico): db execute + migrate resolve. NUNCA migrate dev.
|
||||
|
||||
-- AlterTable LeadCaptura (SQLite no soporta IF NOT EXISTS en ADD COLUMN;
|
||||
-- columnas verificadas ausentes con PRAGMA table_info antes de escribir esto)
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "board" TEXT NOT NULL DEFAULT 'LEADS';
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "etapa" TEXT NOT NULL DEFAULT 'NUEVO';
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "origen" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "desarrollo" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "modelo" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "email" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "motivo" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "submotivo" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "datosJson" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "mondayItemId" TEXT;
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "pendienteSync" BOOLEAN NOT NULL DEFAULT false;
|
||||
-- SQLite no admite DEFAULT CURRENT_TIMESTAMP en ADD COLUMN: default constante
|
||||
-- + backfill. Prisma siempre escribe updatedAt al crear/actualizar (@updatedAt).
|
||||
ALTER TABLE "LeadCaptura" ADD COLUMN "updatedAt" DATETIME NOT NULL DEFAULT '2026-09-05 00:00:00';
|
||||
|
||||
-- Backfill: rows viejas ya estaban en Monday; su item es leadId
|
||||
UPDATE "LeadCaptura" SET "updatedAt" = CURRENT_TIMESTAMP, "mondayItemId" = "leadId";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "LeadCaptura_mondayItemId_idx" ON "LeadCaptura"("mondayItemId");
|
||||
|
||||
-- CreateTable Unidad
|
||||
CREATE TABLE IF NOT EXISTS "Unidad" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"codigo" TEXT NOT NULL,
|
||||
"desarrollo" TEXT NOT NULL,
|
||||
"modelo" TEXT NOT NULL DEFAULT '',
|
||||
"manzana" TEXT NOT NULL DEFAULT '',
|
||||
"lote" TEXT NOT NULL DEFAULT '',
|
||||
"supConstruccion" REAL NOT NULL DEFAULT 0,
|
||||
"supExcedente" REAL NOT NULL DEFAULT 0,
|
||||
"supEstacionamiento" REAL NOT NULL DEFAULT 0,
|
||||
"precioLista" REAL NOT NULL DEFAULT 0,
|
||||
"precioAcabados" REAL NOT NULL DEFAULT 0,
|
||||
"status" TEXT NOT NULL DEFAULT 'DISPONIBLE',
|
||||
"etapa" TEXT NOT NULL DEFAULT '',
|
||||
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Unidad_codigo_key" ON "Unidad"("codigo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "Unidad_desarrollo_idx" ON "Unidad"("desarrollo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "Unidad_status_idx" ON "Unidad"("status");
|
||||
|
||||
-- CreateTable SyncPendiente
|
||||
CREATE TABLE IF NOT EXISTS "SyncPendiente" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"tipo" TEXT NOT NULL,
|
||||
"payloadJson" TEXT NOT NULL,
|
||||
"referenciaLocal" TEXT,
|
||||
"estado" TEXT NOT NULL DEFAULT 'PENDIENTE',
|
||||
"intentos" INTEGER NOT NULL DEFAULT 0,
|
||||
"ultimoError" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "SyncPendiente_estado_createdAt_idx" ON "SyncPendiente"("estado", "createdAt");
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user