Skip to content

Ejemplos JavaScript/Node.js

Esta página contiene ejemplos completos de integración con la API TSALVA usando JavaScript tanto para navegadores como para Node.js.

📍 Importante: En todos los ejemplos de este documento, reemplaza [URL_API] con la URL base proporcionada por el área de TI de RobPixels. Para obtenerla, contacta: gerencia@robpixels.com o soporte@robpixels.com

🚀 Cliente API Básico

Cliente HTTP Reutilizable

javascript
class TSALVAClient {
  constructor(username, password, baseUrl = '[URL_API]') {
    this.baseUrl = baseUrl;
    this.auth = btoa(`${username}:${password}`);
    this.defaultHeaders = {
      'Authorization': `Basic ${this.auth}`,
      'Content-Type': 'application/json'
    };
  }

  async request(method, endpoint, data = null) {
    const config = {
      method,
      headers: this.defaultHeaders
    };

    if (data) {
      config.body = JSON.stringify(data);
    }

    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`, config);
      const result = await response.json();

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${result.msg || 'Error desconocido'}`);
      }

      if (result.status === false) {
        throw new Error(result.msg);
      }

      return result;
    } catch (error) {
      console.error(`Error en ${method} ${endpoint}:`, error.message);
      throw error;
    }
  }

  // Métodos específicos de la API
  async obtenerTipos() {
    const response = await fetch(`${this.baseUrl}/api/v2/types`, {
      headers: { 'Authorization': `Basic ${this.auth}` }
    });
    return response.json();
  }

  async crearOferta(datos) {
    return this.request('POST', '/api/v2/oferta', datos);
  }

  async asignarDetalles(datos) {
    return this.request('POST', '/api/v2/asignacion', datos);
  }

  async cancelarServicio(idServicio, razon, codigoSolicitud = null) {
    const data = { idServicio, razon };
    if (codigoSolicitud) data.codigoSolicitud = codigoSolicitud;
    return this.request('POST', '/api/v2/cancelacion', data);
  }

  async asignacionDirecta(datos) {
    return this.request('POST', '/api/v2/asignacion-directa', datos);
  }

  async agregarDireccion(uuid, direccion) {
    return this.request('POST', '/api/v2/add-address', { uuid, ...direccion });
  }

  async consultarHistorial(criterios) {
    const result = await this.request('POST', '/api/v2/queries/history', criterios);
    return result.data;
  }

  async obtenerOpcionesHistorial(filtro = null) {
    const url = filtro 
      ? `/api/v2/queries/history-options?q=${filtro}` 
      : '/api/v2/queries/history-options';
    const result = await this.request('GET', url);
    return result.data;
  }
}

// Uso del cliente
const cliente = new TSALVAClient(
  process.env.TSALVA_USERNAME,
  process.env.TSALVA_PASSWORD
);

📱 Ejemplos de Uso Completo

Flujo Completo: Oferta → Asignación

javascript
class ServicioTSALVA {
  constructor(cliente) {
    this.cliente = cliente;
    this.serviciosActivos = new Map();
  }

  async crearServicioCompleto(datosServicio) {
    try {
      // 1. Validar tipo de servicio
      const tipos = await this.cliente.obtenerTipos();
      const tipoSeleccionado = tipos.find(t => t.id === parseInt(datosServicio.tipoServicio));
      
      if (!tipoSeleccionado) {
        throw new Error('Tipo de servicio no válido');
      }

      // 2. Validar si requiere destino
      if (tipoSeleccionado.required_destination && 
          (!datosServicio.latitudDestino || !datosServicio.longitudDestino)) {
        throw new Error(`El tipo "${tipoSeleccionado.type}" requiere coordenadas de destino`);
      }

      // 3. Crear oferta
      console.log('Creando oferta...');
      const resultadoOferta = await this.cliente.crearOferta(datosServicio);
      console.log('✅ Oferta creada:', resultadoOferta.msg);

      // 4. Guardar en seguimiento
      this.serviciosActivos.set(datosServicio.uuid, {
        estado: 'OFERTADO',
        datos: datosServicio,
        fechaCreacion: new Date()
      });

      return {
        uuid: datosServicio.uuid,
        estado: 'OFERTADO',
        mensaje: resultadoOferta.msg
      };

    } catch (error) {
      console.error('❌ Error creando servicio:', error.message);
      throw error;
    }
  }

