From 7a91823784169effba9ca9659b6e055f2b4d3c57 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 10:26:20 -0600 Subject: [PATCH] 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 --- ddl/01-transport-schema-ddl.sql | 441 ++++++++++++++++++++++++++++++ ddl/02-fleet-schema-ddl.sql | 409 +++++++++++++++++++++++++++ ddl/03-tracking-schema-ddl.sql | 369 +++++++++++++++++++++++++ ddl/04-fuel-schema-ddl.sql | 315 +++++++++++++++++++++ ddl/05-maintenance-schema-ddl.sql | 306 +++++++++++++++++++++ ddl/06-carriers-schema-ddl.sql | 344 +++++++++++++++++++++++ ddl/07-billing-transport-ddl.sql | 352 ++++++++++++++++++++++++ ddl/08-compliance-schema-ddl.sql | 440 +++++++++++++++++++++++++++++ 8 files changed, 2976 insertions(+) create mode 100644 ddl/01-transport-schema-ddl.sql create mode 100644 ddl/02-fleet-schema-ddl.sql create mode 100644 ddl/03-tracking-schema-ddl.sql create mode 100644 ddl/04-fuel-schema-ddl.sql create mode 100644 ddl/05-maintenance-schema-ddl.sql create mode 100644 ddl/06-carriers-schema-ddl.sql create mode 100644 ddl/07-billing-transport-ddl.sql create mode 100644 ddl/08-compliance-schema-ddl.sql diff --git a/ddl/01-transport-schema-ddl.sql b/ddl/01-transport-schema-ddl.sql new file mode 100644 index 0000000..751137f --- /dev/null +++ b/ddl/01-transport-schema-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/02-fleet-schema-ddl.sql b/ddl/02-fleet-schema-ddl.sql new file mode 100644 index 0000000..7298329 --- /dev/null +++ b/ddl/02-fleet-schema-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/03-tracking-schema-ddl.sql b/ddl/03-tracking-schema-ddl.sql new file mode 100644 index 0000000..4f1880c --- /dev/null +++ b/ddl/03-tracking-schema-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/04-fuel-schema-ddl.sql b/ddl/04-fuel-schema-ddl.sql new file mode 100644 index 0000000..95db598 --- /dev/null +++ b/ddl/04-fuel-schema-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/05-maintenance-schema-ddl.sql b/ddl/05-maintenance-schema-ddl.sql new file mode 100644 index 0000000..1a83362 --- /dev/null +++ b/ddl/05-maintenance-schema-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/06-carriers-schema-ddl.sql b/ddl/06-carriers-schema-ddl.sql new file mode 100644 index 0000000..5ee1ffc --- /dev/null +++ b/ddl/06-carriers-schema-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/07-billing-transport-ddl.sql b/ddl/07-billing-transport-ddl.sql new file mode 100644 index 0000000..c39f23e --- /dev/null +++ b/ddl/07-billing-transport-ddl.sql @@ -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 +-- ============================================================================= diff --git a/ddl/08-compliance-schema-ddl.sql b/ddl/08-compliance-schema-ddl.sql new file mode 100644 index 0000000..1211702 --- /dev/null +++ b/ddl/08-compliance-schema-ddl.sql @@ -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 +-- =============================================================================