erp-transportistas-database-v2/ddl/03-tracking-schema-ddl.sql
Adrian Flores Cortes 7a91823784 feat: Add complete DDL for all transport schemas (01-08)
DDL files created:
- 01-transport-schema-ddl.sql: OT, embarques, viajes, paradas, POD, incidencias
- 02-fleet-schema-ddl.sql: unidades, remolques, operadores, documentos, asignaciones
- 03-tracking-schema-ddl.sql: posiciones GPS, eventos, geocercas, alertas, ETA
- 04-fuel-schema-ddl.sql: cargas combustible, peajes, gastos, anticipos, rendimiento
- 05-maintenance-schema-ddl.sql: planes, programacion, ordenes trabajo, checklist
- 06-carriers-schema-ddl.sql: carriers, documentos, unidades, operadores, scorecard
- 07-billing-transport-ddl.sql: lanes, tarifas, recargos, facturas, fuel surcharge
- 08-compliance-schema-ddl.sql: carta porte CFDI 3.1, HOS NOM-087, inspecciones

Features:
- All tables with tenant_id and RLS policies
- ENUMs for all status and type fields
- Proper indexes for common queries
- PostGIS for geospatial data
- Partitioned tables for high-volume GPS data
- CFDI Carta Porte 3.1 compliant structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 10:26:20 -06:00

370 lines
11 KiB
SQL

