erp-transportistas-v2/docs/30-integraciones/INTEGRACION-GPS-PROVIDERS.md
Adrian Flores Cortes 6ed7f9e2ec [BACKUP] Pre-restructure workspace backup 2026-01-29
- Updated docs and inventory files
- Added new architecture docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:35:54 -06:00

32 KiB

Integracion GPS Multi-Provider

Proyecto: ERP Transportistas | Version: 1.0.0 | Actualizado: 2026-01-27 Referencia GAP: GAP-001 - Modulo GPS Completo (multi-provider)


Descripcion General

El sistema de tracking de ERP Transportistas implementa una arquitectura multi-provider para la ingestion de posiciones GPS. Esta arquitectura permite integrar diferentes proveedores de telematica sin modificar la logica de negocio, utilizando el patron Adapter para normalizar los datos de cada proveedor al esquema unificado del sistema.

La arquitectura esta disenada para:

  • Soportar multiples proveedores GPS simultaneamente
  • Permitir migracion entre proveedores sin afectar la operacion
  • Mantener historico de posiciones independiente del proveedor
  • Facilitar la incorporacion de nuevos proveedores

Arquitectura Multi-Provider

Patron de Abstraccion

El sistema utiliza el patron Adapter combinado con una Factory para abstraer la comunicacion con los diferentes proveedores GPS. Cada proveedor implementa una interface comun que normaliza los datos al formato interno del sistema.

+------------------+     +-------------------+     +------------------+
|  Traccar API     | --> |  TraccarAdapter   | --> |                  |
+------------------+     +-------------------+     |                  |
                                                   |                  |
+------------------+     +-------------------+     |   IGpsProvider   |
|  Wialon API      | --> |  WialonAdapter    | --> |   Interface      |
+------------------+     +-------------------+     |                  |
                                                   |                  |
+------------------+     +-------------------+     |                  |
|  Samsara API     | --> |  SamsaraAdapter   | --> |                  |
+------------------+     +-------------------+     +--------+---------+
                                                            |
+------------------+     +-------------------+              |
|  Geotab API      | --> |  GeotabAdapter    | -------------+
+------------------+     +-------------------+              |
                                                            v
+------------------+     +-------------------+     +------------------+
|  Manual Entry    | --> |  ManualAdapter    | --> | GpsPositionDto   |
+------------------+     +-------------------+     +------------------+
                                                            |
                                                            v
                                                  +------------------+
                                                  | tracking.        |
                                                  | posiciones_gps   |
                                                  +------------------+

Interface IGpsProvider

La interface principal que todos los adapters deben implementar:

// src/modules/tracking/interfaces/gps-provider.interface.ts

export interface GpsPosition {
  deviceId: string;           // IMEI o identificador unico del dispositivo
  timestamp: Date;            // Fecha/hora del GPS (UTC)
  latitude: number;           // Latitud (-90 a 90)
  longitude: number;          // Longitud (-180 a 180)
  altitude?: number;          // Altitud en metros
  speed?: number;             // Velocidad en km/h
  course?: number;            // Rumbo (0-360 grados)
  odometer?: number;          // Odometro en km
  engineOn?: boolean;         // Estado del motor
  hdop?: number;              // Precision horizontal (1-50)
  satellites?: number;        // Numero de satelites
  attributes?: Record<string, any>; // Datos adicionales del proveedor
}

export interface DeviceInfo {
  deviceId: string;
  imei: string;
  name: string;
  model?: string;
  phone?: string;
  status: 'online' | 'offline' | 'unknown';
  lastPosition?: GpsPosition;
}

export interface IGpsProvider {
  // Identificador del proveedor
  readonly providerName: string;

  // Conexion y autenticacion
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;

  // Dispositivos
  getDevices(): Promise<DeviceInfo[]>;
  getDevice(deviceId: string): Promise<DeviceInfo | null>;

