diff --git a/classes/UspsV3Client.php b/classes/UspsV3Client.php index ca13a78..7b4ba53 100644 --- a/classes/UspsV3Client.php +++ b/classes/UspsV3Client.php @@ -1,5 +1,10 @@ token = $token; $this->isLive = $isLive; - // URLs from the OpenAPI spec - $this->baseUrl = $this->isLive - ? 'https://apis.usps.com/prices/v3' + // Base URLs per OpenAPI Spec + $this->baseUrl = $this->isLive + ? 'https://apis.usps.com/prices/v3' : 'https://apis-tem.usps.com/prices/v3'; } /** - * Call Domestic Prices v3 API - * Endpoint: /base-rates/search + * Get Domestic Rate (Rates v3) */ public function getDomesticRate($payload) { @@ -26,56 +30,65 @@ class UspsV3Client } /** - * Call International Prices v3 API - * Endpoint: /base-rates/search (Under International Base path) - * Note: The spec shows a different base URL for International + * Get International Rate (Rates v3) */ public function getInternationalRate($payload) { - // International uses a different base URL structure in the spec - $intlBaseUrl = $this->isLive - ? 'https://apis.usps.com/international-prices/v3' + // International endpoint uses a different base structure per spec + $intlBaseUrl = $this->isLive + ? 'https://apis.usps.com/international-prices/v3' : 'https://apis-tem.usps.com/international-prices/v3'; return $this->post('/base-rates/search', $payload, $intlBaseUrl); } + /** + * Internal POST logic using Symfony HTTP Client + */ private function post($endpoint, $payload, $overrideUrl = null) { $url = ($overrideUrl ? $overrideUrl : $this->baseUrl) . $endpoint; - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Bearer ' . $this->token, - 'Content-Type: application/json', - 'Accept: application/json' + $client = HttpClient::create([ + 'timeout' => 15, + 'verify_peer' => false, + 'verify_host' => false, ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + try { + $response = $client->request('POST', $url, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->token, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + ], + 'json' => $payload + ]); - if (curl_errno($ch)) { - $error = curl_error($ch); + // toArray(false) prevents exception on 4xx/5xx responses so we can parse the error body + $data = $response->toArray(false); + $statusCode = $response->getStatusCode(); - return ['error' => 'CURL Error: ' . $error]; - } + // Handle API Errors (400 Bad Request, 401 Unauthorized, etc) + if ($statusCode >= 400) { + $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown Error'; + + // Try to extract deeper error detail (e.g., from 'errors' array) + if (isset($data['error']['errors'][0]['detail'])) { + $msg .= ' - ' . $data['error']['errors'][0]['detail']; + } elseif (isset($data['error']['code'])) { + $msg .= ' (' . $data['error']['code'] . ')'; + } - - $data = json_decode($response, true); - - // Check for HTTP errors (400, 401, 403, etc) - if ($httpCode >= 400) { - $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown Error'; - if (isset($data['error']['errors'][0]['detail'])) { - $msg .= ' - ' . $data['error']['errors'][0]['detail']; + return ['error' => "API HTTP $statusCode: $msg"]; } - return ['error' => "HTTP $httpCode: $msg"]; - } - return $data; + return $data; + + } catch (TransportExceptionInterface $e) { + return ['error' => 'Network/Transport Error: ' . $e->getMessage()]; + } catch (\Exception $e) { + return ['error' => 'Client Error: ' . $e->getMessage()]; + } } -} +} \ No newline at end of file diff --git a/config_uk.xml b/config_uk.xml new file mode 100644 index 0000000..f6acdf7 --- /dev/null +++ b/config_uk.xml @@ -0,0 +1,12 @@ + + + usps_api_bridge + + + + + + + 1 + 0 + \ No newline at end of file diff --git a/usps_api_bridge.php b/usps_api_bridge.php index 750fcfa..31d05f0 100644 --- a/usps_api_bridge.php +++ b/usps_api_bridge.php @@ -2,6 +2,8 @@ if (!defined('_PS_VERSION_')) { exit; } +use Symfony\Component\HttpClient\HttpClient; + class Usps_Api_Bridge extends Module { @@ -354,46 +356,59 @@ class Usps_Api_Bridge extends Module $clientSecret = Configuration::get('USPS_BRIDGE_CLIENT_SECRET'); $isLive = (bool)Configuration::get('USPS_BRIDGE_LIVE_MODE'); - // URLs based on documentation (Verification pending next step) + // CORRECT URLs based on the OpenAPI Spec provided: + // Prod: https://apis.usps.com/oauth2/v3 + // Test: https://apis-tem.usps.com/oauth2/v3 $url = $isLive - ? 'https://api.usps.com/oauth2/v3/token' - : 'https://api-cat.usps.com/oauth2/v3/token'; + ? 'https://apis.usps.com/oauth2/v3/token' + : 'https://apis-tem.usps.com/oauth2/v3/token'; $this->log("Requesting New Token from: " . $url); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ - 'client_id' => $clientId, - 'client_secret' => $clientSecret, - 'grant_type' => 'client_credentials' - ])); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // Create Symfony Client + $client = HttpClient::create([ + 'timeout' => 10, + 'verify_peer' => true, // Set to true in strict production environments + 'verify_host' => false, + ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + try { + $response = $client->request('POST', $url, [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + // 'json' key automatically encodes the array to JSON and sets Content-Type + 'json' => [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'grant_type' => 'client_credentials', + // 'scope' => 'prices international-prices' // Specifying scope helps avoid ambiguity + ], + ]); - if (curl_errno($ch)) { - $this->log("CURL Error: " . curl_error($ch)); - - return false; - } - + // Get status code + $statusCode = $response->getStatusCode(); - $data = json_decode($response, true); + // Convert response to array (pass false to prevent throwing exceptions on 4xx/5xx) + $data = $response->toArray(false); - if ($httpCode == 200 && isset($data['access_token'])) { - $expiresIn = isset($data['expires_in']) ? (int)$data['expires_in'] : 3599; + if ($statusCode == 200 && isset($data['access_token'])) { + $expiresIn = isset($data['expires_in']) ? (int)$data['expires_in'] : 3599; - Configuration::updateValue('USPS_BRIDGE_ACCESS_TOKEN', $data['access_token']); - Configuration::updateValue('USPS_BRIDGE_TOKEN_EXPIRY', time() + $expiresIn); + Configuration::updateValue('USPS_BRIDGE_ACCESS_TOKEN', $data['access_token']); + Configuration::updateValue('USPS_BRIDGE_TOKEN_EXPIRY', time() + $expiresIn); - $this->log("Token refreshed successfully."); - return $data['access_token']; + $this->log("Token refreshed successfully."); + return $data['access_token']; + } + + // Log detailed error from USPS + $this->log("Token Request Failed [HTTP $statusCode]: " . json_encode($data)); + } catch (\Exception $e) { + $this->log("Symfony HTTP Client Error: " . $e->getMessage()); } - $this->log("Token Request Failed: " . print_r($response, true)); return false; }