-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Tracking DDL
-- =============================================================================
-- Archivo: 03-tracking-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Eventos GPS, geocercas, alertas, posiciones
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS ADICIONALES
-- =============================================================================
CREATE TYPE tracking.tipo_geocerca AS ENUM (
'CLIENTE',
'PROVEEDOR',
'PATIO',
'ZONA_RIESGO',
'CASETA',
'GASOLINERA',
'PUNTO_CONTROL',
'OTRO'
);
CREATE TYPE tracking.severidad_alerta AS ENUM (
'INFO',
'WARNING',
'CRITICAL'
);
CREATE TYPE tracking.tipo_alerta AS ENUM (
'ENTRADA_GEOCERCA',
'SALIDA_GEOCERCA',
'EXCESO_VELOCIDAD',
'PARADA_PROLONGADA',
'DESVIO_RUTA',
'TEMPERATURA_FUERA_RANGO',
'BATERIA_BAJA',
'SIN_SENAL',
'BOTON_PANICO',
'APERTURA_PUERTA',
'CONSUMO_ANOMALO'
);
-- =============================================================================
-- TABLA: posiciones_gps
-- =============================================================================
CREATE TABLE tracking.posiciones_gps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Unidad
unidad_id UUID NOT NULL,
-- Posición
latitud DECIMAL(10, 7) NOT NULL,
longitud DECIMAL(10, 7) NOT NULL,
altitud DECIMAL(8, 2),
-- Velocidad y dirección
velocidad_kmh DECIMAL(6, 2),
rumbo INT, -- 0-360 grados
-- Timestamp
timestamp_gps TIMESTAMPTZ NOT NULL,
timestamp_servidor TIMESTAMPTZ DEFAULT NOW(),
-- Datos adicionales GPS
hdop DECIMAL(4, 2), -- Dilución de precisión horizontal
satelites INT,
-- Estado del vehículo
motor_encendido BOOLEAN,
odometro INT,
-- Proveedor
proveedor_gps VARCHAR(50),
imei VARCHAR(50),
-- Viaje asociado (si está en viaje)
viaje_id UUID,
-- Partición por fecha
fecha_particion DATE NOT NULL DEFAULT CURRENT_DATE
) PARTITION BY RANGE (fecha_particion);
-- Crear particiones mensuales (ejemplo para 2026)
CREATE TABLE tracking.posiciones_gps_2026_01 PARTITION OF tracking.posiciones_gps
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE tracking.posiciones_gps_2026_02 PARTITION OF tracking.posiciones_gps
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE tracking.posiciones_gps_2026_03 PARTITION OF tracking.posiciones_gps
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
CREATE INDEX idx_posicion_unidad_fecha ON tracking.posiciones_gps(unidad_id, timestamp_gps);
CREATE INDEX idx_posicion_viaje ON tracking.posiciones_gps(viaje_id) WHERE viaje_id IS NOT NULL;
CREATE INDEX idx_posicion_geo ON tracking.posiciones_gps USING GIST (
ST_SetSRID(ST_MakePoint(longitud, latitud), 4326)
);
-- =============================================================================
-- TABLA: eventos_tracking
-- =============================================================================
CREATE TABLE tracking.eventos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Viaje
viaje_id UUID NOT NULL,
-- Tipo y fuente
tipo_evento tracking.tipo_evento NOT NULL,
fuente tracking.fuente_evento NOT NULL,
-- Ubicación
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
direccion TEXT,
-- Timestamp
timestamp_evento TIMESTAMPTZ NOT NULL,
timestamp_registro TIMESTAMPTZ DEFAULT NOW(),
-- Datos específicos del evento
datos JSONB,
-- Parada asociada (si aplica)
parada_id UUID,
-- Usuario/Operador que generó
generado_por_id UUID,
generado_por_tipo VARCHAR(20), -- 'OPERADOR', 'SISTEMA', 'USUARIO'
-- Evidencias
evidencias JSONB,
-- Observaciones
observaciones TEXT
);
CREATE INDEX idx_evento_viaje ON tracking.eventos(viaje_id);
CREATE INDEX idx_evento_tipo ON tracking.eventos(tenant_id, tipo_evento);
CREATE INDEX idx_evento_fecha ON tracking.eventos(timestamp_evento);
-- =============================================================================
-- TABLA: geocercas
-- =============================================================================
CREATE TABLE tracking.geocercas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
codigo VARCHAR(50) NOT NULL,
nombre VARCHAR(200) NOT NULL,
tipo tracking.tipo_geocerca NOT NULL,
-- Geometría (polígono o círculo)
es_circular BOOLEAN DEFAULT FALSE,
-- Para geocerca circular
centro_latitud DECIMAL(10, 7),
centro_longitud DECIMAL(10, 7),
radio_metros DECIMAL(10, 2),
-- Para geocerca poligonal (GeoJSON)
poligono GEOMETRY(POLYGON, 4326),
-- Asociación
cliente_id UUID, -- Si tipo = CLIENTE
direccion TEXT,
-- Alertas
alerta_entrada BOOLEAN DEFAULT TRUE,
alerta_salida BOOLEAN DEFAULT TRUE,
tiempo_permanencia_minutos INT, -- Alerta si permanece más de X minutos
-- Configuración
color VARCHAR(7) DEFAULT '#FF0000',
activa BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_geocerca_tenant ON tracking.geocercas(tenant_id);
CREATE INDEX idx_geocerca_tipo ON tracking.geocercas(tenant_id, tipo);
CREATE INDEX idx_geocerca_geo ON tracking.geocercas USING GIST (poligono);
CREATE UNIQUE INDEX idx_geocerca_codigo ON tracking.geocercas(tenant_id, codigo);
-- =============================================================================
-- TABLA: alertas
-- =============================================================================
CREATE TABLE tracking.alertas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Tipo y severidad
tipo tracking.tipo_alerta NOT NULL,
severidad tracking.severidad_alerta NOT NULL,
-- Referencias
unidad_id UUID,
viaje_id UUID,
geocerca_id UUID REFERENCES tracking.geocercas(id),
operador_id UUID,
-- Ubicación
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
-- Descripción
titulo VARCHAR(200) NOT NULL,
mensaje TEXT NOT NULL,
-- Datos específicos
datos JSONB,
-- Timestamp
timestamp_alerta TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Estado
leida BOOLEAN DEFAULT FALSE,
leida_por UUID,
leida_fecha TIMESTAMPTZ,
atendida BOOLEAN DEFAULT FALSE,
atendida_por UUID,
atendida_fecha TIMESTAMPTZ,
resolucion TEXT,
-- Notificaciones enviadas
notificaciones_enviadas JSONB
);
CREATE INDEX idx_alerta_tenant ON tracking.alertas(tenant_id);
CREATE INDEX idx_alerta_unidad ON tracking.alertas(unidad_id);
CREATE INDEX idx_alerta_viaje ON tracking.alertas(viaje_id);
CREATE INDEX idx_alerta_no_atendida ON tracking.alertas(tenant_id, atendida) WHERE atendida = FALSE;
CREATE INDEX idx_alerta_fecha ON tracking.alertas(timestamp_alerta);
-- =============================================================================
-- TABLA: reglas_alerta
-- =============================================================================
CREATE TABLE tracking.reglas_alerta (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
nombre VARCHAR(200) NOT NULL,
descripcion TEXT,
-- Tipo de alerta que genera
tipo_alerta tracking.tipo_alerta NOT NULL,
severidad tracking.severidad_alerta DEFAULT 'WARNING',
-- Condiciones (JSON con configuración)
condiciones JSONB NOT NULL,
-- Ejemplo: { "velocidad_max": 100, "tiempo_parada_max_min": 30, "temp_min": -18, "temp_max": -15 }
-- Aplicabilidad
aplica_todas_unidades BOOLEAN DEFAULT TRUE,
unidades_ids UUID[],
aplica_todos_viajes BOOLEAN DEFAULT TRUE,
-- Notificaciones
notificar_email BOOLEAN DEFAULT TRUE,
notificar_sms BOOLEAN DEFAULT FALSE,
notificar_push BOOLEAN DEFAULT TRUE,
destinatarios JSONB, -- Array de {tipo, id, email, telefono}
-- Estado
activa BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_regla_tenant ON tracking.reglas_alerta(tenant_id);
CREATE INDEX idx_regla_tipo ON tracking.reglas_alerta(tipo_alerta) WHERE activa = TRUE;
-- =============================================================================
-- TABLA: eta_calculado
-- =============================================================================
CREATE TABLE tracking.eta_calculado (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
viaje_id UUID NOT NULL,
parada_id UUID,
-- ETA
eta_original TIMESTAMPTZ,
eta_actual TIMESTAMPTZ NOT NULL,
eta_anterior TIMESTAMPTZ,
-- Cálculo
distancia_restante_km DECIMAL(10, 2),
tiempo_restante_minutos INT,
-- Factores
factor_trafico DECIMAL(3, 2) DEFAULT 1.00,
factor_clima DECIMAL(3, 2) DEFAULT 1.00,
-- Estado
estado VARCHAR(20), -- 'EN_TIEMPO', 'ADELANTADO', 'RETRASADO'
minutos_diferencia INT,
-- Timestamp
calculado_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_eta_viaje ON tracking.eta_calculado(viaje_id);
CREATE INDEX idx_eta_fecha ON tracking.eta_calculado(calculado_at);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE tracking.posiciones_gps ENABLE ROW LEVEL SECURITY;
ALTER TABLE tracking.eventos ENABLE ROW LEVEL SECURITY;
ALTER TABLE tracking.geocercas ENABLE ROW LEVEL SECURITY;
ALTER TABLE tracking.alertas ENABLE ROW LEVEL SECURITY;
ALTER TABLE tracking.reglas_alerta ENABLE ROW LEVEL SECURITY;
ALTER TABLE tracking.eta_calculado ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_posiciones ON tracking.posiciones_gps
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_eventos ON tracking.eventos
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_geocercas ON tracking.geocercas
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_alertas ON tracking.alertas
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_reglas ON tracking.reglas_alerta
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_eta ON tracking.eta_calculado
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE tracking.posiciones_gps IS 'Posiciones GPS de unidades (particionada por fecha)';
COMMENT ON TABLE tracking.eventos IS 'Eventos de tracking durante viajes';
COMMENT ON TABLE tracking.geocercas IS 'Geocercas/zonas de interés';
COMMENT ON TABLE tracking.alertas IS 'Alertas generadas por el sistema de tracking';
COMMENT ON TABLE tracking.reglas_alerta IS 'Reglas de configuración para generar alertas';
COMMENT ON TABLE tracking.eta_calculado IS 'Historial de cálculos de ETA';
-- =============================================================================
-- FIN DDL TRACKING
-- =============================================================================