Apariencia
Ejemplos en PHP
Esta sección proporciona ejemplos completos de integración con la API de TSALVA usando PHP. Los ejemplos incluyen manejo de autenticación, operaciones principales, webhooks y mejores prácticas.
📍 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
Instalación de Dependencias
bash
composer require guzzlehttp/guzzle
composer require monolog/monologCliente API Base
php
<?php
require_once 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use Psr\Http\Message\ResponseInterface;
class TSalvaAPI
{
private $client;
private $username;
private $password;
private $baseUrl;
public function __construct(string $username, string $password, string $baseUrl = '[URL_API]')
{
$this->username = $username;
$this->password = $password;
$this->baseUrl = $baseUrl;
$this->client = new Client([
'base_uri' => $baseUrl,
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Basic ' . base64_encode($username . ':' . $password),
'User-Agent' => 'TSalva-PHP-Client/1.0'
]
]);
}
/**
* Realizar petición HTTP con manejo de errores
*/
private function makeRequest(string $method, string $endpoint, array $data = null): array
{
try {
$options = [];
if ($data !== null) {
$options['json'] = $data;
}
$response = $this->client->request($method, $endpoint, $options);
return $this->parseResponse($response);
} catch (ClientException $e) {
// Error 4xx
$response = $e->getResponse();
$errorData = $this->parseResponse($response);
throw new TSalvaAPIException(
'Error del cliente: ' . ($errorData['message'] ?? $e->getMessage()),
$response->getStatusCode(),
$errorData
);
} catch (ServerException $e) {
// Error 5xx
$response = $e->getResponse();
throw new TSalvaAPIException(
'Error del servidor: ' . $e->getMessage(),
$response->getStatusCode()
);
} catch (RequestException $e) {
// Error de conexión
throw new TSalvaAPIException(
'Error de conexión: ' . $e->getMessage(),
0
);
}
}
/**
* Parsear respuesta JSON
*/
private function parseResponse(ResponseInterface $response): array
{
$body = $response->getBody()->getContents();
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new TSalvaAPIException('Error parseando respuesta JSON: ' . json_last_error_msg());
}
return $data;
}
/**
* Crear una nueva oferta de servicio
*/
public function crearOferta(array $origen, array $destino, array $opciones = []): array
{
$data = array_merge([
'origen' => $origen,
'destino' => $destino
], $opciones);
return $this->makeRequest('POST', '/api/v2/oferta', $data);
}
/**
* Asignar un conductor específico a una oferta
*/
public function asignarConductor(string $ofertaId, string $conductorId): array
{
$data = [
'oferta_id' => $ofertaId,
'conductor_id' => $conductorId
];
return $this->makeRequest('POST', '/api/v2/asignacion', $data);
}
/**
* Cancelar un servicio
*/
public function cancelarServicio(string $ofertaId, string $motivo): array
{
$data = [
'oferta_id' => $ofertaId,
'motivo' => $motivo
];
return $this->makeRequest('POST', '/api/v2/cancelacion', $data);
}
/**
* Obtener tipos de vehículos y servicios disponibles
*/
public function obtenerTipos(): array
{
return $this->makeRequest('GET', '/api/v2/types');
}
/**
* Obtener historial de operaciones
*/
public function obtenerHistorial(array $filtros = []): array
{
return $this->makeRequest('POST', '/api/v2/queries/history', $filtros);
}
/**
* Agregar dirección a una oferta existente
*/
public function agregarDireccion(string $ofertaId, array $direccion): array
{
$data = [
'oferta_id' => $ofertaId,
'direccion' => $direccion
];
return $this->makeRequest('POST', '/api/v2/add-address', $data);
}
/**
* Asignación directa de conductor
*/
public function asignacionDirecta(array $datos): array
{
return $this->makeRequest('POST', '/api/v2/asignacion-directa', $datos);
}
/**
* Marcar como no asignado
*/
public function noAsignacion(string $ofertaId, string $motivo): array
{
$data = [
'oferta_id' => $ofertaId,
'motivo' => $motivo
];
return $this->makeRequest('POST', '/api/v2/no-asignacion', $data);
}
}
/**
* Excepción personalizada para errores de la API
*/
class TSalvaAPIException extends Exception
{
private $statusCode;
private $errorData;
public function __construct(string $message, int $statusCode = 0, array $errorData = [], Exception $previous = null)
{
parent::__construct($message, $statusCode, $previous);
$this->statusCode = $statusCode;
$this->errorData = $errorData;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function getErrorData(): array
{
return $this->errorData;
}
}Clase de Integración Completa
php
<?php
use Monolog\Logger;
use Monolog\Handler\FileHandler;
use Monolog\Handler\StreamHandler;
class TSalvaIntegration
{
private $api;
private $logger;
private $tiposVehiculos;
private $config;
public function __construct(string $username, string $password, array $config = [])
{
$this->api = new TSalvaAPI($username, $password);
$this->config = array_merge([
'log_file' => 'tsalva.log',
'log_level' => Logger::INFO,
'cache_ttl' => 3600,
'webhook_secret' => null
], $config);
$this->setupLogger();
}
/**
* Configurar logger
*/
private function setupLogger(): void
{
$this->logger = new Logger('TSalva');
$this->logger->pushHandler(new FileHandler($this->config['log_file'], $this->config['log_level']));
$this->logger->pushHandler(new StreamHandler('php://stdout', Logger::WARNING));
}
/**
* Inicializar la integración
*/
public function inicializar(): bool
{
try {
$tiposResponse = $this->api->obtenerTipos();
if ($tiposResponse['status'] === 'success') {
$this->tiposVehiculos = $tiposResponse['data']['vehicle_types'] ?? [];
$this->logger->info('Inicialización exitosa', [
'tipos_vehiculos' => count($this->tiposVehiculos)
]);
return true;
} else {
throw new TSalvaAPIException('Error obteniendo tipos: ' . $tiposResponse['message']);
}
} catch (TSalvaAPIException $e) {
$this->logger->error('Error en inicialización', [
'error' => $e->getMessage(),
'status_code' => $e->getStatusCode()
]);
return false;
}
}
/**
* Solicitar un nuevo servicio
*/
public function solicitarServicio(array $origen, array $destino, array $opciones = []): string
{
try {
// Validar datos de entrada
$this->validarDireccion($origen, 'origen');
$this->validarDireccion($destino, 'destino');
// Configurar opciones por defecto
$opcionesDefecto = [
'vehicle_type' => 1,
'priority' => 'normal',
'callback_url' => $this->config['webhook_url'] ?? null,
'notes' => 'Servicio solicitado el ' . date('Y-m-d H:i:s')
];
$opcionesFinal = array_merge($opcionesDefecto, $opciones);
// Validar tipo de vehículo
if (!$this->validarTipoVehiculo($opcionesFinal['vehicle_type'])) {
throw new InvalidArgumentException('Tipo de vehículo inválido: ' . $opcionesFinal['vehicle_type']);
}
// Crear oferta
$response = $this->api->crearOferta($origen, $destino, $opcionesFinal);
if ($response['status'] === 'success') {
$ofertaId = $response['data']['oferta_id'];
$this->logger->info('Servicio creado exitosamente', [
'oferta_id' => $ofertaId,
'vehicle_type' => $opcionesFinal['vehicle_type'],
'priority' => $opcionesFinal['priority']
]);
return $ofertaId;
} else {
throw new TSalvaAPIException('Error al crear servicio: ' . $response['message']);
}
} catch (Exception $e) {
$this->logger->error('Error al solicitar servicio', [
'error' => $e->getMessage(),
'origen' => $origen,
'destino' => $destino
]);
throw $e;
}
}
/**
* Validar dirección
*/
private function validarDireccion(array $direccion, string $tipo): void
{
$camposRequeridos = ['lat', 'lng', 'address'];
foreach ($camposRequeridos as $campo) {
if (!isset($direccion[$campo])) {
throw new InvalidArgumentException("Campo requerido faltante en {$tipo}: {$campo}");
}
}
// Validar coordenadas
$lat = (float) $direccion['lat'];
$lng = (float) $direccion['lng'];
if ($lat < -90 || $lat > 90) {
throw new InvalidArgumentException("Latitud inválida en {$tipo}: {$lat}");
}
if ($lng < -180 || $lng > 180) {
throw new InvalidArgumentException("Longitud inválida en {$tipo}: {$lng}");
}
// Validar dirección
if (strlen(trim($direccion['address'])) < 10) {
throw new InvalidArgumentException("Dirección muy corta en {$tipo}");
}
}
/**
* Validar tipo de vehículo
*/
private function validarTipoVehiculo(int $tipoId): bool
{
if (empty($this->tiposVehiculos)) {
return false;
}
foreach ($this->tiposVehiculos as $tipo) {
if ($tipo['id'] === $tipoId) {
return true;
}
}
return false;
}
/**
* Consultar historial reciente
*/
public function consultarHistorialReciente(int $dias = 7): array
{
try {
$fechaInicio = date('c', strtotime("-{$dias} days"));
$fechaFin = date('c');
$filtros = [
'fecha_inicio' => $fechaInicio,
'fecha_fin' => $fechaFin,
'limit' => 50
];
$response = $this->api->obtenerHistorial($filtros);
if ($response['status'] === 'success') {
$operaciones = $response['data']['operations'];
$this->logger->info('Historial obtenido', [
'operaciones' => count($operaciones),
'dias' => $dias
]);
return $operaciones;
} else {
throw new TSalvaAPIException('Error al obtener historial: ' . $response['message']);
}
} catch (Exception $e) {
$this->logger->error('Error al consultar historial', [
'error' => $e->getMessage(),
'dias' => $dias
]);
throw $e;
}
}
/**
* Generar reporte diario
*/
public function generarReporteDiario(): array
{
try {
$operaciones = $this->consultarHistorialReciente(1);
$reporte = [
'fecha' => date('Y-m-d'),
'total_operaciones' => count($operaciones),
'por_estado' => [],
'por_tipo_vehiculo' => [],
'tiempo_promedio_asignacion' => 0
];
// Agrupar por estado
foreach ($operaciones as $op) {
$estado = $op['state'] ?? 'unknown';
$reporte['por_estado'][$estado] = ($reporte['por_estado'][$estado] ?? 0) + 1;
}
// Agrupar por tipo de vehículo
foreach ($operaciones as $op) {
$tipo = $op['vehicle_type'] ?? 'unknown';
$reporte['por_tipo_vehiculo'][$tipo] = ($reporte['por_tipo_vehiculo'][$tipo] ?? 0) + 1;
}
// Calcular tiempo promedio de asignación
$tiemposAsignacion = [];
foreach ($operaciones as $op) {
if (isset($op['assigned_at']) && isset($op['created_at'])) {
$created = new DateTime($op['created_at']);
$assigned = new DateTime($op['assigned_at']);
$tiempo = $assigned->getTimestamp() - $created->getTimestamp();
$tiemposAsignacion[] = $tiempo;
}
}
if (!empty($tiemposAsignacion)) {
$reporte['tiempo_promedio_asignacion'] = array_sum($tiemposAsignacion) / count($tiemposAsignacion);
}
return $reporte;
} catch (Exception $e) {
$this->logger->error('Error al generar reporte', [
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* Procesar webhook de TSALVA
*/
public function procesarWebhook(array $data): array
{
try {
// Validar datos del webhook
if (empty($data['event']) || empty($data['oferta_id'])) {
throw new InvalidArgumentException('Datos de webhook inválidos');
}
$eventType = $data['event'];
$ofertaId = $data['oferta_id'];
$this->logger->info('Webhook recibido', [
'event' => $eventType,
'oferta_id' => $ofertaId
]);
// Procesar según el tipo de evento
switch ($eventType) {
case 'state_change':
return $this->procesarCambioEstado($data);
case 'assignment':
return $this->procesarAsignacion($data);
case 'completion':
return $this->procesarFinalizacion($data);
default:
$this->logger->warning('Tipo de evento desconocido', ['event' => $eventType]);
return ['status' => 'ignored', 'message' => 'Evento no manejado'];
}
} catch (Exception $e) {
$this->logger->error('Error procesando webhook', [
'error' => $e->getMessage(),
'data' => $data
]);
return [
'status' => 'error',
'message' => $e->getMessage()
];
}
}
/**
* Procesar cambio de estado
*/
private function procesarCambioEstado(array $data): array
{
$ofertaId = $data['oferta_id'];
$estadoAnterior = $data['previous_state'] ?? 'unknown';
$estadoNuevo = $data['new_state'] ?? 'unknown';
$timestamp = $data['timestamp'] ?? date('c');
$this->logger->info('Cambio de estado procesado', [
'oferta_id' => $ofertaId,
'estado_anterior' => $estadoAnterior,
'estado_nuevo' => $estadoNuevo,
'timestamp' => $timestamp
]);
// Lógica específica según el estado
switch ($estadoNuevo) {
case 'assigned':
$conductorInfo = $data['conductor'] ?? [];
$this->notificarClienteAsignacion($ofertaId, $conductorInfo);
break;
case 'completed':
$this->procesarFacturacion($data);
break;
case 'cancelled':
$motivo = $data['cancellation_reason'] ?? 'No especificado';
$this->notificarClienteCancelacion($ofertaId, $motivo);
break;
}
return ['status' => 'processed', 'message' => 'Cambio de estado procesado'];
}
/**
* Procesar asignación
*/
private function procesarAsignacion(array $data): array
{
$ofertaId = $data['oferta_id'];
$conductor = $data['conductor'] ?? [];
$vehiculo = $data['vehiculo'] ?? [];
$eta = $data['eta_minutes'] ?? null;
$this->logger->info('Asignación procesada', [
'oferta_id' => $ofertaId,
'conductor_id' => $conductor['id'] ?? null,
'conductor_nombre' => $conductor['nombre'] ?? null,
'vehiculo_placa' => $vehiculo['placa'] ?? null,
'eta_minutes' => $eta
]);
// Actualizar estado local
$this->actualizarEstadoLocal($ofertaId, [
'estado' => 'assigned',
'conductor_id' => $conductor['id'] ?? null,
'conductor_nombre' => $conductor['nombre'] ?? null,
'conductor_telefono' => $conductor['telefono'] ?? null,
'vehiculo_placa' => $vehiculo['placa'] ?? null,
'eta_minutes' => $eta,
'assigned_at' => date('c')
]);
return ['status' => 'processed', 'message' => 'Asignación procesada'];
}
/**
* Procesar finalización
*/
private function procesarFinalizacion(array $data): array
{
$ofertaId = $data['oferta_id'];
$costoTotal = $data['total_cost'] ?? 0;
$distanciaKm = $data['distance_km'] ?? 0;
$duracionMinutos = $data['duration_minutes'] ?? 0;
$this->logger->info('Finalización procesada', [
'oferta_id' => $ofertaId,
'costo_total' => $costoTotal,
'distancia_km' => $distanciaKm,
'duracion_minutos' => $duracionMinutos
]);
// Actualizar estado local y generar factura
$this->actualizarEstadoLocal($ofertaId, [
'estado' => 'completed',
'costo_total' => $costoTotal,
'distancia_km' => $distanciaKm,
'duracion_minutos' => $duracionMinutos,
'completed_at' => date('c')
]);
return ['status' => 'processed', 'message' => 'Finalización procesada'];
}
// Métodos auxiliares (implementar según lógica de negocio)
private function notificarClienteAsignacion(string $ofertaId, array $conductorInfo): void
{
// Implementar notificación al cliente
$this->logger->info('Notificación de asignación enviada', [
'oferta_id' => $ofertaId,
'conductor' => $conductorInfo['nombre'] ?? 'N/A'
]);
}
private function notificarClienteCancelacion(string $ofertaId, string $motivo): void
{
// Implementar notificación de cancelación
$this->logger->info('Notificación de cancelación enviada', [
'oferta_id' => $ofertaId,
'motivo' => $motivo
]);
}
private function procesarFacturacion(array $data): void
{
// Implementar lógica de facturación
$this->logger->info('Facturación procesada', [
'oferta_id' => $data['oferta_id'],
'costo' => $data['total_cost'] ?? 0
]);
}
private function actualizarEstadoLocal(string $ofertaId, array $datos): void
{
// Implementar actualización de base de datos local
$this->logger->info('Estado local actualizado', [
'oferta_id' => $ofertaId,
'datos' => $datos
]);
}
}Ejemplo de Uso Completo
php
<?php
require_once 'TSalvaIntegration.php';
// Configuración
$config = [
'log_file' => '/var/log/tsalva.log',
'log_level' => Logger::INFO,
'webhook_url' => 'https://tu-dominio.com/webhook/tsalva'
];
// Inicializar integración
$integration = new TSalvaIntegration('tu_usuario', 'tu_password', $config);
if (!$integration->inicializar()) {
die('Error inicializando la integración');
}
try {
// Definir direcciones
$origen = [
'lat' => -34.6037,
'lng' => -58.3816,
'address' => 'Av. Corrientes 1000, Buenos Aires'
];
$destino = [
'lat' => -34.6158,
'lng' => -58.3731,
'address' => 'Av. Santa Fe 2000, Buenos Aires'
];
// Solicitar servicio
$ofertaId = $integration->solicitarServicio(
$origen,
$destino,
[
'vehicle_type' => 1, // Automóvil
'priority' => 'normal'
]
);
echo "Servicio solicitado exitosamente: {$ofertaId}\n";
// Generar reporte diario
$reporte = $integration->generarReporteDiario();
echo "Reporte diario:\n";
echo json_encode($reporte, JSON_PRETTY_PRINT) . "\n";
} catch (TSalvaAPIException $e) {
echo "Error de API: " . $e->getMessage() . "\n";
echo "Código de estado: " . $e->getStatusCode() . "\n";
} catch (Exception $e) {
echo "Error general: " . $e->getMessage() . "\n";
}Webhook Handler
php
<?php
require_once 'TSalvaIntegration.php';
// webhook.php - Handler para webhooks de TSALVA
// Configurar headers
header('Content-Type: application/json');
// Verificar método HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Método no permitido']);
exit;
}
try {
// Obtener datos del webhook
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InvalidArgumentException('JSON inválido: ' . json_last_error_msg());
}
if (empty($data)) {
throw new InvalidArgumentException('No se recibieron datos');
}
// Inicializar integración
$integration = new TSalvaIntegration('tu_usuario', 'tu_password');
$integration->inicializar();
// Procesar webhook
$resultado = $integration->procesarWebhook($data);
// Responder exitosamente
http_response_code(200);
echo json_encode($resultado);
} catch (Exception $e) {
// Log del error
error_log("Error procesando webhook TSALVA: " . $e->getMessage());
// Responder con error
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Error interno del servidor'
]);
}Middleware de Rate Limiting
php
<?php
class RateLimiter
{
private $redis;
private $maxRequests;
private $timeWindow;
public function __construct($redisHost = '127.0.0.1', $redisPort = 6379, $maxRequests = 100, $timeWindow = 3600)
{
$this->redis = new Redis();
$this->redis->connect($redisHost, $redisPort);
$this->maxRequests = $maxRequests;
$this->timeWindow = $timeWindow;
}
public function checkLimit(string $clientId): bool
{
$key = "rate_limit:{$clientId}";
$current = $this->redis->get($key);
if ($current === false) {
// Primera petición
$this->redis->setex($key, $this->timeWindow, 1);
return true;
} else {
// Verificar límite
if ((int)$current < $this->maxRequests) {
$this->redis->incr($key);
return true;
} else {
return false;
}
}
}
public function getRemainingRequests(string $clientId): int
{
$key = "rate_limit:{$clientId}";
$current = $this->redis->get($key);
if ($current === false) {
return $this->maxRequests;
} else {
return max(0, $this->maxRequests - (int)$current);
}
}
}
// Uso del rate limiter
$rateLimiter = new RateLimiter();
$clientId = $_SERVER['REMOTE_ADDR']; // O usar API key
if (!$rateLimiter->checkLimit($clientId)) {
http_response_code(429);
echo json_encode([
'error' => 'Límite de peticiones excedido',
'remaining' => $rateLimiter->getRemainingRequests($clientId)
]);
exit;
}Sistema de Cache
php
<?php
class TSalvaCache
{
private $redis;
private $prefix;
private $defaultTtl;
public function __construct($redisHost = '127.0.0.1', $redisPort = 6379, $prefix = 'tsalva:', $defaultTtl = 3600)
{
$this->redis = new Redis();
$this->redis->connect($redisHost, $redisPort);
$this->prefix = $prefix;
$this->defaultTtl = $defaultTtl;
}
public function get(string $key)
{
$value = $this->redis->get($this->prefix . $key);
return $value !== false ? json_decode($value, true) : null;
}
public function set(string $key, $value, int $ttl = null): bool
{
$ttl = $ttl ?? $this->defaultTtl;
return $this->redis->setex(
$this->prefix . $key,
$ttl,
json_encode($value)
);
}
public function delete(string $key): bool
{
return $this->redis->del($this->prefix . $key) > 0;
}
public function exists(string $key): bool
{
return $this->redis->exists($this->prefix . $key);
}
}
// Implementar cache en la clase TSalvaAPI
class TSalvaAPIWithCache extends TSalvaAPI
{
private $cache;
public function __construct(string $username, string $password, string $baseUrl = '[URL_API]')
{
parent::__construct($username, $password, $baseUrl);
$this->cache = new TSalvaCache();
}
public function obtenerTipos(): array
{
$cacheKey = 'types';
// Intentar obtener del cache
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
// Obtener de la API
$result = parent::obtenerTipos();
// Guardar en cache si es exitoso
if ($result['status'] === 'success') {
$this->cache->set($cacheKey, $result, 7200); // 2 horas
}
return $result;
}
}Testing con PHPUnit
php
<?php
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
class TSalvaAPITest extends TestCase
{
private $api;
private $mockHandler;
protected function setUp(): void
{
$this->mockHandler = new MockHandler();
$handlerStack = HandlerStack::create($this->mockHandler);
// Crear cliente mockeado
$client = new Client(['handler' => $handlerStack]);
// Crear API con cliente mockeado
$this->api = new TSalvaAPI('test_user', 'test_pass');
// Inyectar cliente mockeado usando reflexión
$reflection = new ReflectionClass($this->api);
$property = $reflection->getProperty('client');
$property->setAccessible(true);
$property->setValue($this->api, $client);
}
public function testCrearOfertaExitosa(): void
{
// Mock de respuesta exitosa
$this->mockHandler->append(
new Response(200, [], json_encode([
'status' => 'success',
'data' => ['oferta_id' => 'test-123']
]))
);
// Ejecutar
$origen = ['lat' => -34.6037, 'lng' => -58.3816, 'address' => 'Test'];
$destino = ['lat' => -34.6158, 'lng' => -58.3731, 'address' => 'Test'];
$result = $this->api->crearOferta($origen, $destino);
// Verificar
$this->assertEquals('success', $result['status']);
$this->assertEquals('test-123', $result['data']['oferta_id']);
}
public function testCrearOfertaError(): void
{
// Mock de error HTTP
$this->mockHandler->append(
new Response(400, [], json_encode([
'status' => 'error',
'message' => 'Error de validación'
]))
);
// Verificar excepción
$this->expectException(TSalvaAPIException::class);
$this->expectExceptionMessage('Error del cliente');
$this->api->crearOferta([], []);
}
public function testValidarDireccion(): void
{
$integration = new TSalvaIntegration('test', 'test');
// Dirección válida
$direccionValida = [
'lat' => -34.6037,
'lng' => -58.3816,
'address' => 'Av. Corrientes 1000, Buenos Aires'
];
// No debe lanzar excepción
$reflection = new ReflectionClass($integration);
$method = $reflection->getMethod('validarDireccion');
$method->setAccessible(true);
$this->assertNull($method->invoke($integration, $direccionValida, 'origen'));
// Dirección inválida
$direccionInvalida = ['lat' => 100, 'lng' => 200]; // Coordenadas inválidas
$this->expectException(InvalidArgumentException::class);
$method->invoke($integration, $direccionInvalida, 'origen');
}
}