  async procesarAceptacion(callbackData) {
    const { id: uuid, dniPrestador, nombrePrestador, dniTecnico, telefonoTecnico } = callbackData;
    
    console.log(`🎯 Central ${nombrePrestador} aceptó oferta ${uuid}`);
    
    // Actualizar estado local
    if (this.serviciosActivos.has(uuid)) {
      const servicio = this.serviciosActivos.get(uuid);
      servicio.estado = 'ACEPTADO';
      servicio.prestador = { dni: dniPrestador, nombre: nombrePrestador };
      servicio.tecnico = { dni: dniTecnico, telefono: telefonoTecnico };
    }

    return {
      uuid,
      estado: 'ACEPTADO',
      prestador: nombrePrestador,
      tecnico: telefonoTecnico
    };
  }

  async asignarDatosCliente(uuid, datosCliente) {
    try {
      console.log(`Asignando datos del cliente para ${uuid}...`);
      
      const resultado = await this.cliente.asignarDetalles({
        uuid,
        ...datosCliente
      });

      // Actualizar estado
      if (this.serviciosActivos.has(uuid)) {
        const servicio = this.serviciosActivos.get(uuid);
        servicio.estado = 'ASIGNADO';
        servicio.cliente = datosCliente;
      }

      console.log('✅ Datos asignados:', resultado.msg);
      return resultado;

    } catch (error) {
      console.error('❌ Error asignando datos:', error.message);
      throw error;
    }
  }

  obtenerServicio(uuid) {
    return this.serviciosActivos.get(uuid);
  }

  listarServiciosActivos() {
    return Array.from(this.serviciosActivos.entries()).map(([uuid, data]) => ({
      uuid,
      ...data
    }));
  }
}

// Ejemplo de uso
const servicio = new ServicioTSALVA(cliente);

// Crear servicio completo
const nuevoServicio = await servicio.crearServicioCompleto({
  uuid: generateUUID(),
  ciudad: 'Medellín',
  departamento: 'Antioquia',
  latitudOrigen: 6.2442,
  longitudOrigen: -75.5812,
  direccionOrigen: 'Carrera 25A #1A Sur-45',
  tipoServicio: '1',
  descripcionServicio: 'Conductor para vehículo del cliente',
  callbackUrl: 'https://tu-servidor.com/webhook/aceptacion'
});

Webhook Handler para Express.js

javascript
const express = require('express');
const app = express();

app.use(express.json());

// Middleware de validación
const validarWebhook = (req, res, next) => {
  const apiKey = req.headers['x-apikey'];
  
  if (!apiKey) {
    return res.status(401).json({ error: 'API Key requerida' });
  }
  
  if (apiKey !== process.env.TSALVA_WEBHOOK_API_KEY) {
    return res.status(401).json({ error: 'API Key inválida' });
  }
  
  next();
};

// Webhook de aceptación de ofertas
app.put('/webhook/aceptacion', validarWebhook, async (req, res) => {
  try {
    const callbackData = req.body;
    console.log('📞 Callback de aceptación recibido:', callbackData.id);
    
    // Procesar aceptación
    const resultado = await servicio.procesarAceptacion(callbackData);
    
    // Enviar notificación al cliente (ejemplo)
    await enviarNotificacionCliente(resultado.uuid, {
      titulo: '🎯 Servicio Aceptado',
      mensaje: `La central ${resultado.prestador} aceptó tu solicitud`,
      tecnico: resultado.tecnico
    });
    
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Error procesando callback:', error);
    res.status(500).json({ error: 'Error interno' });
  }
});

// Webhook de cambios de estado
app.put('/webhook/cambio-estado', validarWebhook, async (req, res) => {
  try {
    const { idServicio, codigoEstado, detalleEstado } = req.body;
    
    console.log(`🔄 Cambio de estado: ${idServicio} → ${codigoEstado}`);
    
    // Procesar según el estado
    switch (codigoEstado) {
      case 'EN_CAMINO':
        await procesarTecnicoEnCamino(idServicio, detalleEstado);
        break;
      
      case 'EN_SITIO':
        await procesarTecnicoEnSitio(idServicio, detalleEstado);
        break;
      
      case 'COMPLETADO':
        await procesarServicioCompletado(idServicio, detalleEstado);
        break;
      
      case 'INCOMPLETO':
        await procesarServicioIncompleto(idServicio, detalleEstado);
        break;
      
      default:
        console.log(`Estado no manejado: ${codigoEstado}`);
    }
    
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Error procesando webhook:', error);
    res.status(500).json({ error: 'Error interno' });
  }
});