  // Posiciones
  getLastPosition(deviceId: string): Promise<GpsPosition | null>;
  getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]>;

  // Tiempo real (opcional - no todos los proveedores lo soportan)
  subscribeToPositions?(callback: (position: GpsPosition) => void): void;
  unsubscribeFromPositions?(): void;

  // Webhook (opcional - para proveedores que soportan push)
  parseWebhookPayload?(payload: any): GpsPosition | GpsPosition[] | null;
}

GpsProviderFactory

Factory para obtener el adapter correcto segun configuracion:

// src/modules/tracking/providers/gps-provider.factory.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IGpsProvider } from '../interfaces/gps-provider.interface';
import { TraccarAdapter } from './traccar.adapter';
import { WialonAdapter } from './wialon.adapter';
import { SamsaraAdapter } from './samsara.adapter';
import { GeotabAdapter } from './geotab.adapter';
import { ManualAdapter } from './manual.adapter';

export type GpsProviderType = 'traccar' | 'wialon' | 'samsara' | 'geotab' | 'manual';

@Injectable()
export class GpsProviderFactory {
  constructor(private configService: ConfigService) {}

  create(provider?: GpsProviderType): IGpsProvider {
    const providerType = provider || this.configService.get<GpsProviderType>('GPS_PROVIDER', 'manual');

    switch (providerType) {
      case 'traccar':
        return new TraccarAdapter(this.configService);
      case 'wialon':
        return new WialonAdapter(this.configService);
      case 'samsara':
        return new SamsaraAdapter(this.configService);
      case 'geotab':
        return new GeotabAdapter(this.configService);
      case 'manual':
      default:
        return new ManualAdapter();
    }
  }

  getSupportedProviders(): GpsProviderType[] {
    return ['traccar', 'wialon', 'samsara', 'geotab', 'manual'];
  }
}

Proveedores Soportados

1. Traccar (Recomendado)

Tipo: Open Source, Auto-hospedado Licencia: Apache 2.0 (Gratuito) Documentacion: https://www.traccar.org/documentation/

Caracteristica Valor
API REST + WebSocket
Autenticacion Basic Auth / Token
Protocolos GPS 200+ (GT06, Teltonika, Queclink, etc.)
Tiempo Real WebSocket nativo
Webhook Soportado
Geocercas Soportado
Alertas Soportado

Ventajas:

  • Sin costo de licencias
  • Control total de datos (on-premise)
  • Soporta la mayoria de dispositivos GPS del mercado
  • WebSocket para actualizaciones en tiempo real
  • API REST completa y bien documentada
  • Comunidad activa

Configuracion:

// src/modules/tracking/providers/traccar.adapter.ts

import { IGpsProvider, GpsPosition, DeviceInfo } from '../interfaces/gps-provider.interface';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import WebSocket from 'ws';

export class TraccarAdapter implements IGpsProvider {
  readonly providerName = 'traccar';
  private client: AxiosInstance;
  private ws: WebSocket | null = null;
  private connected = false;

  constructor(private configService: ConfigService) {
    const baseURL = this.configService.get<string>('TRACCAR_API_URL', 'http://localhost:8082/api');
    const email = this.configService.get<string>('TRACCAR_EMAIL');
    const password = this.configService.get<string>('TRACCAR_PASSWORD');

    this.client = axios.create({
      baseURL,
      auth: { username: email, password },
      headers: { 'Content-Type': 'application/json' },
    });
  }

  async connect(): Promise<void> {
    // Verificar conexion obteniendo sesion
    const response = await this.client.get('/session');
    if (response.status === 200) {
      this.connected = true;
    }
  }

  async disconnect(): Promise<void> {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.connected = false;
  }

  isConnected(): boolean {
    return this.connected;
  }

  async getDevices(): Promise<DeviceInfo[]> {
    const response = await this.client.get('/devices');
    return response.data.map((d: any) => this.mapDevice(d));
  }

