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>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 10:26:20 -06:00
parent c93e2b1e0e
commit 7a91823784
8 changed files with 2976 additions and 0 deletions

View File

@ -0,0 +1,441 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Transport DDL
-- =============================================================================
-- Archivo: 01-transport-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Ordenes de transporte, embarques, viajes, paradas, POD
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS ADICIONALES
-- =============================================================================
CREATE TYPE transport.estado_orden AS ENUM (
'BORRADOR',
'CONFIRMADA',
'ASIGNADA',
'EN_PROCESO',
'COMPLETADA',
'FACTURADA',
'CANCELADA'
);
CREATE TYPE transport.tipo_carga AS ENUM (
'GENERAL',
'PELIGROSA',
'REFRIGERADA',
'SOBREDIMENSIONADA',
'GRANEL',
'LIQUIDOS',
'CONTENEDOR',
'AUTOMOVILES'
);
CREATE TYPE transport.estado_pod AS ENUM (
'PENDIENTE',
'PARCIAL',
'COMPLETO',
'RECHAZADO'
);
-- =============================================================================
-- TABLA: ordenes_transporte (OT)
-- =============================================================================
CREATE TABLE transport.ordenes_transporte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
codigo VARCHAR(50) NOT NULL,
referencia_cliente VARCHAR(100),
-- Cliente (Shipper)
shipper_id UUID NOT NULL,
shipper_nombre VARCHAR(200) NOT NULL,
-- Destinatario (Consignee)
consignee_id UUID NOT NULL,
consignee_nombre VARCHAR(200) NOT NULL,
-- Origen
origen_direccion TEXT NOT NULL,
origen_codigo_postal VARCHAR(10),
origen_ciudad VARCHAR(100),
origen_estado VARCHAR(100),
origen_pais VARCHAR(3) DEFAULT 'MEX',
origen_latitud DECIMAL(10, 7),
origen_longitud DECIMAL(10, 7),
origen_contacto VARCHAR(200),
origen_telefono VARCHAR(30),
-- Destino
destino_direccion TEXT NOT NULL,
destino_codigo_postal VARCHAR(10),
destino_ciudad VARCHAR(100),
destino_estado VARCHAR(100),
destino_pais VARCHAR(3) DEFAULT 'MEX',
destino_latitud DECIMAL(10, 7),
destino_longitud DECIMAL(10, 7),
destino_contacto VARCHAR(200),
destino_telefono VARCHAR(30),
-- Fechas programadas
fecha_recoleccion_programada TIMESTAMPTZ,
fecha_entrega_programada TIMESTAMPTZ,
ventana_recoleccion_inicio TIME,
ventana_recoleccion_fin TIME,
ventana_entrega_inicio TIME,
ventana_entrega_fin TIME,
-- Carga
tipo_carga transport.tipo_carga DEFAULT 'GENERAL',
descripcion_carga TEXT,
peso_kg DECIMAL(12, 2),
volumen_m3 DECIMAL(12, 4),
piezas INT,
pallets INT,
valor_declarado DECIMAL(15, 2),
moneda VARCHAR(3) DEFAULT 'MXN',
-- Requisitos
requiere_temperatura BOOLEAN DEFAULT FALSE,
temperatura_min DECIMAL(5, 2),
temperatura_max DECIMAL(5, 2),
requiere_gps BOOLEAN DEFAULT FALSE,
requiere_escolta BOOLEAN DEFAULT FALSE,
requiere_cita BOOLEAN DEFAULT FALSE,
instrucciones_especiales TEXT,
-- Servicio y tarifa
modalidad_servicio transport.modalidad_servicio DEFAULT 'FTL',
tarifa_id UUID,
tarifa_base DECIMAL(15, 2),
recargos DECIMAL(15, 2) DEFAULT 0,
descuentos DECIMAL(15, 2) DEFAULT 0,
subtotal DECIMAL(15, 2),
iva DECIMAL(15, 2),
total DECIMAL(15, 2),
-- Estado
estado transport.estado_orden DEFAULT 'BORRADOR',
-- Asignación
viaje_id UUID,
embarque_id UUID,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by_id UUID,
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_ot_tenant_codigo UNIQUE (tenant_id, codigo)
);
CREATE INDEX idx_ot_tenant ON transport.ordenes_transporte(tenant_id);
CREATE INDEX idx_ot_estado ON transport.ordenes_transporte(tenant_id, estado);
CREATE INDEX idx_ot_shipper ON transport.ordenes_transporte(tenant_id, shipper_id);
CREATE INDEX idx_ot_fechas ON transport.ordenes_transporte(tenant_id, fecha_recoleccion_programada);
CREATE INDEX idx_ot_viaje ON transport.ordenes_transporte(viaje_id) WHERE viaje_id IS NOT NULL;
-- =============================================================================
-- TABLA: embarques (Agrupación de OTs)
-- =============================================================================
CREATE TABLE transport.embarques (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
codigo VARCHAR(50) NOT NULL,
descripcion VARCHAR(500),
-- Cliente principal
cliente_id UUID NOT NULL,
-- Totales consolidados
total_ots INT DEFAULT 0,
peso_total_kg DECIMAL(12, 2),
volumen_total_m3 DECIMAL(12, 4),
-- Estado
estado VARCHAR(20) DEFAULT 'ABIERTO',
-- Viaje asignado
viaje_id UUID,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_embarque_tenant_codigo UNIQUE (tenant_id, codigo)
);
CREATE INDEX idx_embarque_tenant ON transport.embarques(tenant_id);
CREATE INDEX idx_embarque_cliente ON transport.embarques(tenant_id, cliente_id);
-- =============================================================================
-- TABLA: viajes
-- =============================================================================
CREATE TABLE transport.viajes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
codigo VARCHAR(50) NOT NULL,
-- Unidad y operador (referencias a fleet schema)
unidad_id UUID NOT NULL,
remolque_id UUID,
operador_id UUID NOT NULL,
-- Ruta
origen_principal VARCHAR(200),
destino_principal VARCHAR(200),
distancia_estimada_km DECIMAL(10, 2),
tiempo_estimado_horas DECIMAL(6, 2),
-- Fechas programadas
fecha_salida_programada TIMESTAMPTZ,
fecha_llegada_programada TIMESTAMPTZ,
-- Fechas reales
fecha_salida_real TIMESTAMPTZ,
fecha_llegada_real TIMESTAMPTZ,
-- Kilometraje
km_inicio INT,
km_fin INT,
km_recorridos INT GENERATED ALWAYS AS (km_fin - km_inicio) STORED,
-- Estado
estado transport.estado_viaje DEFAULT 'BORRADOR',
-- Checklist pre-viaje
checklist_completado BOOLEAN DEFAULT FALSE,
checklist_fecha TIMESTAMPTZ,
checklist_observaciones TEXT,
-- Sellos
sellos_salida JSONB,
sellos_llegada JSONB,
-- Costos
costo_combustible DECIMAL(15, 2) DEFAULT 0,
costo_peajes DECIMAL(15, 2) DEFAULT 0,
costo_viaticos DECIMAL(15, 2) DEFAULT 0,
costo_otros DECIMAL(15, 2) DEFAULT 0,
costo_total DECIMAL(15, 2) DEFAULT 0,
-- Ingresos
ingreso_total DECIMAL(15, 2) DEFAULT 0,
margen DECIMAL(15, 2) GENERATED ALWAYS AS (ingreso_total - costo_total) STORED,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by_id UUID,
CONSTRAINT uq_viaje_tenant_codigo UNIQUE (tenant_id, codigo)
);
CREATE INDEX idx_viaje_tenant ON transport.viajes(tenant_id);
CREATE INDEX idx_viaje_estado ON transport.viajes(tenant_id, estado);
CREATE INDEX idx_viaje_unidad ON transport.viajes(tenant_id, unidad_id);
CREATE INDEX idx_viaje_operador ON transport.viajes(tenant_id, operador_id);
CREATE INDEX idx_viaje_fechas ON transport.viajes(tenant_id, fecha_salida_programada);
-- =============================================================================
-- TABLA: paradas_viaje (Multi-paradas)
-- =============================================================================
CREATE TABLE transport.paradas_viaje (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
viaje_id UUID NOT NULL REFERENCES transport.viajes(id),
-- Secuencia
secuencia INT NOT NULL,
-- Tipo de parada
tipo VARCHAR(20) NOT NULL, -- 'RECOLECCION', 'ENTREGA', 'ESCALA'
-- Ubicación
direccion TEXT NOT NULL,
codigo_postal VARCHAR(10),
ciudad VARCHAR(100),
estado VARCHAR(100),
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
-- Contacto
contacto_nombre VARCHAR(200),
contacto_telefono VARCHAR(30),
-- Programación
hora_programada_llegada TIMESTAMPTZ,
hora_programada_salida TIMESTAMPTZ,
-- Real
hora_real_llegada TIMESTAMPTZ,
hora_real_salida TIMESTAMPTZ,
-- OTs asociadas
ots_ids UUID[],
-- Estado
estado VARCHAR(20) DEFAULT 'PENDIENTE',
-- Observaciones
observaciones TEXT,
CONSTRAINT uq_parada_viaje_secuencia UNIQUE (viaje_id, secuencia)
);
CREATE INDEX idx_parada_viaje ON transport.paradas_viaje(viaje_id);
-- =============================================================================
-- TABLA: pod (Proof of Delivery)
-- =============================================================================
CREATE TABLE transport.pod (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
viaje_id UUID NOT NULL REFERENCES transport.viajes(id),
parada_id UUID REFERENCES transport.paradas_viaje(id),
ot_id UUID REFERENCES transport.ordenes_transporte(id),
-- Estado POD
estado transport.estado_pod DEFAULT 'PENDIENTE',
-- Recepción
receptor_nombre VARCHAR(200),
receptor_identificacion VARCHAR(50),
fecha_recepcion TIMESTAMPTZ,
-- Firma digital
firma_digital TEXT, -- Base64 de la imagen de firma
-- Evidencias (URLs o IDs de archivos)
fotos_entrega JSONB,
-- Cantidades
piezas_entregadas INT,
piezas_rechazadas INT,
piezas_danadas INT,
-- Observaciones
observaciones TEXT,
motivo_rechazo TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID,
CONSTRAINT uq_pod_ot UNIQUE (ot_id)
);
CREATE INDEX idx_pod_viaje ON transport.pod(viaje_id);
CREATE INDEX idx_pod_estado ON transport.pod(tenant_id, estado);
-- =============================================================================
-- TABLA: incidencias
-- =============================================================================
CREATE TABLE transport.incidencias (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Referencias
viaje_id UUID REFERENCES transport.viajes(id),
ot_id UUID REFERENCES transport.ordenes_transporte(id),
unidad_id UUID,
operador_id UUID,
-- Incidencia
codigo VARCHAR(50) NOT NULL,
tipo transport.tipo_incidencia NOT NULL,
descripcion TEXT NOT NULL,
-- Ubicación
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
direccion TEXT,
-- Fecha/hora
fecha_incidencia TIMESTAMPTZ NOT NULL,
fecha_reporte TIMESTAMPTZ DEFAULT NOW(),
-- Impacto
tiempo_perdido_horas DECIMAL(6, 2),
costo_estimado DECIMAL(15, 2),
-- Resolución
estado VARCHAR(20) DEFAULT 'ABIERTA',
fecha_resolucion TIMESTAMPTZ,
resolucion TEXT,
responsable_id UUID,
-- Evidencias
evidencias JSONB,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_incidencia_codigo UNIQUE (tenant_id, codigo)
);
CREATE INDEX idx_incidencia_tenant ON transport.incidencias(tenant_id);
CREATE INDEX idx_incidencia_viaje ON transport.incidencias(viaje_id) WHERE viaje_id IS NOT NULL;
CREATE INDEX idx_incidencia_estado ON transport.incidencias(tenant_id, estado);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE transport.ordenes_transporte ENABLE ROW LEVEL SECURITY;
ALTER TABLE transport.embarques ENABLE ROW LEVEL SECURITY;
ALTER TABLE transport.viajes ENABLE ROW LEVEL SECURITY;
ALTER TABLE transport.paradas_viaje ENABLE ROW LEVEL SECURITY;
ALTER TABLE transport.pod ENABLE ROW LEVEL SECURITY;
ALTER TABLE transport.incidencias ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_ot ON transport.ordenes_transporte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_embarques ON transport.embarques
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_viajes ON transport.viajes
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_paradas ON transport.paradas_viaje
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_pod ON transport.pod
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_incidencias ON transport.incidencias
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE transport.ordenes_transporte IS 'Ordenes de transporte (OT) - solicitudes de servicio';
COMMENT ON TABLE transport.embarques IS 'Agrupación de OTs para consolidación';
COMMENT ON TABLE transport.viajes IS 'Ejecución operativa de transporte';
COMMENT ON TABLE transport.paradas_viaje IS 'Paradas programadas en un viaje (multi-drop)';
COMMENT ON TABLE transport.pod IS 'Proof of Delivery - evidencia de entrega';
COMMENT ON TABLE transport.incidencias IS 'Registro de incidencias durante el transporte';
-- =============================================================================
-- FIN DDL TRANSPORT
-- =============================================================================