// Funciones de procesamiento
async function procesarTecnicoEnCamino(idServicio, detalle) {
  console.log(`🚗 Técnico en camino para ${idServicio}`);
  console.log(`📍 ETA: ${detalle.eta} minutos`);
  
  // Actualizar base de datos
  await actualizarEstadoServicio(idServicio, 'EN_CAMINO', {
    eta: detalle.eta,
    ubicacionTecnico: {
      lat: detalle.latitud,
      lng: detalle.longitud
    }
  });
  
  // Notificar cliente
  await enviarNotificacionCliente(idServicio, {
    titulo: '🚗 Técnico en camino',
    mensaje: `Tu técnico llegará en aproximadamente ${detalle.eta} minutos`,
    ubicacion: { lat: detalle.latitud, lng: detalle.longitud }
  });
}

async function procesarServicioCompletado(idServicio, detalle) {
  console.log(`✅ Servicio ${idServicio} completado`);
  
  // Actualizar base de datos
  await actualizarEstadoServicio(idServicio, 'COMPLETADO', {
    tarifa: detalle.tarifaServicio,
    distancia: detalle.distancia,
    tiempo: detalle.tiempo,
    fechaCompletado: detalle.fechaCambioEstado
  });
  
  // Procesar facturación
  await procesarFacturacion(idServicio, {
    tarifa: parseFloat(detalle.tarifaServicio),
    banderazo: parseFloat(detalle.banderazo),
    recargos: parseFloat(detalle.recargosAdicioneales || 0)
  });
  
  // Enviar encuesta de satisfacción
  await enviarEncuestaSatisfaccion(idServicio);
}

🎯 Casos de Uso Específicos

Sistema de Reservas

javascript
class SistemaReservas {
  constructor(cliente) {
    this.cliente = cliente;
    this.reservas = new Map();
  }

  async programarServicio(fechaHora, datosServicio) {
    const uuid = generateUUID();
    const reserva = {
      uuid,
      fechaProgramada: fechaHora,
      datos: datosServicio,
      estado: 'PROGRAMADO'
    };

    this.reservas.set(uuid, reserva);
    
    // Programar ejecución
    const tiempoEspera = new Date(fechaHora) - new Date();
    if (tiempoEspera > 0) {
      setTimeout(() => this.ejecutarReserva(uuid), tiempoEspera);
    }

    return reserva;
  }

  async ejecutarReserva(uuid) {
    const reserva = this.reservas.get(uuid);
    if (!reserva || reserva.estado !== 'PROGRAMADO') return;

    try {
      console.log(`⏰ Ejecutando reserva programada: ${uuid}`);
      
      // Usar asignación directa para servicios programados
      const resultado = await this.cliente.asignacionDirecta({
        uuid,
        ...reserva.datos,
        fechaCita: reserva.fechaProgramada.toISOString().split('T')[0],
        horaCita: reserva.fechaProgramada.toTimeString().substr(0, 5)
      });

      reserva.estado = 'EJECUTADO';
      reserva.codigoServicio = resultado.codigoServicio;
      
      console.log('✅ Reserva ejecutada:', resultado.msg);
      
    } catch (error) {
      console.error('❌ Error ejecutando reserva:', error.message);
      reserva.estado = 'ERROR';
      reserva.error = error.message;
    }
  }
}

Dashboard de Monitoreo

javascript
class DashboardMonitoreo {
  constructor(cliente) {
    this.cliente = cliente;
    this.metricas = {
      serviciosHoy: 0,
      completados: 0,
      ingresos: 0,
      prestadores: new Set()
    };
  }

  async generarReporteDiario() {
    const hoy = new Date().toISOString().split('T')[0];
    
    try {
      const servicios = await this.cliente.consultarHistorial({
        line: '1',
        startDate: `${hoy} 00:00`,
        endDate: `${hoy} 23:59`
      });

      this.metricas = servicios.reduce((acc, servicio) => {
        acc.serviciosHoy++;
        
        if (servicio.estado === 'Completado') {
          acc.completados++;
          if (servicio.facturacion) {
            acc.ingresos += parseFloat(servicio.facturacion.tarifa || 0);
          }
        }
        
        if (servicio.prestador) {
          acc.prestadores.add(servicio.prestador.nombre);
        }
        
        return acc;
      }, {
        serviciosHoy: 0,
        completados: 0,
        ingresos: 0,
        prestadores: new Set()
      });

      return {
        fecha: hoy,
        totalServicios: this.metricas.serviciosHoy,
        completados: this.metricas.completados,
        tasaExito: (this.metricas.completados / this.metricas.serviciosHoy * 100).toFixed(1),
        ingresosTotales: this.metricas.ingresos,
        prestadoresActivos: this.metricas.prestadores.size
      };

    } catch (error) {
      console.error('Error generando reporte:', error);
      throw error;
    }
  }