  async getDevice(deviceId: string): Promise<DeviceInfo | null> {
    const response = await this.client.get(`/devices?uniqueId=${deviceId}`);
    if (response.data.length > 0) {
      return this.mapDevice(response.data[0]);
    }
    return null;
  }

  async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
    const device = await this.getDevice(deviceId);
    if (!device) return null;

    const response = await this.client.get(`/positions?deviceId=${device.deviceId}`);
    if (response.data.length > 0) {
      return this.mapPosition(response.data[0]);
    }
    return null;
  }

  async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
    const device = await this.getDevice(deviceId);
    if (!device) return [];

    const response = await this.client.get('/positions', {
      params: {
        deviceId: device.deviceId,
        from: from.toISOString(),
        to: to.toISOString(),
      },
    });
    return response.data.map((p: any) => this.mapPosition(p));
  }

  subscribeToPositions(callback: (position: GpsPosition) => void): void {
    const wsUrl = this.configService.get<string>('TRACCAR_WS_URL', 'ws://localhost:8082/api/socket');

    this.ws = new WebSocket(wsUrl);

    this.ws.on('message', (data: string) => {
      const message = JSON.parse(data);
      if (message.positions) {
        message.positions.forEach((p: any) => {
          callback(this.mapPosition(p));
        });
      }
    });
  }

  unsubscribeFromPositions(): void {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }

  parseWebhookPayload(payload: any): GpsPosition | null {
    if (payload.position) {
      return this.mapPosition(payload.position);
    }
    return null;
  }

  private mapDevice(traccarDevice: any): DeviceInfo {
    return {
      deviceId: traccarDevice.id.toString(),
      imei: traccarDevice.uniqueId,
      name: traccarDevice.name,
      model: traccarDevice.model,
      phone: traccarDevice.phone,
      status: traccarDevice.status === 'online' ? 'online' : 'offline',
    };
  }

  private mapPosition(traccarPosition: any): GpsPosition {
    return {
      deviceId: traccarPosition.deviceId.toString(),
      timestamp: new Date(traccarPosition.deviceTime || traccarPosition.fixTime),
      latitude: traccarPosition.latitude,
      longitude: traccarPosition.longitude,
      altitude: traccarPosition.altitude,
      speed: traccarPosition.speed ? traccarPosition.speed * 1.852 : 0, // knots to km/h
      course: traccarPosition.course,
      odometer: traccarPosition.attributes?.totalDistance
        ? traccarPosition.attributes.totalDistance / 1000
        : undefined,
      engineOn: traccarPosition.attributes?.ignition,
      hdop: traccarPosition.attributes?.hdop,
      satellites: traccarPosition.attributes?.sat,
      attributes: traccarPosition.attributes,
    };
  }
}

Variables de Entorno:

# Traccar Configuration
GPS_PROVIDER=traccar
TRACCAR_API_URL=http://traccar.example.com:8082/api
TRACCAR_WS_URL=ws://traccar.example.com:8082/api/socket
TRACCAR_EMAIL=admin@example.com
TRACCAR_PASSWORD=secret

2. Wialon (Gurtam)

Tipo: SaaS / On-Premise Licencia: Comercial (por unidad/mes) Documentacion: https://sdk.wialon.com/

Caracteristica Valor
API REST (RemoteAPI)
Autenticacion Token
Protocolos GPS 2500+
Tiempo Real Long Polling
Webhook Limitado
Geocercas Avanzado
Alertas Avanzado

Ventajas:

  • Plataforma madura y estable
  • Soporte para casi cualquier dispositivo GPS
  • Herramientas de analisis avanzadas
  • Soporte tecnico profesional

Configuracion:

// src/modules/tracking/providers/wialon.adapter.ts

export class WialonAdapter implements IGpsProvider {
  readonly providerName = 'wialon';
  private sid: string | null = null;
  private baseUrl: string;
  private token: string;

