<?php namespace App\Integrations\Celcoin; use Exception; /** * Cliente HTTP para integração com a API da Celcoin * Suporte a mTLS, retry automático e logging completo */ class CelcoinHttpClient { private string $baseUrl; private ?string $clientId; private ?string $clientSecret; private ?string $accessToken; private ?string $certificatePath; private ?string $certificatePassword; private int $timeout = 30; private int $maxRetries = 3; private int $companyId; private $logger; public function __construct(int $companyId, array $config = []) { $this->companyId = $companyId; $this->baseUrl = $config['base_url'] ?? 'https://sandbox.api.celcoin.com.br'; $this->clientId = $config['client_id'] ?? null; $this->clientSecret = $config['client_secret'] ?? null; $this->accessToken = $config['access_token'] ?? null; $this->certificatePath = $config['certificate_path'] ?? null; $this->certificatePassword = $config['certificate_password'] ?? null; $this->timeout = $config['timeout'] ?? 30; $this->maxRetries = $config['max_retries'] ?? 3; // Inicializar logger (usar o logger do sistema se disponível) $this->logger = $config['logger'] ?? null; } /** * Executa uma requisição HTTP para a API da Celcoin */ public function request(string $method, string $endpoint, array $data = [], array $headers = []): array { $url = $this->baseUrl . $endpoint; $attempts = 0; $lastException = null; while ($attempts < $this->maxRetries) { try { $attempts++; // Preparar headers padrão $defaultHeaders = [ 'Content-Type: application/json', 'Accept: application/json', 'User-Agent: SaaS-Barbearia/1.0' ]; // Adicionar token de acesso se disponível if ($this->accessToken) { $defaultHeaders[] = 'Authorization: Bearer ' . $this->accessToken; } // Mesclar headers customizados $headers = array_merge($defaultHeaders, $headers); // Inicializar cURL $ch = curl_init(); // Configurar URL e método curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Configurar dados da requisição if (!empty($data) && in_array(strtoupper($method), ['POST', 'PUT', 'PATCH'])) { $jsonData = json_encode($data); curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); } // Configurar certificado mTLS se disponível if ($this->certificatePath) { curl_setopt($ch, CURLOPT_SSLCERT, $this->certificatePath); if ($this->certificatePassword) { curl_setopt($ch, CURLOPT_SSLCERTPASSWD, $this->certificatePassword); } curl_setopt($ch, CURLOPT_SSLKEY, $this->certificatePath); curl_setopt($ch, CURLOPT_SSLKEYPASSWD, $this->certificatePassword); } // Configurações de segurança SSL curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // Executar requisição $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); $errorCode = curl_errno($ch); curl_close($ch); // Log da requisição (mascarando dados sensíveis) $this->logRequest($method, $endpoint, $data, $httpCode, $attempts); // Verificar erro de conexão if ($error) { throw new Exception("HTTP Error: {$error} (Code: {$errorCode})"); } // Decodificar resposta JSON $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception("Invalid JSON response: " . json_last_error_msg()); } // Verificar código HTTP if ($httpCode >= 400) { $errorMessage = $responseData['message'] ?? $responseData['error'] ?? 'Unknown error'; throw new Exception("API Error [{$httpCode}]: {$errorMessage}"); } return [ 'success' => true, 'data' => $responseData, 'http_code' => $httpCode, 'attempts' => $attempts ]; } catch (Exception $e) { $lastException = $e; // Log do erro $this->logError($method, $endpoint, $e->getMessage(), $attempts); // Verificar se deve tentar novamente if ($attempts >= $this->maxRetries || !$this->shouldRetry($e, $httpCode ?? 0)) { break; } // Aguardar antes de tentar novamente (exponential backoff) $waitTime = pow(2, $attempts - 1) * 1000000; // microseconds usleep(min($waitTime, 5000000)); // máximo 5 segundos } } // Se chegou aqui, todas as tentativas falharam throw new Exception("Request failed after {$this->maxRetries} attempts: " . $lastException->getMessage()); } /** * GET request */ public function get(string $endpoint, array $headers = []): array { return $this->request('GET', $endpoint, [], $headers); } /** * POST request */ public function post(string $endpoint, array $data = [], array $headers = []): array { return $this->request('POST', $endpoint, $data, $headers); } /** * PUT request */ public function put(string $endpoint, array $data = [], array $headers = []): array { return $this->request('PUT', $endpoint, $data, $headers); } /** * DELETE request */ public function delete(string $endpoint, array $headers = []): array { return $this->request('DELETE', $endpoint, [], $headers); } /** * Define o token de acesso */ public function setAccessToken(string $token): void { $this->accessToken = $token; } /** * Remove o token de acesso */ public function clearAccessToken(): void { $this->accessToken = null; } /** * Verifica se deve tentar novamente baseado no erro */ private function shouldRetry(Exception $e, int $httpCode): bool { // Tentar novamente para erros temporários $retryableCodes = [408, 429, 500, 502, 503, 504]; // Request Timeout, Too Many Requests, Server Errors // Tentar novamente para erros de rede específicos $retryableErrors = [ 'CURLE_COULDNT_CONNECT', 'CURLE_COULDNT_RESOLVE_HOST', 'CURLE_OPERATION_TIMEOUTED', 'CURLE_SSL_CONNECT_ERROR' ]; return in_array($httpCode, $retryableCodes) || in_array($e->getMessage(), $retryableErrors) || strpos($e->getMessage(), 'HTTP Error') !== false; } /** * Log da requisição (mascarando dados sensíveis) */ private function logRequest(string $method, string $endpoint, array $data, int $httpCode, int $attempts): void { if (!$this->logger) return; $safeData = $this->maskSensitiveData($data); $this->logger->info('Celcoin API Request', [ 'company_id' => $this->companyId, 'method' => $method, 'endpoint' => $endpoint, 'data' => $safeData, 'http_code' => $httpCode, 'attempts' => $attempts, 'timestamp' => date('Y-m-d H:i:s') ]); } /** * Log de erro */ private function logError(string $method, string $endpoint, string $error, int $attempts): void { if (!$this->logger) return; $this->logger->error('Celcoin API Error', [ 'company_id' => $this->companyId, 'method' => $method, 'endpoint' => $endpoint, 'error' => $error, 'attempts' => $attempts, 'timestamp' => date('Y-m-d H:i:s') ]); } /** * Mascara dados sensíveis para logging */ private function maskSensitiveData(array $data): array { $sensitiveFields = [ 'password', 'token', 'secret', 'key', 'card_number', 'cvv', 'pin', 'client_secret', 'access_token', 'refresh_token', 'webhook_secret' ]; $masked = $data; foreach ($masked as $key => $value) { if (is_array($value)) { $masked[$key] = $this->maskSensitiveData($value); } elseif (is_string($value) && $this->isSensitiveField($key)) { $masked[$key] = $this->maskValue($value); } } return $masked; } /** * Verifica se o campo é sensível */ private function isSensitiveField(string $field): bool { $sensitiveFields = [ 'password', 'token', 'secret', 'key', 'card_number', 'cvv', 'pin', 'client_secret', 'access_token', 'refresh_token', 'webhook_secret', 'document', 'cpf', 'cnpj', 'account_number', 'agency' ]; $field = strtolower($field); foreach ($sensitiveFields as $sensitive) { if (strpos($field, $sensitive) !== false) { return true; } } return false; } /** * Mascara o valor sensível */ private function maskValue(string $value): string { $length = strlen($value); if ($length <= 4) { return str_repeat('*', $length); } // Mostrar primeiros e últimos 2 caracteres return substr($value, 0, 2) . str_repeat('*', $length - 4) . substr($value, -2); } }