  async monitorearTiempoReal() {
    // Simular monitoreo en tiempo real
    setInterval(async () => {
      try {
        const reporte = await this.generarReporteDiario();
        console.log('📊 Métricas actualizadas:', reporte);
        
        // Enviar a frontend via WebSocket
        if (global.io) {
          global.io.emit('metricas_update', reporte);
        }
        
      } catch (error) {
        console.error('Error en monitoreo:', error);
      }
    }, 60000); // Cada minuto
  }
}

🔧 Utilidades y Helpers

Generador de UUID

javascript
function generateUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    const r = Math.random() * 16 | 0;
    const v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

Validador de Datos

javascript
class ValidadorTSALVA {
  static validarCoordenadas(lat, lng) {
    const latNum = parseFloat(lat);
    const lngNum = parseFloat(lng);
    
    if (isNaN(latNum) || isNaN(lngNum)) {
      throw new Error('Coordenadas deben ser números válidos');
    }
    
    if (latNum < -90 || latNum > 90) {
      throw new Error('Latitud debe estar entre -90 y 90');
    }
    
    if (lngNum < -180 || lngNum > 180) {
      throw new Error('Longitud debe estar entre -180 y 180');
    }
    
    return { lat: latNum, lng: lngNum };
  }

  static validarUUID(uuid) {
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    if (!uuidRegex.test(uuid)) {
      throw new Error('UUID no tiene formato válido');
    }
    return uuid;
  }

  static validarTelefono(telefono) {
    // Validar formato colombiano
    const telefonoRegex = /^\+57\s[3][0-9]{9}$/;
    if (!telefonoRegex.test(telefono)) {
      throw new Error('Teléfono debe tener formato +57 3XXXXXXXXX');
    }
    return telefono;
  }
}

Rate Limiter

javascript
class RateLimiter {
  constructor(maxRequests = 100, windowMs = 60000) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = [];
  }

  async checkLimit() {
    const now = Date.now();
    
    // Limpiar peticiones fuera de la ventana
    this.requests = this.requests.filter(time => now - time < this.windowMs);
    
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = Math.min(...this.requests);
      const waitTime = this.windowMs - (now - oldestRequest);
      
      console.log(`⏱️ Rate limit alcanzado, esperando ${waitTime}ms`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      
      return this.checkLimit();
    }
    
    this.requests.push(now);
    return true;
  }
}

// Uso con el cliente
const rateLimiter = new RateLimiter();

class TSALVAClientWithRateLimit extends TSALVAClient {
  async request(method, endpoint, data = null) {
    await rateLimiter.checkLimit();
    return super.request(method, endpoint, data);
  }
}

🚀 Aplicación de Ejemplo Completa

javascript
// app.js - Aplicación completa de ejemplo
const express = require('express');
const winston = require('winston');

// Configurar logging
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'tsalva-app.log' })
  ]
});

const app = express();
app.use(express.json());

// Inicializar cliente TSALVA
const cliente = new TSALVAClient(
  process.env.TSALVA_USERNAME,
  process.env.TSALVA_PASSWORD
);

const servicio = new ServicioTSALVA(cliente);
const dashboard = new DashboardMonitoreo(cliente);

// Rutas de la aplicación
app.post('/api/servicios', async (req, res) => {
  try {
    const resultado = await servicio.crearServicioCompleto(req.body);
    logger.info('Servicio creado', { uuid: resultado.uuid });
    res.json(resultado);
  } catch (error) {
    logger.error('Error creando servicio', { error: error.message });
    res.status(400).json({ error: error.message });
  }
});

app.get('/api/dashboard', async (req, res) => {
  try {
    const reporte = await dashboard.generarReporteDiario();
    res.json(reporte);
  } catch (error) {
    logger.error('Error generando dashboard', { error: error.message });
    res.status(500).json({ error: error.message });
  }
});

// Iniciar servidor
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Servidor iniciado en puerto ${PORT}`);
  dashboard.monitorearTiempoReal();
});

module.exports = app;

Recomendación

Estos ejemplos están listos para producción. Recuerda configurar las variables de entorno apropiadas y manejar los errores según las necesidades de tu aplicación.

Importante

Siempre valida y sanitiza los datos de entrada, especialmente en los webhooks que son endpoints públicos.

Tsalva API - Documentación desarrollada por RobPixels