  constructor(private configService: ConfigService) {
    this.baseUrl = this.configService.get<string>('WIALON_API_URL', 'https://hst-api.wialon.com/wialon/ajax.html');
    this.token = this.configService.get<string>('WIALON_TOKEN', '');
  }

  async connect(): Promise<void> {
    const response = await axios.get(this.baseUrl, {
      params: {
        svc: 'token/login',
        params: JSON.stringify({ token: this.token }),
      },
    });

    if (response.data.eid) {
      this.sid = response.data.eid;
    } else {
      throw new Error('Wialon authentication failed');
    }
  }

  async getDevices(): Promise<DeviceInfo[]> {
    const response = await this.request('core/search_items', {
      spec: {
        itemsType: 'avl_unit',
        propName: 'sys_name',
        propValueMask: '*',
        sortType: 'sys_name',
      },
      force: 1,
      flags: 1025, // 1 (base) + 1024 (last message)
      from: 0,
      to: 0,
    });

    return response.items.map((unit: any) => this.mapDevice(unit));
  }

  async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
    const response = await this.request('core/search_item', {
      id: deviceId,
      flags: 1025,
    });

    if (response.item && response.item.pos) {
      return this.mapPosition(response.item);
    }
    return null;
  }

  async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
    const response = await this.request('messages/load_interval', {
      itemId: deviceId,
      timeFrom: Math.floor(from.getTime() / 1000),
      timeTo: Math.floor(to.getTime() / 1000),
      flags: 0,
      flagsMask: 0,
      loadCount: 10000,
    });

    return response.messages
      .filter((m: any) => m.pos)
      .map((m: any) => this.mapMessageToPosition(deviceId, m));
  }

  private async request(svc: string, params: any): Promise<any> {
    const response = await axios.get(this.baseUrl, {
      params: {
        svc,
        params: JSON.stringify(params),
        sid: this.sid,
      },
    });
    return response.data;
  }

  private mapPosition(unit: any): GpsPosition {
    return {
      deviceId: unit.id.toString(),
      timestamp: new Date(unit.pos.t * 1000),
      latitude: unit.pos.y,
      longitude: unit.pos.x,
      altitude: unit.pos.z,
      speed: unit.pos.s,
      course: unit.pos.c,
      satellites: unit.pos.sc,
    };
  }
}

Variables de Entorno:

# Wialon Configuration
GPS_PROVIDER=wialon
WIALON_API_URL=https://hst-api.wialon.com/wialon/ajax.html
WIALON_TOKEN=your_wialon_token_here

3. Samsara

Tipo: SaaS (Fleet Management Moderno) Licencia: Comercial (por vehiculo/mes) Documentacion: https://developers.samsara.com/

Caracteristica Valor
API REST (OpenAPI 3.0)
Autenticacion Bearer Token
Dispositivos Hardware propietario
Tiempo Real Webhooks
Geocercas Soportado
Alertas Avanzado
Dashcam Integrado

Ventajas:

  • API moderna y bien documentada
  • Webhooks para tiempo real
  • Integracion con dashcam y ELD
  • Soporte para IFTA, DVIR, HOS

Configuracion:

// src/modules/tracking/providers/samsara.adapter.ts

export class SamsaraAdapter implements IGpsProvider {
  readonly providerName = 'samsara';
  private client: AxiosInstance;

  constructor(private configService: ConfigService) {
    const apiKey = this.configService.get<string>('SAMSARA_API_KEY');

    this.client = axios.create({
      baseURL: 'https://api.samsara.com',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
    });
  }

  async getDevices(): Promise<DeviceInfo[]> {
    const response = await this.client.get('/fleet/vehicles');
    return response.data.data.map((v: any) => ({
      deviceId: v.id,
      imei: v.serial,
      name: v.name,
      model: v.make + ' ' + v.model,
      status: v.gps?.time ? 'online' : 'offline',
    }));
  }

