# 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: ```typescript // 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; // 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; disconnect(): Promise; isConnected(): boolean; // Dispositivos getDevices(): Promise; getDevice(deviceId: string): Promise; // Posiciones getLastPosition(deviceId: string): Promise; getPositions(deviceId: string, from: Date, to: Date): Promise; // 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: ```typescript // 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('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:** ```typescript // 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('TRACCAR_API_URL', 'http://localhost:8082/api'); const email = this.configService.get('TRACCAR_EMAIL'); const password = this.configService.get('TRACCAR_PASSWORD'); this.client = axios.create({ baseURL, auth: { username: email, password }, headers: { 'Content-Type': 'application/json' }, }); } async connect(): Promise { // Verificar conexion obteniendo sesion const response = await this.client.get('/session'); if (response.status === 200) { this.connected = true; } } async disconnect(): Promise { if (this.ws) { this.ws.close(); this.ws = null; } this.connected = false; } isConnected(): boolean { return this.connected; } async getDevices(): Promise { const response = await this.client.get('/devices'); return response.data.map((d: any) => this.mapDevice(d)); } async getDevice(deviceId: string): Promise { 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 { 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 { 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('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:** ```env # 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:** ```typescript // 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('WIALON_API_URL', 'https://hst-api.wialon.com/wialon/ajax.html'); this.token = this.configService.get('WIALON_TOKEN', ''); } async connect(): Promise { 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 { 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 { 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 { 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 { 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:** ```env # 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:** ```typescript // 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('SAMSARA_API_KEY'); this.client = axios.create({ baseURL: 'https://api.samsara.com', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, }); } async getDevices(): Promise { 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 { 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 { 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:** ```env # 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:** ```typescript // 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('GEOTAB_DATABASE'), userName: this.configService.get('GEOTAB_USERNAME'), password: this.configService.get('GEOTAB_PASSWORD'), }; } async connect(): Promise { const response = await axios.post( `https://${this.configService.get('GEOTAB_SERVER', 'my.geotab.com')}/apiv1`, { method: 'Authenticate', params: this.credentials, } ); this.sessionId = response.data.result.credentials.sessionId; } async getDevices(): Promise { 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 { 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 { 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 { const response = await axios.post( `https://${this.configService.get('GEOTAB_SERVER', 'my.geotab.com')}/apiv1`, { method, params: { ...params, credentials: { ...this.credentials, sessionId: this.sessionId }, }, } ); return response.data.result; } } ``` **Variables de Entorno:** ```env # 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 ```typescript // src/modules/tracking/providers/manual.adapter.ts export class ManualAdapter implements IGpsProvider { readonly providerName = 'manual'; private connected = true; async connect(): Promise { this.connected = true; } async disconnect(): Promise { this.connected = false; } isConnected(): boolean { return this.connected; } async getDevices(): Promise { // Retorna dispositivos virtuales registrados en el sistema return []; } async getDevice(deviceId: string): Promise { return null; } async getLastPosition(deviceId: string): Promise { // Consulta directamente a la BD local return null; } async getPositions(deviceId: string, from: Date, to: Date): Promise { // 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:** ```sql 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 ```env # =========================================== # 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: ```typescript // 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:** ```json { "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:** ```json { "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: ```typescript // 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 - [Traccar Documentation](https://www.traccar.org/documentation/) - [Wialon SDK](https://sdk.wialon.com/) - [Samsara API Reference](https://developers.samsara.com/reference) - [Geotab SDK](https://developers.geotab.com/sdk/introduction) - [PostGIS Documentation](https://postgis.net/documentation/) --- *Integracion GPS Multi-Provider - ERP Transportistas v1.0.0*