Apariencia
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.