  async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
    const response = await this.client.get(`/fleet/vehicles/${deviceId}/locations`);
    const location = response.data.data[0];

    if (!location) return null;

    return {
      deviceId,
      timestamp: new Date(location.time),
      latitude: location.latitude,
      longitude: location.longitude,
      speed: location.speed ? location.speed * 3.6 : 0, // m/s to km/h
      course: location.heading,
    };
  }

  async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
    const response = await this.client.get(`/fleet/vehicles/${deviceId}/locations/history`, {
      params: {
        startTime: from.toISOString(),
        endTime: to.toISOString(),
      },
    });

    return response.data.data.map((loc: any) => ({
      deviceId,
      timestamp: new Date(loc.time),
      latitude: loc.latitude,
      longitude: loc.longitude,
      speed: loc.speed ? loc.speed * 3.6 : 0,
      course: loc.heading,
    }));
  }

  parseWebhookPayload(payload: any): GpsPosition | null {
    if (payload.eventType === 'VehicleGpsUpdated') {
      const data = payload.data;
      return {
        deviceId: data.vehicle.id,
        timestamp: new Date(data.gps.time),
        latitude: data.gps.latitude,
        longitude: data.gps.longitude,
        speed: data.gps.speedMilesPerHour ? data.gps.speedMilesPerHour * 1.60934 : 0,
        course: data.gps.headingDegrees,
      };
    }
    return null;
  }
}

Variables de Entorno:

# Samsara Configuration
GPS_PROVIDER=samsara
SAMSARA_API_KEY=your_samsara_api_key_here
SAMSARA_WEBHOOK_SECRET=your_webhook_secret

4. Geotab

Tipo: SaaS (Telematica Empresarial) Licencia: Comercial (por dispositivo/mes) Documentacion: https://developers.geotab.com/

Caracteristica Valor
API REST + OData
Autenticacion Session Token
Dispositivos Hardware propietario (GO)
Tiempo Real Data Feed
Geocercas Avanzado
Alertas Reglas configurables
Add-Ins Marketplace extenso

Ventajas:

  • Precision de datos excepcional
  • SDK y Add-In framework
  • Integraciones empresariales
  • Reportes de conduccion detallados

Configuracion:

// src/modules/tracking/providers/geotab.adapter.ts

export class GeotabAdapter implements IGpsProvider {
  readonly providerName = 'geotab';
  private credentials: any;
  private sessionId: string | null = null;

  constructor(private configService: ConfigService) {
    this.credentials = {
      database: this.configService.get<string>('GEOTAB_DATABASE'),
      userName: this.configService.get<string>('GEOTAB_USERNAME'),
      password: this.configService.get<string>('GEOTAB_PASSWORD'),
    };
  }

  async connect(): Promise<void> {
    const response = await axios.post(
      `https://${this.configService.get<string>('GEOTAB_SERVER', 'my.geotab.com')}/apiv1`,
      {
        method: 'Authenticate',
        params: this.credentials,
      }
    );

    this.sessionId = response.data.result.credentials.sessionId;
  }

  async getDevices(): Promise<DeviceInfo[]> {
    const response = await this.call('Get', {
      typeName: 'Device',
      resultsLimit: 1000,
    });

    return response.map((d: any) => ({
      deviceId: d.id,
      imei: d.serialNumber,
      name: d.name,
      model: d.deviceType,
      status: 'unknown',
    }));
  }

  async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
    const response = await this.call('Get', {
      typeName: 'DeviceStatusInfo',
      search: { deviceSearch: { id: deviceId } },
    });

    if (response.length === 0) return null;