409
ddl/02-fleet-schema-ddl.sql Normal file
View File

@ -0,0 +1,409 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Fleet DDL
-- =============================================================================
-- Archivo: 02-fleet-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Unidades, remolques, operadores, documentos, licencias
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS ADICIONALES
-- =============================================================================
CREATE TYPE fleet.tipo_licencia AS ENUM (
'A', -- Motociclista
'B', -- Automovilista particular
'C', -- Chofer particular
'D', -- Chofer público pasajeros
'E', -- Chofer público carga
'F' -- Federal (SCT)
);
CREATE TYPE fleet.estado_operador AS ENUM (
'ACTIVO',
'EN_VIAJE',
'DESCANSO',
'VACACIONES',
'INCAPACIDAD',
'SUSPENDIDO',
'BAJA'
);
CREATE TYPE fleet.tipo_documento AS ENUM (
'LICENCIA',
'INE',
'CURP',
'RFC',
'NSS',
'TARJETA_CIRCULACION',
'POLIZA_SEGURO',
'VERIFICACION',
'PERMISO_SCT',
'CERTIFICADO_FISICO',
'ANTIDOPING',
'OTRO'
);
-- =============================================================================
-- TABLA: unidades (Tractoras y vehículos)
-- =============================================================================
CREATE TABLE fleet.unidades (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
numero_economico VARCHAR(20) NOT NULL,
tipo fleet.tipo_unidad NOT NULL,
-- Vehículo
marca VARCHAR(50),
modelo VARCHAR(50),
anio INT,
color VARCHAR(30),
numero_serie VARCHAR(50),
numero_motor VARCHAR(50),
-- Placas
placa VARCHAR(15),
placa_estado VARCHAR(50),
-- SCT
permiso_sct VARCHAR(50),
tipo_permiso_sct VARCHAR(10),
configuracion_vehicular VARCHAR(10), -- C2, C3, T3S2, etc.
-- Capacidades
capacidad_peso_kg DECIMAL(10, 2),
capacidad_volumen_m3 DECIMAL(10, 4),
capacidad_pallets INT,
-- Combustible
tipo_combustible VARCHAR(20), -- DIESEL, GASOLINA, GAS
rendimiento_km_litro DECIMAL(6, 2),
capacidad_tanque_litros DECIMAL(8, 2),
-- Odómetro
odometro_actual INT DEFAULT 0,
odometro_ultimo_servicio INT,
-- GPS
tiene_gps BOOLEAN DEFAULT FALSE,
gps_proveedor VARCHAR(50),
gps_imei VARCHAR(50),
-- Estado
estado fleet.estado_unidad DEFAULT 'DISPONIBLE',
ubicacion_actual_lat DECIMAL(10, 7),
ubicacion_actual_lng DECIMAL(10, 7),
ultima_actualizacion_ubicacion TIMESTAMPTZ,
-- Propiedad
es_propia BOOLEAN DEFAULT TRUE,
propietario_id UUID, -- Si no es propia, referencia al carrier
-- Costos
costo_adquisicion DECIMAL(15, 2),
fecha_adquisicion DATE,
valor_actual DECIMAL(15, 2),
-- Fechas importantes
fecha_verificacion_proxima DATE,
fecha_poliza_vencimiento DATE,
fecha_permiso_vencimiento DATE,
-- Activo
activo BOOLEAN DEFAULT TRUE,
fecha_baja DATE,
motivo_baja TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by_id UUID,
CONSTRAINT uq_unidad_numero UNIQUE (tenant_id, numero_economico),
CONSTRAINT uq_unidad_placa UNIQUE (tenant_id, placa)
);
CREATE INDEX idx_unidad_tenant ON fleet.unidades(tenant_id);
CREATE INDEX idx_unidad_tipo ON fleet.unidades(tenant_id, tipo);
CREATE INDEX idx_unidad_estado ON fleet.unidades(tenant_id, estado);
CREATE INDEX idx_unidad_activo ON fleet.unidades(tenant_id, activo) WHERE activo = TRUE;
-- =============================================================================
-- TABLA: remolques
-- =============================================================================
CREATE TABLE fleet.remolques (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
numero_economico VARCHAR(20) NOT NULL,
tipo fleet.tipo_unidad NOT NULL, -- CAJA_SECA, PLATAFORMA, etc.
-- Vehículo
marca VARCHAR(50),
modelo VARCHAR(50),
anio INT,
numero_serie VARCHAR(50),
-- Placas
placa VARCHAR(15),
placa_estado VARCHAR(50),
-- Dimensiones
largo_metros DECIMAL(6, 2),
ancho_metros DECIMAL(6, 2),
alto_metros DECIMAL(6, 2),
-- Capacidades
capacidad_peso_kg DECIMAL(10, 2),
capacidad_volumen_m3 DECIMAL(10, 4),
capacidad_pallets INT,
-- Refrigeración (si aplica)
es_refrigerado BOOLEAN DEFAULT FALSE,
marca_refrigeracion VARCHAR(50),
modelo_refrigeracion VARCHAR(50),
temperatura_min DECIMAL(5, 2),
temperatura_max DECIMAL(5, 2),
-- Estado
estado fleet.estado_unidad DEFAULT 'DISPONIBLE',
-- Propiedad
es_propia BOOLEAN DEFAULT TRUE,
propietario_id UUID,
-- Fechas importantes
fecha_verificacion_proxima DATE,
fecha_poliza_vencimiento DATE,
-- Activo
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT uq_remolque_numero UNIQUE (tenant_id, numero_economico)
);
CREATE INDEX idx_remolque_tenant ON fleet.remolques(tenant_id);
CREATE INDEX idx_remolque_tipo ON fleet.remolques(tenant_id, tipo);
CREATE INDEX idx_remolque_estado ON fleet.remolques(tenant_id, estado);
-- =============================================================================
-- TABLA: operadores (Conductores)
-- =============================================================================
CREATE TABLE fleet.operadores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
numero_empleado VARCHAR(20) NOT NULL,
nombre VARCHAR(100) NOT NULL,
apellido_paterno VARCHAR(100) NOT NULL,
apellido_materno VARCHAR(100),
nombre_completo VARCHAR(300) GENERATED ALWAYS AS (
nombre || ' ' || apellido_paterno || COALESCE(' ' || apellido_materno, '')
) STORED,
-- Documentos de identidad
curp VARCHAR(18),
rfc VARCHAR(13),
nss VARCHAR(15), -- Número Seguro Social
-- Contacto
telefono VARCHAR(30),
telefono_emergencia VARCHAR(30),
email VARCHAR(255),
-- Dirección
direccion TEXT,
codigo_postal VARCHAR(10),
ciudad VARCHAR(100),
estado VARCHAR(100),
-- Datos de nacimiento
fecha_nacimiento DATE,
lugar_nacimiento VARCHAR(100),
nacionalidad VARCHAR(50) DEFAULT 'Mexicana',
-- Licencia de conducir
tipo_licencia fleet.tipo_licencia,
numero_licencia VARCHAR(30),
licencia_vigencia DATE,
licencia_estado_expedicion VARCHAR(50),
-- Certificaciones
certificado_fisico_vigencia DATE,
antidoping_vigencia DATE,
capacitacion_materiales_peligrosos BOOLEAN DEFAULT FALSE,
capacitacion_mp_vigencia DATE,
-- Estado
estado fleet.estado_operador DEFAULT 'ACTIVO',
-- Unidad asignada (default)
unidad_asignada_id UUID REFERENCES fleet.unidades(id),
-- Métricas de desempeño
calificacion DECIMAL(3, 2) DEFAULT 5.00,
total_viajes INT DEFAULT 0,
total_km INT DEFAULT 0,
incidentes INT DEFAULT 0,
-- Datos bancarios (para pagos)
banco VARCHAR(100),
cuenta_bancaria VARCHAR(30),
clabe VARCHAR(18),
-- Salario
salario_base DECIMAL(12, 2),
tipo_pago VARCHAR(20), -- 'FIJO', 'POR_VIAJE', 'MIXTO'
-- Fechas
fecha_ingreso DATE,
fecha_baja DATE,
motivo_baja TEXT,
-- Activo
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by_id UUID,
CONSTRAINT uq_operador_numero UNIQUE (tenant_id, numero_empleado),
CONSTRAINT uq_operador_curp UNIQUE (tenant_id, curp)
);
CREATE INDEX idx_operador_tenant ON fleet.operadores(tenant_id);
CREATE INDEX idx_operador_estado ON fleet.operadores(tenant_id, estado);
CREATE INDEX idx_operador_activo ON fleet.operadores(tenant_id, activo) WHERE activo = TRUE;
CREATE INDEX idx_operador_licencia ON fleet.operadores(licencia_vigencia);
-- =============================================================================
-- TABLA: documentos_flota
-- =============================================================================
CREATE TABLE fleet.documentos_flota (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Referencia polimórfica
entidad_tipo VARCHAR(20) NOT NULL, -- 'UNIDAD', 'REMOLQUE', 'OPERADOR'
entidad_id UUID NOT NULL,
-- Documento
tipo_documento fleet.tipo_documento NOT NULL,
nombre VARCHAR(200) NOT NULL,
numero_documento VARCHAR(100),
descripcion TEXT,
-- Vigencia
fecha_emision DATE,
fecha_vencimiento DATE,
dias_alerta_vencimiento INT DEFAULT 30,
-- Archivo
archivo_url TEXT,
archivo_nombre VARCHAR(255),
archivo_tipo VARCHAR(50),
archivo_tamano_bytes BIGINT,
-- Estado
verificado BOOLEAN DEFAULT FALSE,
verificado_por UUID,
verificado_fecha TIMESTAMPTZ,
-- Activo
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_documento_entidad ON fleet.documentos_flota(entidad_tipo, entidad_id);
CREATE INDEX idx_documento_vencimiento ON fleet.documentos_flota(fecha_vencimiento) WHERE activo = TRUE;
CREATE INDEX idx_documento_tipo ON fleet.documentos_flota(tenant_id, tipo_documento);
-- =============================================================================
-- TABLA: asignaciones_unidad_operador
-- =============================================================================
CREATE TABLE fleet.asignaciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
unidad_id UUID NOT NULL REFERENCES fleet.unidades(id),
operador_id UUID NOT NULL REFERENCES fleet.operadores(id),
remolque_id UUID REFERENCES fleet.remolques(id),
-- Vigencia de asignación
fecha_inicio TIMESTAMPTZ NOT NULL,
fecha_fin TIMESTAMPTZ,
-- Activa
activa BOOLEAN DEFAULT TRUE,
-- Motivo
motivo VARCHAR(200),
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_asignacion_unidad ON fleet.asignaciones(unidad_id, activa);
CREATE INDEX idx_asignacion_operador ON fleet.asignaciones(operador_id, activa);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE fleet.unidades ENABLE ROW LEVEL SECURITY;
ALTER TABLE fleet.remolques ENABLE ROW LEVEL SECURITY;
ALTER TABLE fleet.operadores ENABLE ROW LEVEL SECURITY;
ALTER TABLE fleet.documentos_flota ENABLE ROW LEVEL SECURITY;
ALTER TABLE fleet.asignaciones ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_unidades ON fleet.unidades
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_remolques ON fleet.remolques
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_operadores ON fleet.operadores
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_documentos ON fleet.documentos_flota
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_asignaciones ON fleet.asignaciones
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE fleet.unidades IS 'Unidades motrices (tractoras, camiones, camionetas)';
COMMENT ON TABLE fleet.remolques IS 'Remolques, cajas, plataformas, tanques';
COMMENT ON TABLE fleet.operadores IS 'Operadores/conductores de la flota';
COMMENT ON TABLE fleet.documentos_flota IS 'Documentos de unidades, remolques y operadores';
COMMENT ON TABLE fleet.asignaciones IS 'Historial de asignaciones unidad-operador';
-- =============================================================================
-- FIN DDL FLEET
-- =============================================================================

View File

@ -0,0 +1,369 @@
-- =============================================================================
-- 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
-- =============================================================================

315
ddl/04-fuel-schema-ddl.sql Normal file
View File

@ -0,0 +1,315 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Fuel DDL
-- =============================================================================
-- Archivo: 04-fuel-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Combustible, peajes, gastos de viaje, viaticos
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS
-- =============================================================================
CREATE TYPE fuel.tipo_carga_combustible AS ENUM (
'VALE',
'TARJETA',
'EFECTIVO',
'FACTURA_DIRECTA'
);
CREATE TYPE fuel.tipo_gasto AS ENUM (
'COMBUSTIBLE',
'PEAJE',
'VIATICO',
'HOSPEDAJE',
'ALIMENTOS',
'ESTACIONAMIENTO',
'MULTA',
'MANIOBRA',
'REPARACION_MENOR',
'OTRO'
);
CREATE TYPE fuel.estado_gasto AS ENUM (
'PENDIENTE',
'APROBADO',
'RECHAZADO',
'PAGADO'
);
-- =============================================================================
-- TABLA: cargas_combustible
-- =============================================================================
CREATE TABLE fuel.cargas_combustible (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Unidad y viaje
unidad_id UUID NOT NULL,
viaje_id UUID,
operador_id UUID NOT NULL,
-- Carga
tipo_carga fuel.tipo_carga_combustible NOT NULL,
tipo_combustible VARCHAR(20) NOT NULL, -- DIESEL, GASOLINA, GAS
litros DECIMAL(10, 3) NOT NULL,
precio_litro DECIMAL(10, 4) NOT NULL,
total DECIMAL(12, 2) NOT NULL,
-- Odómetro
odometro_carga INT,
rendimiento_calculado DECIMAL(6, 2), -- km/litro desde última carga
-- Ubicación
estacion_id UUID,
estacion_nombre VARCHAR(200),
estacion_direccion TEXT,
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
-- Vale/Factura
numero_vale VARCHAR(50),
numero_factura VARCHAR(50),
folio_ticket VARCHAR(50),
-- Fecha
fecha_carga TIMESTAMPTZ NOT NULL,
-- Aprobación
estado fuel.estado_gasto DEFAULT 'PENDIENTE',
aprobado_por UUID,
aprobado_fecha TIMESTAMPTZ,
-- Evidencia
foto_ticket_url TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_carga_unidad ON fuel.cargas_combustible(unidad_id);
CREATE INDEX idx_carga_viaje ON fuel.cargas_combustible(viaje_id);
CREATE INDEX idx_carga_fecha ON fuel.cargas_combustible(tenant_id, fecha_carga);
-- =============================================================================
-- TABLA: cruces_peaje
-- =============================================================================
CREATE TABLE fuel.cruces_peaje (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Unidad y viaje
unidad_id UUID NOT NULL,
viaje_id UUID,
operador_id UUID,
-- Peaje
caseta_nombre VARCHAR(200) NOT NULL,
caseta_codigo VARCHAR(50),
carretera VARCHAR(200),
-- Monto
monto DECIMAL(10, 2) NOT NULL,
tipo_pago VARCHAR(20), -- EFECTIVO, TAG, PREPAGO
-- TAG (si aplica)
tag_numero VARCHAR(50),
-- Ubicación
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
-- Fecha
fecha_cruce TIMESTAMPTZ NOT NULL,
-- Comprobante
numero_ticket VARCHAR(50),
foto_ticket_url TEXT,
-- Estado
estado fuel.estado_gasto DEFAULT 'APROBADO',
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_peaje_unidad ON fuel.cruces_peaje(unidad_id);
CREATE INDEX idx_peaje_viaje ON fuel.cruces_peaje(viaje_id);
CREATE INDEX idx_peaje_fecha ON fuel.cruces_peaje(tenant_id, fecha_cruce);
-- =============================================================================
-- TABLA: gastos_viaje
-- =============================================================================
CREATE TABLE fuel.gastos_viaje (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Viaje y operador
viaje_id UUID NOT NULL,
operador_id UUID NOT NULL,
-- Gasto
tipo_gasto fuel.tipo_gasto NOT NULL,
descripcion VARCHAR(500) NOT NULL,
monto DECIMAL(12, 2) NOT NULL,
-- Comprobante
tiene_factura BOOLEAN DEFAULT FALSE,
numero_factura VARCHAR(50),
numero_ticket VARCHAR(50),
foto_comprobante_url TEXT,
-- Ubicación
lugar VARCHAR(200),
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
-- Fecha
fecha_gasto TIMESTAMPTZ NOT NULL,
-- Estado
estado fuel.estado_gasto DEFAULT 'PENDIENTE',
aprobado_por UUID,
aprobado_fecha TIMESTAMPTZ,
motivo_rechazo TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_gasto_viaje ON fuel.gastos_viaje(viaje_id);
CREATE INDEX idx_gasto_operador ON fuel.gastos_viaje(operador_id);
CREATE INDEX idx_gasto_estado ON fuel.gastos_viaje(tenant_id, estado);
-- =============================================================================
-- TABLA: anticipos_viaticos
-- =============================================================================
CREATE TABLE fuel.anticipos_viaticos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Viaje y operador
viaje_id UUID NOT NULL,
operador_id UUID NOT NULL,
-- Anticipo
monto_solicitado DECIMAL(12, 2) NOT NULL,
monto_aprobado DECIMAL(12, 2),
monto_comprobado DECIMAL(12, 2) DEFAULT 0,
monto_reintegro DECIMAL(12, 2) DEFAULT 0,
-- Conceptos desglosados
combustible_estimado DECIMAL(12, 2),
peajes_estimado DECIMAL(12, 2),
viaticos_estimado DECIMAL(12, 2),
-- Estado
estado VARCHAR(20) DEFAULT 'SOLICITADO',
-- SOLICITADO, APROBADO, ENTREGADO, COMPROBANDO, LIQUIDADO
-- Fechas
fecha_solicitud TIMESTAMPTZ DEFAULT NOW(),
fecha_aprobacion TIMESTAMPTZ,
fecha_entrega TIMESTAMPTZ,
fecha_liquidacion TIMESTAMPTZ,
-- Aprobaciones
aprobado_por UUID,
entregado_por UUID,
liquidado_por UUID,
-- Observaciones
observaciones TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_anticipo_viaje ON fuel.anticipos_viaticos(viaje_id);
CREATE INDEX idx_anticipo_operador ON fuel.anticipos_viaticos(operador_id);
CREATE INDEX idx_anticipo_estado ON fuel.anticipos_viaticos(tenant_id, estado);
-- =============================================================================
-- TABLA: control_rendimiento
-- =============================================================================
CREATE TABLE fuel.control_rendimiento (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Unidad
unidad_id UUID NOT NULL,
-- Período
fecha_inicio DATE NOT NULL,
fecha_fin DATE NOT NULL,
-- Métricas
km_recorridos INT NOT NULL,
litros_consumidos DECIMAL(12, 3) NOT NULL,
rendimiento_real DECIMAL(6, 2) NOT NULL,
rendimiento_esperado DECIMAL(6, 2),
variacion_porcentaje DECIMAL(5, 2),
-- Costos
costo_total_combustible DECIMAL(15, 2),
costo_por_km DECIMAL(8, 4),
-- Alertas
tiene_anomalia BOOLEAN DEFAULT FALSE,
tipo_anomalia VARCHAR(50),
descripcion_anomalia TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_rendimiento_unidad ON fuel.control_rendimiento(unidad_id);
CREATE INDEX idx_rendimiento_fecha ON fuel.control_rendimiento(tenant_id, fecha_inicio);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE fuel.cargas_combustible ENABLE ROW LEVEL SECURITY;
ALTER TABLE fuel.cruces_peaje ENABLE ROW LEVEL SECURITY;
ALTER TABLE fuel.gastos_viaje ENABLE ROW LEVEL SECURITY;
ALTER TABLE fuel.anticipos_viaticos ENABLE ROW LEVEL SECURITY;
ALTER TABLE fuel.control_rendimiento ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_cargas ON fuel.cargas_combustible
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_peajes ON fuel.cruces_peaje
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_gastos ON fuel.gastos_viaje
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_anticipos ON fuel.anticipos_viaticos
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_rendimiento ON fuel.control_rendimiento
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE fuel.cargas_combustible IS 'Registro de cargas de combustible';
COMMENT ON TABLE fuel.cruces_peaje IS 'Cruces de casetas de peaje';
COMMENT ON TABLE fuel.gastos_viaje IS 'Gastos diversos durante el viaje';
COMMENT ON TABLE fuel.anticipos_viaticos IS 'Anticipos entregados a operadores';
COMMENT ON TABLE fuel.control_rendimiento IS 'Control de rendimiento de combustible por unidad';
-- =============================================================================
-- FIN DDL FUEL
-- =============================================================================

View File

@ -0,0 +1,306 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Maintenance DDL
-- =============================================================================
-- Archivo: 05-maintenance-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Mantenimiento preventivo, correctivo, ordenes de trabajo
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS
-- =============================================================================
CREATE TYPE maintenance.tipo_mantenimiento AS ENUM (
'PREVENTIVO',
'CORRECTIVO',
'PREDICTIVO',
'EMERGENCIA'
);
CREATE TYPE maintenance.prioridad AS ENUM (
'BAJA',
'MEDIA',
'ALTA',
'URGENTE'
);
CREATE TYPE maintenance.estado_orden AS ENUM (
'BORRADOR',
'PROGRAMADA',
'EN_PROCESO',
'ESPERANDO_REFACCIONES',
'COMPLETADA',
'CANCELADA'
);
CREATE TYPE maintenance.tipo_servicio AS ENUM (
'CAMBIO_ACEITE',
'FRENOS',
'LLANTAS',
'SUSPENSION',
'MOTOR',
'TRANSMISION',
'ELECTRICO',
'CARROCERIA',
'REFRIGERACION',
'ALINEACION_BALANCEO',
'REVISION_GENERAL',
'OTRO'
);
-- =============================================================================
-- TABLA: planes_mantenimiento
-- =============================================================================
CREATE TABLE maintenance.planes_mantenimiento (
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,
descripcion TEXT,
-- Tipo de unidad que aplica
aplica_tipo_unidad fleet.tipo_unidad[],
aplica_todas_unidades BOOLEAN DEFAULT FALSE,
-- Frecuencia
frecuencia_km INT,
frecuencia_dias INT,
frecuencia_horas_motor INT,
-- Servicios incluidos
servicios maintenance.tipo_servicio[] NOT NULL,
-- Costos estimados
costo_estimado_mano_obra DECIMAL(12, 2),
costo_estimado_refacciones DECIMAL(12, 2),
tiempo_estimado_horas DECIMAL(6, 2),
-- Estado
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_plan_tenant ON maintenance.planes_mantenimiento(tenant_id);
CREATE UNIQUE INDEX idx_plan_codigo ON maintenance.planes_mantenimiento(tenant_id, codigo);
-- =============================================================================
-- TABLA: programacion_mantenimiento
-- =============================================================================
CREATE TABLE maintenance.programacion_mantenimiento (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Unidad y plan
unidad_id UUID NOT NULL,
plan_id UUID REFERENCES maintenance.planes_mantenimiento(id),
-- Tipo
tipo maintenance.tipo_mantenimiento NOT NULL,
-- Próximo mantenimiento
proximo_km INT,
proxima_fecha DATE,
proximas_horas_motor INT,
-- Último mantenimiento
ultimo_km INT,
ultima_fecha DATE,
ultima_orden_id UUID,
-- Estado
vencido BOOLEAN DEFAULT FALSE,
dias_para_vencer INT,
-- Auditoría
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_programacion_unidad ON maintenance.programacion_mantenimiento(unidad_id);
CREATE INDEX idx_programacion_vencido ON maintenance.programacion_mantenimiento(tenant_id, vencido);
CREATE INDEX idx_programacion_proxima ON maintenance.programacion_mantenimiento(proxima_fecha);
-- =============================================================================
-- TABLA: ordenes_trabajo
-- =============================================================================
CREATE TABLE maintenance.ordenes_trabajo (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
numero_orden VARCHAR(50) NOT NULL,
-- Unidad
unidad_id UUID NOT NULL,
remolque_id UUID,
-- Tipo y prioridad
tipo maintenance.tipo_mantenimiento NOT NULL,
prioridad maintenance.prioridad DEFAULT 'MEDIA',
-- Diagnóstico inicial
descripcion_falla TEXT,
reportado_por VARCHAR(200),
reportado_fecha TIMESTAMPTZ,
-- Programación
fecha_programada DATE,
hora_programada TIME,
taller_id UUID,
taller_externo_nombre VARCHAR(200),
es_taller_externo BOOLEAN DEFAULT FALSE,
-- Ejecución
fecha_inicio TIMESTAMPTZ,
fecha_fin TIMESTAMPTZ,
mecanico_responsable VARCHAR(200),
-- Odómetro
odometro_entrada INT,
odometro_salida INT,
-- Diagnóstico final
diagnostico_final TEXT,
trabajos_realizados TEXT,
-- Costos
costo_mano_obra DECIMAL(12, 2) DEFAULT 0,
costo_refacciones DECIMAL(12, 2) DEFAULT 0,
costo_otros DECIMAL(12, 2) DEFAULT 0,
costo_total DECIMAL(12, 2) DEFAULT 0,
-- Plan relacionado
plan_id UUID REFERENCES maintenance.planes_mantenimiento(id),
-- Estado
estado maintenance.estado_orden DEFAULT 'BORRADOR',
-- Garantía
tiene_garantia BOOLEAN DEFAULT FALSE,
garantia_dias INT,
garantia_km INT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by_id UUID
);
CREATE INDEX idx_ot_tenant ON maintenance.ordenes_trabajo(tenant_id);
CREATE INDEX idx_ot_unidad ON maintenance.ordenes_trabajo(unidad_id);
CREATE INDEX idx_ot_estado ON maintenance.ordenes_trabajo(tenant_id, estado);
CREATE INDEX idx_ot_fecha ON maintenance.ordenes_trabajo(fecha_programada);
CREATE UNIQUE INDEX idx_ot_numero ON maintenance.ordenes_trabajo(tenant_id, numero_orden);
-- =============================================================================
-- TABLA: lineas_orden_trabajo (Refacciones usadas)
-- =============================================================================
CREATE TABLE maintenance.lineas_orden_trabajo (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
orden_id UUID NOT NULL REFERENCES maintenance.ordenes_trabajo(id),
-- Tipo de línea
tipo VARCHAR(20) NOT NULL, -- 'REFACCION', 'MANO_OBRA', 'OTRO'
-- Refacción (si aplica)
producto_id UUID,
numero_parte VARCHAR(100),
descripcion VARCHAR(500) NOT NULL,
-- Cantidades
cantidad DECIMAL(10, 3) NOT NULL,
unidad_medida VARCHAR(20),
-- Precios
precio_unitario DECIMAL(12, 2) NOT NULL,
descuento DECIMAL(12, 2) DEFAULT 0,
total DECIMAL(12, 2) NOT NULL,
-- Proveedor
proveedor_id UUID,
proveedor_nombre VARCHAR(200),
-- Factura
factura_proveedor VARCHAR(50)
);
CREATE INDEX idx_linea_ot ON maintenance.lineas_orden_trabajo(orden_id);
-- =============================================================================
-- TABLA: checklist_mantenimiento
-- =============================================================================
CREATE TABLE maintenance.checklist_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
orden_id UUID NOT NULL REFERENCES maintenance.ordenes_trabajo(id),
-- Item
categoria VARCHAR(100),
descripcion VARCHAR(500) NOT NULL,
obligatorio BOOLEAN DEFAULT FALSE,
-- Resultado
resultado VARCHAR(20), -- 'OK', 'REPARADO', 'PENDIENTE', 'NO_APLICA'
observaciones TEXT,
-- Evidencia
foto_url TEXT,
-- Revisado
revisado_por VARCHAR(200),
revisado_fecha TIMESTAMPTZ
);
CREATE INDEX idx_checklist_orden ON maintenance.checklist_items(orden_id);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE maintenance.planes_mantenimiento ENABLE ROW LEVEL SECURITY;
ALTER TABLE maintenance.programacion_mantenimiento ENABLE ROW LEVEL SECURITY;
ALTER TABLE maintenance.ordenes_trabajo ENABLE ROW LEVEL SECURITY;
ALTER TABLE maintenance.lineas_orden_trabajo ENABLE ROW LEVEL SECURITY;
ALTER TABLE maintenance.checklist_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_planes ON maintenance.planes_mantenimiento
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_programacion ON maintenance.programacion_mantenimiento
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_ordenes ON maintenance.ordenes_trabajo
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_lineas ON maintenance.lineas_orden_trabajo
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_checklist ON maintenance.checklist_items
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE maintenance.planes_mantenimiento IS 'Planes de mantenimiento preventivo';
COMMENT ON TABLE maintenance.programacion_mantenimiento IS 'Programación de próximos mantenimientos por unidad';
COMMENT ON TABLE maintenance.ordenes_trabajo IS 'Órdenes de trabajo de mantenimiento';
COMMENT ON TABLE maintenance.lineas_orden_trabajo IS 'Líneas de detalle de órdenes de trabajo';
COMMENT ON TABLE maintenance.checklist_items IS 'Items de checklist de mantenimiento';
-- =============================================================================
-- FIN DDL MAINTENANCE
-- =============================================================================

View File

@ -0,0 +1,344 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Carriers DDL
-- =============================================================================
-- Archivo: 06-carriers-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Transportistas subcontratados, documentos, scorecard
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS
-- =============================================================================
CREATE TYPE carriers.estado_carrier AS ENUM (
'PROSPECTO',
'EN_VALIDACION',
'ACTIVO',
'SUSPENDIDO',
'BAJA'
);
CREATE TYPE carriers.tipo_contrato AS ENUM (
'SPOT',
'DEDICADO',
'PREFERENTE'
);
-- =============================================================================
-- TABLA: carriers (Transportistas terceros)
-- =============================================================================
CREATE TABLE carriers.carriers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
codigo VARCHAR(20) NOT NULL,
razon_social VARCHAR(200) NOT NULL,
nombre_comercial VARCHAR(200),
-- Fiscal
rfc VARCHAR(13) NOT NULL,
regimen_fiscal VARCHAR(100),
-- SCT
permiso_sct VARCHAR(50),
tipo_permiso_sct VARCHAR(10),
permiso_vigencia DATE,
-- Contacto
contacto_nombre VARCHAR(200),
contacto_telefono VARCHAR(30),
contacto_email VARCHAR(255),
-- Dirección
direccion TEXT,
codigo_postal VARCHAR(10),
ciudad VARCHAR(100),
estado VARCHAR(100),
-- Operación
cobertura_estados TEXT[], -- Estados donde opera
tipos_equipo fleet.tipo_unidad[], -- Tipos de unidad disponibles
capacidad_unidades INT,
-- Tipo de relación
tipo_contrato carriers.tipo_contrato DEFAULT 'SPOT',
-- Términos comerciales
dias_pago INT DEFAULT 30,
porcentaje_retencion DECIMAL(5, 2) DEFAULT 4.00,
-- Seguros
poliza_seguro_carga VARCHAR(100),
poliza_vigencia DATE,
suma_asegurada DECIMAL(15, 2),
-- Evaluación
calificacion DECIMAL(3, 2) DEFAULT 0,
total_viajes INT DEFAULT 0,
viajes_a_tiempo INT DEFAULT 0,
incidentes INT DEFAULT 0,
-- Estado
estado carriers.estado_carrier DEFAULT 'PROSPECTO',
fecha_alta DATE,
fecha_baja DATE,
motivo_baja TEXT,
-- Datos bancarios
banco VARCHAR(100),
cuenta_bancaria VARCHAR(30),
clabe VARCHAR(18),
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by_id UUID,
CONSTRAINT uq_carrier_codigo UNIQUE (tenant_id, codigo),
CONSTRAINT uq_carrier_rfc UNIQUE (tenant_id, rfc)
);
CREATE INDEX idx_carrier_tenant ON carriers.carriers(tenant_id);
CREATE INDEX idx_carrier_estado ON carriers.carriers(tenant_id, estado);
-- =============================================================================
-- TABLA: documentos_carrier
-- =============================================================================
CREATE TABLE carriers.documentos_carrier (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carrier_id UUID NOT NULL REFERENCES carriers.carriers(id),
-- Documento
tipo_documento VARCHAR(50) NOT NULL,
nombre VARCHAR(200) NOT NULL,
descripcion TEXT,
-- Vigencia
fecha_emision DATE,
fecha_vencimiento DATE,
-- Archivo
archivo_url TEXT,
archivo_nombre VARCHAR(255),
-- Verificación
verificado BOOLEAN DEFAULT FALSE,
verificado_por UUID,
verificado_fecha TIMESTAMPTZ,
-- Estado
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_doc_carrier ON carriers.documentos_carrier(carrier_id);
CREATE INDEX idx_doc_vencimiento ON carriers.documentos_carrier(fecha_vencimiento);
-- =============================================================================
-- TABLA: unidades_carrier
-- =============================================================================
CREATE TABLE carriers.unidades_carrier (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carrier_id UUID NOT NULL REFERENCES carriers.carriers(id),
-- Unidad
numero_economico VARCHAR(20) NOT NULL,
tipo fleet.tipo_unidad NOT NULL,
marca VARCHAR(50),
modelo VARCHAR(50),
anio INT,
placa VARCHAR(15),
-- SCT
configuracion_vehicular VARCHAR(10),
-- Capacidad
capacidad_peso_kg DECIMAL(10, 2),
capacidad_volumen_m3 DECIMAL(10, 4),
-- GPS
tiene_gps BOOLEAN DEFAULT FALSE,
gps_proveedor VARCHAR(50),
-- Estado
activa BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_unidad_carrier ON carriers.unidades_carrier(carrier_id);
-- =============================================================================
-- TABLA: operadores_carrier
-- =============================================================================
CREATE TABLE carriers.operadores_carrier (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carrier_id UUID NOT NULL REFERENCES carriers.carriers(id),
-- Operador
nombre_completo VARCHAR(300) NOT NULL,
telefono VARCHAR(30),
-- Licencia
tipo_licencia fleet.tipo_licencia,
numero_licencia VARCHAR(30),
licencia_vigencia DATE,
-- Estado
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_operador_carrier ON carriers.operadores_carrier(carrier_id);
-- =============================================================================
-- TABLA: asignaciones_carrier (Viajes asignados a carriers)
-- =============================================================================
CREATE TABLE carriers.asignaciones_carrier (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Carrier
carrier_id UUID NOT NULL REFERENCES carriers.carriers(id),
unidad_carrier_id UUID REFERENCES carriers.unidades_carrier(id),
operador_carrier_id UUID REFERENCES carriers.operadores_carrier(id),
-- Viaje/OT
viaje_id UUID,
ot_id UUID,
-- Tarifa acordada
tarifa_acordada DECIMAL(15, 2) NOT NULL,
moneda VARCHAR(3) DEFAULT 'MXN',
-- Fechas
fecha_asignacion TIMESTAMPTZ DEFAULT NOW(),
fecha_confirmacion TIMESTAMPTZ,
-- Estado
estado VARCHAR(20) DEFAULT 'PENDIENTE',
-- PENDIENTE, CONFIRMADA, EN_PROCESO, COMPLETADA, CANCELADA
-- Facturación
factura_carrier VARCHAR(50),
fecha_factura DATE,
monto_facturado DECIMAL(15, 2),
fecha_pago DATE,
-- Evaluación del viaje
calificacion INT, -- 1-5
comentarios TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_asignacion_carrier ON carriers.asignaciones_carrier(carrier_id);
CREATE INDEX idx_asignacion_viaje ON carriers.asignaciones_carrier(viaje_id);
CREATE INDEX idx_asignacion_estado ON carriers.asignaciones_carrier(tenant_id, estado);
-- =============================================================================
-- TABLA: scorecard_carrier
-- =============================================================================
CREATE TABLE carriers.scorecard (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carrier_id UUID NOT NULL REFERENCES carriers.carriers(id),
-- Período
periodo_inicio DATE NOT NULL,
periodo_fin DATE NOT NULL,
-- Métricas de servicio
total_viajes INT DEFAULT 0,
viajes_a_tiempo INT DEFAULT 0,
viajes_retrasados INT DEFAULT 0,
viajes_cancelados INT DEFAULT 0,
-- Porcentajes
otif_porcentaje DECIMAL(5, 2), -- On Time In Full
puntualidad_porcentaje DECIMAL(5, 2),
-- Incidentes
total_incidentes INT DEFAULT 0,
incidentes_graves INT DEFAULT 0,
-- Calificación
calificacion_servicio DECIMAL(3, 2),
calificacion_documentacion DECIMAL(3, 2),
calificacion_comunicacion DECIMAL(3, 2),
calificacion_general DECIMAL(3, 2),
-- Financiero
monto_total_servicios DECIMAL(15, 2),
monto_penalizaciones DECIMAL(15, 2),
-- Auditoría
calculado_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_scorecard_carrier ON carriers.scorecard(carrier_id);
CREATE INDEX idx_scorecard_periodo ON carriers.scorecard(periodo_inicio, periodo_fin);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE carriers.carriers ENABLE ROW LEVEL SECURITY;
ALTER TABLE carriers.documentos_carrier ENABLE ROW LEVEL SECURITY;
ALTER TABLE carriers.unidades_carrier ENABLE ROW LEVEL SECURITY;
ALTER TABLE carriers.operadores_carrier ENABLE ROW LEVEL SECURITY;
ALTER TABLE carriers.asignaciones_carrier ENABLE ROW LEVEL SECURITY;
ALTER TABLE carriers.scorecard ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_carriers ON carriers.carriers
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_docs ON carriers.documentos_carrier
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_unidades ON carriers.unidades_carrier
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_operadores ON carriers.operadores_carrier
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_asignaciones ON carriers.asignaciones_carrier
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_scorecard ON carriers.scorecard
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE carriers.carriers IS 'Transportistas subcontratados';
COMMENT ON TABLE carriers.documentos_carrier IS 'Documentos de carriers';
COMMENT ON TABLE carriers.unidades_carrier IS 'Unidades de carriers';
COMMENT ON TABLE carriers.operadores_carrier IS 'Operadores de carriers';
COMMENT ON TABLE carriers.asignaciones_carrier IS 'Asignaciones de viajes a carriers';
COMMENT ON TABLE carriers.scorecard IS 'Evaluación periódica de carriers';
-- =============================================================================
-- FIN DDL CARRIERS
-- =============================================================================

View File

@ -0,0 +1,352 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Billing Transport DDL
-- =============================================================================
-- Archivo: 07-billing-transport-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Tarifas, facturacion, recargos especificos de transporte
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS
-- =============================================================================
CREATE TYPE billing.tipo_tarifa AS ENUM (
'POR_VIAJE',
'POR_KM',
'POR_TONELADA',
'POR_M3',
'POR_PALLET',
'POR_HORA',
'MIXTA'
);
CREATE TYPE billing.tipo_recargo AS ENUM (
'FUEL_SURCHARGE',
'DETENTION',
'MANIOBRAS',
'CUSTODIA',
'ESCOLTA',
'PERNOCTA',
'ESTADIAS',
'FALSO_FLETE',
'SEGURO_ADICIONAL',
'OTRO'
);
CREATE TYPE billing.estado_factura AS ENUM (
'BORRADOR',
'EMITIDA',
'ENVIADA',
'PAGADA',
'PARCIAL',
'VENCIDA',
'CANCELADA'
);
-- =============================================================================
-- TABLA: lanes (Rutas origen-destino)
-- =============================================================================
CREATE TABLE billing.lanes (
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,
-- Origen
origen_ciudad VARCHAR(100) NOT NULL,
origen_estado VARCHAR(100) NOT NULL,
origen_codigo_postal VARCHAR(10),
-- Destino
destino_ciudad VARCHAR(100) NOT NULL,
destino_estado VARCHAR(100) NOT NULL,
destino_codigo_postal VARCHAR(10),
-- Distancia
distancia_km DECIMAL(10, 2),
tiempo_estimado_horas DECIMAL(6, 2),
-- Estado
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_lane_tenant ON billing.lanes(tenant_id);
CREATE UNIQUE INDEX idx_lane_codigo ON billing.lanes(tenant_id, codigo);
-- =============================================================================
-- TABLA: tarifas
-- =============================================================================
CREATE TABLE billing.tarifas (
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,
descripcion TEXT,
-- Cliente (opcional para tarifas generales)
cliente_id UUID,
-- Lane (opcional)
lane_id UUID REFERENCES billing.lanes(id),
-- Tipo de servicio
modalidad_servicio transport.modalidad_servicio,
tipo_equipo fleet.tipo_unidad,
-- Tipo de tarifa
tipo_tarifa billing.tipo_tarifa NOT NULL,
-- Precios
tarifa_base DECIMAL(15, 2) NOT NULL,
tarifa_km DECIMAL(10, 4),
tarifa_tonelada DECIMAL(10, 4),
tarifa_m3 DECIMAL(10, 4),
tarifa_pallet DECIMAL(10, 4),
tarifa_hora DECIMAL(10, 4),
-- Mínimos
minimo_facturar DECIMAL(15, 2),
peso_minimo_kg DECIMAL(10, 2),
-- Moneda
moneda VARCHAR(3) DEFAULT 'MXN',
-- Vigencia
fecha_inicio DATE NOT NULL,
fecha_fin DATE,
-- 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_tarifa_tenant ON billing.tarifas(tenant_id);
CREATE INDEX idx_tarifa_cliente ON billing.tarifas(cliente_id);
CREATE INDEX idx_tarifa_lane ON billing.tarifas(lane_id);
CREATE INDEX idx_tarifa_activa ON billing.tarifas(tenant_id, activa, fecha_inicio);
-- =============================================================================
-- TABLA: recargos_catalogo
-- =============================================================================
CREATE TABLE billing.recargos_catalogo (
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 billing.tipo_recargo NOT NULL,
descripcion TEXT,
-- Monto
es_porcentaje BOOLEAN DEFAULT FALSE,
monto DECIMAL(15, 4) NOT NULL, -- Si es_porcentaje=true, es porcentaje; sino monto fijo
moneda VARCHAR(3) DEFAULT 'MXN',
-- Aplicación
aplica_automatico BOOLEAN DEFAULT FALSE,
condicion_aplicacion TEXT, -- Descripción de cuándo aplica
-- Estado
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_recargo_tenant ON billing.recargos_catalogo(tenant_id);
CREATE UNIQUE INDEX idx_recargo_codigo ON billing.recargos_catalogo(tenant_id, codigo);
-- =============================================================================
-- TABLA: facturas_transporte
-- =============================================================================
CREATE TABLE billing.facturas_transporte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Identificación
serie VARCHAR(10),
folio VARCHAR(20) NOT NULL,
uuid_cfdi UUID, -- UUID del CFDI timbrado
-- Cliente
cliente_id UUID NOT NULL,
cliente_rfc VARCHAR(13) NOT NULL,
cliente_razon_social VARCHAR(200) NOT NULL,
cliente_uso_cfdi VARCHAR(10),
-- Fechas
fecha_emision TIMESTAMPTZ NOT NULL,
fecha_vencimiento DATE,
-- Totales
subtotal DECIMAL(15, 2) NOT NULL,
descuento DECIMAL(15, 2) DEFAULT 0,
iva DECIMAL(15, 2) DEFAULT 0,
retencion_iva DECIMAL(15, 2) DEFAULT 0,
retencion_isr DECIMAL(15, 2) DEFAULT 0,
total DECIMAL(15, 2) NOT NULL,
moneda VARCHAR(3) DEFAULT 'MXN',
tipo_cambio DECIMAL(10, 4) DEFAULT 1,
-- Pago
forma_pago VARCHAR(10),
metodo_pago VARCHAR(10),
condiciones_pago VARCHAR(200),
-- Relacionados
viaje_ids UUID[],
ot_ids UUID[],
-- CFDI
xml_cfdi TEXT,
pdf_url TEXT,
-- Estado
estado billing.estado_factura DEFAULT 'BORRADOR',
-- Pago
monto_pagado DECIMAL(15, 2) DEFAULT 0,
fecha_pago TIMESTAMPTZ,
-- Cancelación
fecha_cancelacion TIMESTAMPTZ,
motivo_cancelacion TEXT,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_factura_tenant ON billing.facturas_transporte(tenant_id);
CREATE INDEX idx_factura_cliente ON billing.facturas_transporte(cliente_id);
CREATE INDEX idx_factura_estado ON billing.facturas_transporte(tenant_id, estado);
CREATE INDEX idx_factura_fecha ON billing.facturas_transporte(fecha_emision);
CREATE UNIQUE INDEX idx_factura_folio ON billing.facturas_transporte(tenant_id, serie, folio);
-- =============================================================================
-- TABLA: lineas_factura
-- =============================================================================
CREATE TABLE billing.lineas_factura (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
factura_id UUID NOT NULL REFERENCES billing.facturas_transporte(id),
-- Secuencia
linea INT NOT NULL,
-- Concepto
descripcion TEXT NOT NULL,
clave_producto_sat VARCHAR(10), -- Catálogo SAT
unidad_sat VARCHAR(10),
-- Cantidades
cantidad DECIMAL(12, 4) NOT NULL,
precio_unitario DECIMAL(15, 4) NOT NULL,
descuento DECIMAL(15, 2) DEFAULT 0,
importe DECIMAL(15, 2) NOT NULL,
-- Impuestos
iva_tasa DECIMAL(5, 2) DEFAULT 16,
iva_monto DECIMAL(15, 2),
-- Referencia
viaje_id UUID,
ot_id UUID,
recargo_id UUID REFERENCES billing.recargos_catalogo(id)
);
CREATE INDEX idx_linea_factura ON billing.lineas_factura(factura_id);
-- =============================================================================
-- TABLA: fuel_surcharge (Índice de combustible)
-- =============================================================================
CREATE TABLE billing.fuel_surcharge (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Período
fecha_inicio DATE NOT NULL,
fecha_fin DATE NOT NULL,
-- Precios de referencia
precio_diesel_referencia DECIMAL(10, 4), -- Precio base
precio_diesel_actual DECIMAL(10, 4),
-- Surcharge
porcentaje_surcharge DECIMAL(5, 2) NOT NULL,
-- Estado
activo BOOLEAN DEFAULT TRUE,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL
);
CREATE INDEX idx_fuel_surcharge_fecha ON billing.fuel_surcharge(tenant_id, fecha_inicio, fecha_fin);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE billing.lanes ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing.tarifas ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing.recargos_catalogo ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing.facturas_transporte ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing.lineas_factura ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing.fuel_surcharge ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_lanes ON billing.lanes
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_tarifas ON billing.tarifas
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_recargos ON billing.recargos_catalogo
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_facturas ON billing.facturas_transporte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_lineas ON billing.lineas_factura
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_fuel ON billing.fuel_surcharge
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE billing.lanes IS 'Rutas origen-destino para tarifas';
COMMENT ON TABLE billing.tarifas IS 'Catálogo de tarifas de transporte';
COMMENT ON TABLE billing.recargos_catalogo IS 'Catálogo de recargos aplicables';
COMMENT ON TABLE billing.facturas_transporte IS 'Facturas emitidas a clientes';
COMMENT ON TABLE billing.lineas_factura IS 'Detalle de líneas de factura';
COMMENT ON TABLE billing.fuel_surcharge IS 'Índices de fuel surcharge por período';
-- =============================================================================
-- FIN DDL BILLING
-- =============================================================================

View File

@ -0,0 +1,440 @@
-- =============================================================================
-- ERP TRANSPORTISTAS - Schema Compliance DDL
-- =============================================================================
-- Archivo: 08-compliance-schema-ddl.sql
-- Version: 1.0.0
-- Fecha: 2026-01-25
-- Descripcion: Carta Porte CFDI 3.1, HOS, inspecciones, NOM-087/068
-- =============================================================================
-- =============================================================================
-- TIPOS ENUMERADOS
-- =============================================================================
CREATE TYPE compliance.estado_carta_porte AS ENUM (
'BORRADOR',
'VALIDADA',
'TIMBRADA',
'CANCELADA'
);
CREATE TYPE compliance.tipo_cfdi_carta_porte AS ENUM (
'INGRESO', -- Servicio de transporte
'TRASLADO' -- Traslado propio
);
CREATE TYPE compliance.estado_hos AS ENUM (
'DRIVING', -- Conduciendo
'ON_DUTY', -- En servicio (no conduciendo)
'SLEEPER', -- En litera
'OFF_DUTY' -- Fuera de servicio
);
-- =============================================================================
-- TABLA: cartas_porte
-- =============================================================================
CREATE TABLE compliance.cartas_porte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Viaje relacionado
viaje_id UUID NOT NULL,
-- CFDI
tipo_cfdi compliance.tipo_cfdi_carta_porte NOT NULL,
version_carta_porte VARCHAR(10) DEFAULT '3.1',
-- Identificación CFDI
serie VARCHAR(10),
folio VARCHAR(20),
uuid_cfdi UUID,
fecha_timbrado TIMESTAMPTZ,
-- Emisor
emisor_rfc VARCHAR(13) NOT NULL,
emisor_nombre VARCHAR(200) NOT NULL,
emisor_regimen_fiscal VARCHAR(10),
-- Receptor (para Ingreso) / Propietario (para Traslado)
receptor_rfc VARCHAR(13) NOT NULL,
receptor_nombre VARCHAR(200) NOT NULL,
receptor_uso_cfdi VARCHAR(10),
receptor_domicilio_fiscal_cp VARCHAR(10),
-- Totales CFDI
subtotal DECIMAL(15, 2),
total DECIMAL(15, 2),
moneda VARCHAR(3) DEFAULT 'MXN',
-- Datos transporte federal
transporte_internacional BOOLEAN DEFAULT FALSE,
entrada_salida_merc VARCHAR(10), -- 'Entrada' o 'Salida'
pais_origen_destino VARCHAR(3),
-- Datos específicos autotransporte
permiso_sct VARCHAR(50),
num_permiso_sct VARCHAR(50),
config_vehicular VARCHAR(10), -- C2, C3, T3S2, etc.
peso_bruto_total DECIMAL(12, 3),
unidad_peso VARCHAR(10) DEFAULT 'KGM',
num_total_mercancias INT,
-- Seguro
asegura_resp_civil VARCHAR(100),
poliza_resp_civil VARCHAR(50),
asegura_med_ambiente VARCHAR(100),
poliza_med_ambiente VARCHAR(50),
asegura_carga VARCHAR(100),
poliza_carga VARCHAR(50),
prima_seguro DECIMAL(15, 2),
-- Estado
estado compliance.estado_carta_porte DEFAULT 'BORRADOR',
-- XML y PDF
xml_cfdi TEXT,
xml_carta_porte TEXT,
pdf_url TEXT,
qr_url TEXT,
-- Cancelación
fecha_cancelacion TIMESTAMPTZ,
motivo_cancelacion TEXT,
uuid_sustitucion UUID,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by_id UUID NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_carta_porte_tenant ON compliance.cartas_porte(tenant_id);
CREATE INDEX idx_carta_porte_viaje ON compliance.cartas_porte(viaje_id);
CREATE INDEX idx_carta_porte_uuid ON compliance.cartas_porte(uuid_cfdi);
CREATE INDEX idx_carta_porte_estado ON compliance.cartas_porte(tenant_id, estado);
-- =============================================================================
-- TABLA: ubicaciones_carta_porte
-- =============================================================================
CREATE TABLE compliance.ubicaciones_carta_porte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carta_porte_id UUID NOT NULL REFERENCES compliance.cartas_porte(id),
-- Tipo
tipo_ubicacion VARCHAR(10) NOT NULL, -- 'Origen' o 'Destino'
-- ID Ubicación (catálogo SAT)
id_ubicacion VARCHAR(10),
-- RFC
rfc_remitente_destinatario VARCHAR(13),
nombre_remitente_destinatario VARCHAR(200),
-- Domicilio
pais VARCHAR(3) DEFAULT 'MEX',
estado VARCHAR(10), -- Clave SAT
municipio VARCHAR(10), -- Clave SAT
localidad VARCHAR(10),
codigo_postal VARCHAR(10) NOT NULL,
colonia VARCHAR(10),
calle VARCHAR(200),
numero_exterior VARCHAR(50),
numero_interior VARCHAR(50),
referencia VARCHAR(500),
-- Fechas
fecha_hora_salida_llegada TIMESTAMPTZ,
-- Distancia
distancia_recorrida DECIMAL(10, 2), -- Solo para destinos
-- Secuencia (orden en la ruta)
secuencia INT NOT NULL
);
CREATE INDEX idx_ubicacion_carta ON compliance.ubicaciones_carta_porte(carta_porte_id);
-- =============================================================================
-- TABLA: mercancias_carta_porte
-- =============================================================================
CREATE TABLE compliance.mercancias_carta_porte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carta_porte_id UUID NOT NULL REFERENCES compliance.cartas_porte(id),
-- Bienes transportados
bienes_transp VARCHAR(10) NOT NULL, -- Clave SAT
descripcion VARCHAR(1000) NOT NULL,
cantidad DECIMAL(14, 3) NOT NULL,
clave_unidad VARCHAR(10) NOT NULL, -- Clave SAT
unidad VARCHAR(50),
-- Dimensiones
peso_en_kg DECIMAL(14, 3) NOT NULL,
largo_cm DECIMAL(10, 2),
ancho_cm DECIMAL(10, 2),
alto_cm DECIMAL(10, 2),
-- Valor
valor_mercancia DECIMAL(15, 2),
moneda VARCHAR(3) DEFAULT 'MXN',
-- Material peligroso
material_peligroso BOOLEAN DEFAULT FALSE,
cve_material_peligroso VARCHAR(10),
tipo_embalaje VARCHAR(10),
descripcion_embalaje VARCHAR(200),
-- Fracción arancelaria (comercio exterior)
fraccion_arancelaria VARCHAR(10),
uuid_comercio_ext UUID,
-- Pedimentos (comercio exterior)
pedimentos TEXT[], -- Array de números de pedimento
-- Guías (paquetería)
guias TEXT[], -- Array de números de guía
-- Secuencia
secuencia INT NOT NULL
);
CREATE INDEX idx_mercancia_carta ON compliance.mercancias_carta_porte(carta_porte_id);
-- =============================================================================
-- TABLA: figuras_transporte (Operador, propietario, arrendatario)
-- =============================================================================
CREATE TABLE compliance.figuras_transporte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carta_porte_id UUID NOT NULL REFERENCES compliance.cartas_porte(id),
-- Tipo de figura
tipo_figura VARCHAR(10) NOT NULL, -- '01'=Operador, '02'=Propietario, '03'=Arrendador
-- Datos
rfc_figura VARCHAR(13),
nombre_figura VARCHAR(200),
num_licencia VARCHAR(50), -- Solo para operadores
-- Domicilio (opcional)
pais VARCHAR(3),
estado VARCHAR(10),
codigo_postal VARCHAR(10),
calle VARCHAR(200),
-- Partes transporte (solo para Propietario/Arrendador)
partes_transporte JSONB -- Array de {parte_transporte: string}
);
CREATE INDEX idx_figura_carta ON compliance.figuras_transporte(carta_porte_id);
-- =============================================================================
-- TABLA: autotransporte_carta_porte
-- =============================================================================
CREATE TABLE compliance.autotransporte_carta_porte (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
carta_porte_id UUID NOT NULL REFERENCES compliance.cartas_porte(id),
-- Permiso
perm_sct VARCHAR(10) NOT NULL, -- Tipo permiso SAT
num_permiso_sct VARCHAR(50) NOT NULL,
-- Identificación vehicular tractora
config_vehicular VARCHAR(10) NOT NULL, -- C2, C3, T3S2, etc.
placa_vm VARCHAR(15) NOT NULL,
anio_modelo_vm INT,
-- Remolques (puede haber hasta 2)
remolques JSONB -- Array de {sub_tipo_rem, placa}
);
CREATE INDEX idx_autotransporte_carta ON compliance.autotransporte_carta_porte(carta_porte_id);
-- =============================================================================
-- TABLA: hos_logs (Hours of Service - NOM-087)
-- =============================================================================
CREATE TABLE compliance.hos_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Operador y viaje
operador_id UUID NOT NULL,
viaje_id UUID,
-- Log
fecha DATE NOT NULL,
hora_inicio TIME NOT NULL,
hora_fin TIME,
duracion_minutos INT,
-- Estado
estado compliance.estado_hos NOT NULL,
-- Ubicación
latitud DECIMAL(10, 7),
longitud DECIMAL(10, 7),
ubicacion_descripcion VARCHAR(200),
-- Odómetro
odometro_inicio INT,
odometro_fin INT,
-- Observaciones
observaciones TEXT,
-- Certificado
certificado_por_operador BOOLEAN DEFAULT FALSE,
certificado_fecha TIMESTAMPTZ,
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_hos_operador ON compliance.hos_logs(operador_id);
CREATE INDEX idx_hos_fecha ON compliance.hos_logs(tenant_id, fecha);
CREATE INDEX idx_hos_viaje ON compliance.hos_logs(viaje_id);
-- =============================================================================
-- TABLA: hos_resumen_diario
-- =============================================================================
CREATE TABLE compliance.hos_resumen_diario (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
operador_id UUID NOT NULL,
fecha DATE NOT NULL,
-- Horas por estado
horas_driving DECIMAL(4, 2) DEFAULT 0,
horas_on_duty DECIMAL(4, 2) DEFAULT 0,
horas_sleeper DECIMAL(4, 2) DEFAULT 0,
horas_off_duty DECIMAL(4, 2) DEFAULT 0,
horas_totales DECIMAL(4, 2) DEFAULT 0,
-- Cumplimiento NOM-087
horas_conduccion_disponibles DECIMAL(4, 2),
horas_servicio_disponibles DECIMAL(4, 2),
en_cumplimiento BOOLEAN DEFAULT TRUE,
violaciones TEXT[],
-- Acumulados (ciclo de 7 días)
horas_conduccion_ciclo DECIMAL(5, 2),
horas_servicio_ciclo DECIMAL(5, 2),
-- Certificación
certificado BOOLEAN DEFAULT FALSE,
certificado_fecha TIMESTAMPTZ,
CONSTRAINT uq_hos_resumen UNIQUE (operador_id, fecha)
);
CREATE INDEX idx_hos_resumen_operador ON compliance.hos_resumen_diario(operador_id);
CREATE INDEX idx_hos_resumen_fecha ON compliance.hos_resumen_diario(tenant_id, fecha);
-- =============================================================================
-- TABLA: inspecciones_pre_viaje
-- =============================================================================
CREATE TABLE compliance.inspecciones_pre_viaje (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id),
-- Viaje y unidad
viaje_id UUID NOT NULL,
unidad_id UUID NOT NULL,
remolque_id UUID,
operador_id UUID NOT NULL,
-- Fecha
fecha_inspeccion TIMESTAMPTZ NOT NULL,
-- Resultado general
aprobada BOOLEAN DEFAULT FALSE,
-- Items checklist (JSON con resultados)
checklist_items JSONB NOT NULL,
-- Ejemplo: [{ "item": "Frenos", "estado": "OK", "observacion": null }, ...]
-- Defectos encontrados
defectos_encontrados TEXT[],
defectos_criticos INT DEFAULT 0,
defectos_menores INT DEFAULT 0,
-- Firma
firma_operador TEXT, -- Base64
firma_fecha TIMESTAMPTZ,
-- Evidencias
fotos JSONB, -- URLs de fotos
-- Auditoría
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_inspeccion_viaje ON compliance.inspecciones_pre_viaje(viaje_id);
CREATE INDEX idx_inspeccion_unidad ON compliance.inspecciones_pre_viaje(unidad_id);
CREATE INDEX idx_inspeccion_fecha ON compliance.inspecciones_pre_viaje(tenant_id, fecha_inspeccion);
-- =============================================================================
-- RLS POLICIES
-- =============================================================================
ALTER TABLE compliance.cartas_porte ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.ubicaciones_carta_porte ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.mercancias_carta_porte ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.figuras_transporte ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.autotransporte_carta_porte ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.hos_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.hos_resumen_diario ENABLE ROW LEVEL SECURITY;
ALTER TABLE compliance.inspecciones_pre_viaje ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_cartas ON compliance.cartas_porte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_ubicaciones ON compliance.ubicaciones_carta_porte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_mercancias ON compliance.mercancias_carta_porte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_figuras ON compliance.figuras_transporte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_autotransporte ON compliance.autotransporte_carta_porte
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_hos ON compliance.hos_logs
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_hos_resumen ON compliance.hos_resumen_diario
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation_inspecciones ON compliance.inspecciones_pre_viaje
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- =============================================================================
-- COMENTARIOS
-- =============================================================================
COMMENT ON TABLE compliance.cartas_porte IS 'CFDI con complemento Carta Porte 3.1';
COMMENT ON TABLE compliance.ubicaciones_carta_porte IS 'Ubicaciones origen/destino de la carta porte';
COMMENT ON TABLE compliance.mercancias_carta_porte IS 'Mercancías transportadas en la carta porte';
COMMENT ON TABLE compliance.figuras_transporte IS 'Figuras de transporte (operador, propietario, arrendador)';
COMMENT ON TABLE compliance.autotransporte_carta_porte IS 'Datos del autotransporte federal';
COMMENT ON TABLE compliance.hos_logs IS 'Registros de horas de servicio (NOM-087)';
COMMENT ON TABLE compliance.hos_resumen_diario IS 'Resumen diario de HOS por operador';
COMMENT ON TABLE compliance.inspecciones_pre_viaje IS 'Inspecciones pre-viaje de unidades';
-- =============================================================================
-- FIN DDL COMPLIANCE
-- =============================================================================