Skip to content

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/monolog

Cliente 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');
    }
}

Referencias

Recursos Adicionales

Tsalva API - Documentación desarrollada por RobPixels