    const status = response[0];
    return {
      deviceId,
      timestamp: new Date(status.dateTime),
      latitude: status.latitude,
      longitude: status.longitude,
      speed: status.speed,
      course: status.bearing,
    };
  }

  async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
    const response = await this.call('Get', {
      typeName: 'LogRecord',
      search: {
        deviceSearch: { id: deviceId },
        fromDate: from.toISOString(),
        toDate: to.toISOString(),
      },
    });

    return response.map((log: any) => ({
      deviceId,
      timestamp: new Date(log.dateTime),
      latitude: log.latitude,
      longitude: log.longitude,
      speed: log.speed,
    }));
  }

  private async call(method: string, params: any): Promise<any> {
    const response = await axios.post(
      `https://${this.configService.get<string>('GEOTAB_SERVER', 'my.geotab.com')}/apiv1`,
      {
        method,
        params: {
          ...params,
          credentials: { ...this.credentials, sessionId: this.sessionId },
        },
      }
    );
    return response.data.result;
  }
}

Variables de Entorno:

# Geotab Configuration
GPS_PROVIDER=geotab
GEOTAB_SERVER=my.geotab.com
GEOTAB_DATABASE=your_database
GEOTAB_USERNAME=your_username
GEOTAB_PASSWORD=your_password

5. Manual (Fallback)

Tipo: Entrada Manual / Importacion Licencia: N/A (Sin costo) Uso: Fallback cuando no hay GPS, pruebas, migracion de datos

Caracteristica Valor
API Endpoints propios
Autenticacion JWT del sistema
Dispositivos Virtuales
Tiempo Real No

Casos de Uso:

  • Empresas sin dispositivos GPS
  • Periodo de transicion
  • Pruebas y desarrollo
  • Importacion de datos historicos
// src/modules/tracking/providers/manual.adapter.ts

export class ManualAdapter implements IGpsProvider {
  readonly providerName = 'manual';
  private connected = true;

  async connect(): Promise<void> {
    this.connected = true;
  }

  async disconnect(): Promise<void> {
    this.connected = false;
  }

  isConnected(): boolean {
    return this.connected;
  }

  async getDevices(): Promise<DeviceInfo[]> {
    // Retorna dispositivos virtuales registrados en el sistema
    return [];
  }

  async getDevice(deviceId: string): Promise<DeviceInfo | null> {
    return null;
  }

  async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
    // Consulta directamente a la BD local
    return null;
  }

  async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
    // Consulta directamente a la BD local
    return [];
  }

  // El adapter manual no requiere parseo de webhook
  // Las posiciones se ingresan via endpoint POST /api/tracking/posiciones
}

Entidades de Base de Datos

GpsDevice (Dispositivo GPS)

Almacena la configuracion de dispositivos GPS vinculados a unidades.

Tabla: tracking.gps_devices

Campo Tipo Descripcion
id UUID Identificador unico
tenant_id UUID Tenant propietario
unidad_id UUID FK a fleet.unidades
imei VARCHAR(50) IMEI del dispositivo
proveedor VARCHAR(50) traccar, wialon, samsara, geotab, manual
device_id_externo VARCHAR(100) ID en el sistema del proveedor
nombre VARCHAR(100) Nombre descriptivo
modelo VARCHAR(100) Modelo del dispositivo
sim_numero VARCHAR(20) Numero de SIM
sim_operador VARCHAR(50) Operador de la SIM
intervalo_segundos INTEGER Intervalo de reporte (default 30)
activo BOOLEAN Estado activo/inactivo
ultima_posicion_at TIMESTAMPTZ Fecha de ultima posicion
created_at TIMESTAMPTZ Fecha de creacion
updated_at TIMESTAMPTZ Fecha de actualizacion

Indices:

  • idx_gps_device_tenant (tenant_id)
  • idx_gps_device_imei UNIQUE (tenant_id, imei)
  • idx_gps_device_unidad (unidad_id)

GpsPosition (Posicion GPS)

Almacena las posiciones GPS recibidas. Tabla particionada por mes.

Tabla: tracking.posiciones_gps

