- 03a-gps-devices-ddl.sql: GPS device tracking schema - 09-dispatch-schema-ddl.sql: Dispatch management schema - 10-offline-schema-ddl.sql: Offline operation schema Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
359 lines
12 KiB
PL/PgSQL
359 lines
12 KiB
PL/PgSQL
-- =============================================================================
|
|
-- ERP TRANSPORTISTAS - Schema Offline DDL
|
|
-- =============================================================================
|
|
-- Archivo: 10-offline-schema-ddl.sql
|
|
-- Version: 1.0.0
|
|
-- Fecha: 2026-01-28
|
|
-- Descripcion: Cola de operaciones offline para sincronizacion
|
|
-- Basado en: OfflineQueue.entity.ts (TASK-007)
|
|
-- =============================================================================
|
|
|
|
-- =============================================================================
|
|
-- SCHEMA OFFLINE
|
|
-- =============================================================================
|
|
|
|
CREATE SCHEMA IF NOT EXISTS offline;
|
|
COMMENT ON SCHEMA offline IS 'Sistema de sincronizacion offline para operaciones en campo';
|
|
|
|
-- =============================================================================
|
|
-- TIPOS ENUMERADOS
|
|
-- =============================================================================
|
|
|
|
-- Tipos de operacion que pueden ser encoladas offline
|
|
CREATE TYPE offline.tipo_operacion_offline AS ENUM (
|
|
-- GPS Operations
|
|
'GPS_POSICION',
|
|
'GPS_EVENTO',
|
|
|
|
-- Dispatch Operations
|
|
'VIAJE_ESTADO',
|
|
'VIAJE_EVENTO',
|
|
'CHECKIN',
|
|
'CHECKOUT',
|
|
|
|
-- POD Operations
|
|
'POD_FOTO',
|
|
'POD_FIRMA',
|
|
'POD_DOCUMENTO',
|
|
|
|
-- Checklist Operations
|
|
'CHECKLIST_ITEM',
|
|
'CHECKLIST_COMPLETADO',
|
|
|
|
-- Generic
|
|
'CUSTOM'
|
|
);
|
|
|
|
COMMENT ON TYPE offline.tipo_operacion_offline IS 'Tipos de operaciones que pueden encolarse offline';
|
|
|
|
-- Estados de sincronizacion para operaciones encoladas
|
|
CREATE TYPE offline.estado_sincronizacion AS ENUM (
|
|
'PENDIENTE', -- Esperando sincronizacion
|
|
'EN_PROCESO', -- Sincronizacion en progreso
|
|
'COMPLETADO', -- Sincronizado exitosamente
|
|
'ERROR', -- Error durante sincronizacion
|
|
'CONFLICTO', -- Conflicto de datos detectado
|
|
'DESCARTADO' -- Operacion descartada
|
|
);
|
|
|
|
COMMENT ON TYPE offline.estado_sincronizacion IS 'Estados del proceso de sincronizacion';
|
|
|
|
-- Niveles de prioridad para cola de sincronizacion
|
|
CREATE TYPE offline.prioridad_sync AS ENUM (
|
|
'1', -- CRITICA: Posiciones GPS, eventos de seguridad
|
|
'2', -- ALTA: POD, cambios de estado
|
|
'3', -- NORMAL: Items de checklist, notas
|
|
'4' -- BAJA: Fotos, documentos
|
|
);
|
|
|
|
COMMENT ON TYPE offline.prioridad_sync IS 'Prioridades de sincronizacion (1=critica, 4=baja)';
|
|
|
|
-- =============================================================================
|
|
-- TABLA: offline_queue
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE offline.offline_queue (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
|
|
|
|
-- Referencias opcionales
|
|
dispositivo_id UUID, -- Dispositivo que genero la operacion
|
|
usuario_id UUID, -- Usuario que genero la operacion
|
|
unidad_id UUID, -- Unidad relacionada
|
|
viaje_id UUID, -- Viaje relacionado
|
|
|
|
-- Detalles de la operacion
|
|
tipo_operacion offline.tipo_operacion_offline NOT NULL,
|
|
estado offline.estado_sincronizacion DEFAULT 'PENDIENTE',
|
|
prioridad offline.prioridad_sync DEFAULT '3',
|
|
|
|
-- Payload - datos a sincronizar (JSONB)
|
|
payload JSONB NOT NULL,
|
|
|
|
-- Metadata de la operacion
|
|
endpoint_destino VARCHAR(255) NOT NULL, -- URL/endpoint destino
|
|
metodo_http VARCHAR(10) DEFAULT 'POST', -- GET, POST, PUT, PATCH, DELETE
|
|
|
|
-- Timestamps offline
|
|
creado_offline_en TIMESTAMPTZ NOT NULL, -- Timestamp cuando se creo offline
|
|
cliente_id VARCHAR(100), -- UUID generado en cliente para deduplicacion
|
|
|
|
-- Tracking de sincronizacion
|
|
intentos_sync INT DEFAULT 0, -- Contador de intentos
|
|
max_intentos INT DEFAULT 5, -- Maximo de intentos permitidos
|
|
ultimo_intento_en TIMESTAMPTZ, -- Timestamp del ultimo intento
|
|
sincronizado_en TIMESTAMPTZ, -- Timestamp cuando se sincronizo exitosamente
|
|
|
|
-- Manejo de errores
|
|
ultimo_error TEXT, -- Ultimo mensaje de error
|
|
historial_errores JSONB DEFAULT '[]', -- Array de {timestamp, error}
|
|
|
|
-- Resolucion de conflictos
|
|
version_servidor INT, -- Version del servidor para conflictos
|
|
resolucion_conflicto VARCHAR(50), -- 'CLIENT_WINS', 'SERVER_WINS', 'MERGE', 'MANUAL'
|
|
|
|
-- Optimizacion de ancho de banda
|
|
tamano_bytes INT, -- Tamano del payload en bytes
|
|
comprimido BOOLEAN DEFAULT FALSE, -- Si el payload esta comprimido
|
|
|
|
-- Auditoria
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- INDICES
|
|
-- =============================================================================
|
|
|
|
-- Indice principal por tenant
|
|
CREATE INDEX idx_offline_queue_tenant
|
|
ON offline.offline_queue(tenant_id);
|
|
|
|
-- Indice por dispositivo para sincronizacion
|
|
CREATE INDEX idx_offline_queue_dispositivo
|
|
ON offline.offline_queue(dispositivo_id)
|
|
WHERE dispositivo_id IS NOT NULL;
|
|
|
|
-- Indice por estado para procesamiento de cola
|
|
CREATE INDEX idx_offline_queue_estado
|
|
ON offline.offline_queue(estado);
|
|
|
|
-- Indice por prioridad y timestamp para ordenamiento de cola FIFO con prioridad
|
|
CREATE INDEX idx_offline_queue_prioridad
|
|
ON offline.offline_queue(prioridad, creado_offline_en);
|
|
|
|
-- Indice compuesto para operaciones pendientes por tenant
|
|
CREATE INDEX idx_offline_queue_pendientes
|
|
ON offline.offline_queue(tenant_id, estado, prioridad)
|
|
WHERE estado IN ('PENDIENTE', 'EN_PROCESO', 'ERROR');
|
|
|
|
-- Indice para buscar por cliente_id (deduplicacion)
|
|
CREATE UNIQUE INDEX idx_offline_queue_cliente_id
|
|
ON offline.offline_queue(tenant_id, cliente_id)
|
|
WHERE cliente_id IS NOT NULL;
|
|
|
|
-- Indice por viaje para consultas relacionadas
|
|
CREATE INDEX idx_offline_queue_viaje
|
|
ON offline.offline_queue(viaje_id)
|
|
WHERE viaje_id IS NOT NULL;
|
|
|
|
-- Indice por unidad para consultas relacionadas
|
|
CREATE INDEX idx_offline_queue_unidad
|
|
ON offline.offline_queue(unidad_id)
|
|
WHERE unidad_id IS NOT NULL;
|
|
|
|
-- Indice para limpieza de registros antiguos sincronizados
|
|
CREATE INDEX idx_offline_queue_sincronizado
|
|
ON offline.offline_queue(sincronizado_en)
|
|
WHERE estado = 'COMPLETADO';
|
|
|
|
-- =============================================================================
|
|
-- TRIGGER: Actualizar updated_at automaticamente
|
|
-- =============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION offline.update_offline_queue_timestamp()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_offline_queue_updated_at
|
|
BEFORE UPDATE ON offline.offline_queue
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION offline.update_offline_queue_timestamp();
|
|
|
|
-- =============================================================================
|
|
-- RLS POLICIES
|
|
-- =============================================================================
|
|
|
|
ALTER TABLE offline.offline_queue ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_offline_queue ON offline.offline_queue
|
|
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
|
|
|
-- =============================================================================
|
|
-- COMENTARIOS
|
|
-- =============================================================================
|
|
|
|
COMMENT ON TABLE offline.offline_queue IS 'Cola de operaciones pendientes de sincronizacion desde dispositivos offline';
|
|
|
|
COMMENT ON COLUMN offline.offline_queue.tipo_operacion IS 'Tipo de operacion: GPS, POD, Checklist, etc.';
|
|
COMMENT ON COLUMN offline.offline_queue.estado IS 'Estado actual de la sincronizacion';
|
|
COMMENT ON COLUMN offline.offline_queue.prioridad IS 'Prioridad de sincronizacion (1=critica, 4=baja)';
|
|
COMMENT ON COLUMN offline.offline_queue.payload IS 'Datos de la operacion en formato JSON';
|
|
COMMENT ON COLUMN offline.offline_queue.endpoint_destino IS 'URL o endpoint API destino para la sincronizacion';
|
|
COMMENT ON COLUMN offline.offline_queue.creado_offline_en IS 'Timestamp original cuando la operacion fue creada offline';
|
|
COMMENT ON COLUMN offline.offline_queue.cliente_id IS 'UUID generado en el cliente para deduplicacion de operaciones';
|
|
COMMENT ON COLUMN offline.offline_queue.intentos_sync IS 'Numero de intentos de sincronizacion realizados';
|
|
COMMENT ON COLUMN offline.offline_queue.historial_errores IS 'Historial de errores como array JSON [{timestamp, error}]';
|
|
COMMENT ON COLUMN offline.offline_queue.resolucion_conflicto IS 'Estrategia de resolucion: CLIENT_WINS, SERVER_WINS, MERGE, MANUAL';
|
|
COMMENT ON COLUMN offline.offline_queue.comprimido IS 'Indica si el payload esta comprimido (gzip)';
|
|
|
|
-- =============================================================================
|
|
-- FUNCIONES AUXILIARES
|
|
-- =============================================================================
|
|
|
|
-- Funcion para obtener operaciones pendientes ordenadas por prioridad
|
|
CREATE OR REPLACE FUNCTION offline.get_pending_operations(
|
|
p_tenant_id UUID,
|
|
p_limit INT DEFAULT 100
|
|
)
|
|
RETURNS TABLE (
|
|
id UUID,
|
|
tipo_operacion offline.tipo_operacion_offline,
|
|
prioridad offline.prioridad_sync,
|
|
payload JSONB,
|
|
endpoint_destino VARCHAR(255),
|
|
metodo_http VARCHAR(10),
|
|
creado_offline_en TIMESTAMPTZ,
|
|
intentos_sync INT
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
oq.id,
|
|
oq.tipo_operacion,
|
|
oq.prioridad,
|
|
oq.payload,
|
|
oq.endpoint_destino,
|
|
oq.metodo_http,
|
|
oq.creado_offline_en,
|
|
oq.intentos_sync
|
|
FROM offline.offline_queue oq
|
|
WHERE oq.tenant_id = p_tenant_id
|
|
AND oq.estado IN ('PENDIENTE', 'ERROR')
|
|
AND oq.intentos_sync < oq.max_intentos
|
|
ORDER BY oq.prioridad ASC, oq.creado_offline_en ASC
|
|
LIMIT p_limit;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Funcion para marcar operacion como en proceso
|
|
CREATE OR REPLACE FUNCTION offline.start_sync_operation(
|
|
p_operation_id UUID
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_updated INT;
|
|
BEGIN
|
|
UPDATE offline.offline_queue
|
|
SET
|
|
estado = 'EN_PROCESO',
|
|
ultimo_intento_en = NOW(),
|
|
intentos_sync = intentos_sync + 1
|
|
WHERE id = p_operation_id
|
|
AND estado IN ('PENDIENTE', 'ERROR');
|
|
|
|
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
|
RETURN v_updated > 0;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para completar operacion exitosamente
|
|
CREATE OR REPLACE FUNCTION offline.complete_sync_operation(
|
|
p_operation_id UUID
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_updated INT;
|
|
BEGIN
|
|
UPDATE offline.offline_queue
|
|
SET
|
|
estado = 'COMPLETADO',
|
|
sincronizado_en = NOW()
|
|
WHERE id = p_operation_id
|
|
AND estado = 'EN_PROCESO';
|
|
|
|
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
|
RETURN v_updated > 0;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para registrar error en operacion
|
|
CREATE OR REPLACE FUNCTION offline.fail_sync_operation(
|
|
p_operation_id UUID,
|
|
p_error_message TEXT
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_updated INT;
|
|
v_historial JSONB;
|
|
BEGIN
|
|
-- Obtener historial actual
|
|
SELECT historial_errores INTO v_historial
|
|
FROM offline.offline_queue
|
|
WHERE id = p_operation_id;
|
|
|
|
-- Agregar nuevo error al historial
|
|
v_historial = v_historial || jsonb_build_object(
|
|
'timestamp', NOW()::TEXT,
|
|
'error', p_error_message
|
|
);
|
|
|
|
UPDATE offline.offline_queue
|
|
SET
|
|
estado = 'ERROR',
|
|
ultimo_error = p_error_message,
|
|
historial_errores = v_historial
|
|
WHERE id = p_operation_id
|
|
AND estado = 'EN_PROCESO';
|
|
|
|
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
|
RETURN v_updated > 0;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para limpiar operaciones antiguas completadas
|
|
CREATE OR REPLACE FUNCTION offline.cleanup_old_operations(
|
|
p_days_to_keep INT DEFAULT 30
|
|
)
|
|
RETURNS INT AS $$
|
|
DECLARE
|
|
v_deleted INT;
|
|
BEGIN
|
|
DELETE FROM offline.offline_queue
|
|
WHERE estado = 'COMPLETADO'
|
|
AND sincronizado_en < NOW() - (p_days_to_keep || ' days')::INTERVAL;
|
|
|
|
GET DIAGNOSTICS v_deleted = ROW_COUNT;
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- =============================================================================
|
|
-- GRANTS
|
|
-- =============================================================================
|
|
|
|
-- Permisos para funciones
|
|
GRANT EXECUTE ON FUNCTION offline.get_pending_operations(UUID, INT) TO PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION offline.start_sync_operation(UUID) TO PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION offline.complete_sync_operation(UUID) TO PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION offline.fail_sync_operation(UUID, TEXT) TO PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION offline.cleanup_old_operations(INT) TO PUBLIC;
|
|
|
|
-- =============================================================================
|
|
-- FIN DDL OFFLINE
|
|
-- =============================================================================
|