Campo Tipo Descripcion
id UUID Identificador unico
tenant_id UUID Tenant propietario
unidad_id UUID FK a fleet.unidades
viaje_id UUID FK a transport.viajes (opcional)
imei VARCHAR(50) IMEI del dispositivo
proveedor_gps VARCHAR(50) Proveedor de la posicion
timestamp_gps TIMESTAMPTZ Fecha/hora del GPS
timestamp_servidor TIMESTAMPTZ Fecha/hora de recepcion
latitud DECIMAL(10,7) Latitud
longitud DECIMAL(10,7) Longitud
altitud DECIMAL(8,2) Altitud en metros
velocidad_kmh DECIMAL(6,2) Velocidad en km/h
rumbo SMALLINT Direccion (0-360)
odometro_km DECIMAL(12,2) Odometro en km
motor_encendido BOOLEAN Estado del motor
hdop DECIMAL(4,2) Precision horizontal
satelites SMALLINT Numero de satelites
valida BOOLEAN Posicion valida/invalida
datos_extra JSONB Datos adicionales
fecha_particion DATE Campo de particionamiento

Particionamiento:

CREATE TABLE tracking.posiciones_gps (
  -- campos...
) PARTITION BY RANGE (fecha_particion);

-- Crear particiones mensuales automaticamente
CREATE TABLE tracking.posiciones_gps_2026_01 PARTITION OF tracking.posiciones_gps
  FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

RouteSegment (Segmento de Ruta)

Almacena segmentos de ruta calculados entre posiciones.

Tabla: tracking.route_segments

Campo Tipo Descripcion
id UUID Identificador unico
tenant_id UUID Tenant propietario
viaje_id UUID FK a transport.viajes
posicion_inicio_id UUID FK a posiciones_gps
posicion_fin_id UUID FK a posiciones_gps
distancia_km DECIMAL(10,3) Distancia del segmento
duracion_segundos INTEGER Duracion en segundos
velocidad_promedio_kmh DECIMAL(6,2) Velocidad promedio
en_movimiento BOOLEAN Si estaba en movimiento
geometria GEOMETRY(LINESTRING, 4326) LineString PostGIS
created_at TIMESTAMPTZ Fecha de creacion

Configuracion del Sistema

Variables de Entorno

# ===========================================
# GPS Provider Configuration
# ===========================================

# Proveedor activo: traccar | wialon | samsara | geotab | manual
GPS_PROVIDER=traccar

# URL para recibir webhooks de proveedores GPS
GPS_WEBHOOK_URL=https://api.example.com/webhooks/gps

# Intervalo de polling para proveedores sin tiempo real (segundos)
GPS_POLLING_INTERVAL=30

# ===========================================
# Traccar (Recomendado)
# ===========================================
TRACCAR_API_URL=http://traccar.example.com:8082/api
TRACCAR_WS_URL=ws://traccar.example.com:8082/api/socket
TRACCAR_EMAIL=admin@example.com
TRACCAR_PASSWORD=secret

# ===========================================
# Wialon
# ===========================================
WIALON_API_URL=https://hst-api.wialon.com/wialon/ajax.html
WIALON_TOKEN=your_token_here

# ===========================================
# Samsara
# ===========================================
SAMSARA_API_KEY=samsara_api_key
SAMSARA_WEBHOOK_SECRET=webhook_secret

# ===========================================
# Geotab
# ===========================================
GEOTAB_SERVER=my.geotab.com
GEOTAB_DATABASE=your_database
GEOTAB_USERNAME=your_username
GEOTAB_PASSWORD=your_password

Configuracion Multi-Tenant

Cada tenant puede tener su propia configuracion de proveedor GPS:

// tenant_settings en la tabla de tenants
{
  "gps": {
    "provider": "traccar",
    "config": {
      "apiUrl": "https://tenant-traccar.example.com/api",
      "credentials": "encrypted_reference"
    },
    "defaultInterval": 30,
    "validationRules": {
      "maxSpeed": 180,
      "minSatellites": 4,
      "maxHdop": 10
    }
  }
}

API Endpoints

Ingestion de Posiciones

POST /api/tracking/posiciones

Endpoint para recibir posiciones GPS (usado por webhooks o adapter manual).

Request:

{
  "deviceId": "352093088937641",
  "timestamp": "2026-01-27T14:30:00Z",
  "latitude": 19.4326,
  "longitude": -99.1332,
  "speed": 65.5,
  "course": 180,
  "altitude": 2240,
  "engineOn": true,
  "hdop": 1.2,
  "satellites": 12
}

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "unidadId": "unit-uuid",
  "viajeId": "viaje-uuid",
  "procesado": true,
  "alertasGeneradas": 0
}

Webhook Endpoint

POST /api/webhooks/gps/:provider

Endpoint generico para recibir webhooks de cualquier proveedor.

POST /api/webhooks/gps/traccar
POST /api/webhooks/gps/samsara

Validacion de Posiciones

El sistema valida cada posicion recibida antes de almacenarla:

// src/modules/tracking/services/position-validator.service.ts

export interface ValidationResult {
  valid: boolean;
  reasons: string[];
}

export class PositionValidatorService {
  validate(position: GpsPosition, lastPosition?: GpsPosition): ValidationResult {
    const reasons: string[] = [];

    // 1. Coordenadas validas
    if (position.latitude < -90 || position.latitude > 90) {
      reasons.push('Latitud fuera de rango');
    }
    if (position.longitude < -180 || position.longitude > 180) {
      reasons.push('Longitud fuera de rango');
    }

    // 2. Velocidad razonable (max 200 km/h para transporte de carga)
    if (position.speed && position.speed > 200) {
      reasons.push('Velocidad excesiva');
    }

    // 3. HDOP aceptable (precision)
    if (position.hdop && position.hdop > 10) {
      reasons.push('Precision GPS baja');
    }

    // 4. Minimo de satelites
    if (position.satellites && position.satellites < 4) {
      reasons.push('Satelites insuficientes');
    }

    // 5. Salto de posicion (teleportacion)
    if (lastPosition) {
      const distance = this.calculateDistance(
        lastPosition.latitude, lastPosition.longitude,
        position.latitude, position.longitude
      );
      const timeDiff = (position.timestamp.getTime() - lastPosition.timestamp.getTime()) / 1000;
      const maxPossibleDistance = (200 / 3600) * timeDiff; // km a max 200 km/h

      if (distance > maxPossibleDistance * 1.5) {
        reasons.push('Salto de posicion detectado');
      }
    }

    return {
      valid: reasons.length === 0,
      reasons,
    };
  }

  private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
    // Formula Haversine
    const R = 6371; // Radio de la Tierra en km
    const dLat = this.toRad(lat2 - lat1);
    const dLon = this.toRad(lon2 - lon1);
    const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
              Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) *
              Math.sin(dLon/2) * Math.sin(dLon/2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    return R * c;
  }

  private toRad(deg: number): number {
    return deg * Math.PI / 180;
  }
}

Migracion Entre Proveedores

Procedimiento para cambiar de proveedor GPS:

  1. Preparacion:

    • Configurar nuevo proveedor en paralelo
    • Mapear IMEI/device_id entre sistemas
    • Ejecutar pruebas de conectividad
  2. Migracion Gradual:

    • Activar nuevo proveedor para unidades piloto
    • Monitorear recepcion de posiciones
    • Validar consistencia de datos
  3. Cutover:

    • Actualizar GPS_PROVIDER en configuracion
    • Actualizar mapping de dispositivos
    • Desactivar proveedor anterior
  4. Post-Migracion:

    • Verificar todas las unidades reportando
    • Revisar alertas y geocercas
    • Documentar configuracion final

Referencias


Integracion GPS Multi-Provider - ERP Transportistas v1.0.0