Replace 86+ generic RuntimeException throws with domain-specific exception classes: AllegroApiException, AllegroOAuthException, ApaczkaApiException, ShipmentException, IntegrationConfigException — all extending OrderProException extends RuntimeException. Existing catch(RuntimeException) blocks unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
240 lines
8.7 KiB
PHP
240 lines
8.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Settings;
|
|
|
|
use AppCorexceptionsApaczkaApiException;
|
|
|
|
final class ApaczkaApiClient
|
|
{
|
|
private const API_BASE_URL = 'https://www.apaczka.pl/api/v2';
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function getServiceStructure(string $appId, string $appSecret): array
|
|
{
|
|
$response = $this->request('/service_structure/', 'service_structure', $appId, $appSecret, []);
|
|
$services = $response['response']['services'] ?? $response['services'] ?? [];
|
|
return is_array($services) ? $services : [];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $orderPayload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function sendOrder(string $appId, string $appSecret, array $orderPayload): array
|
|
{
|
|
return $this->request('/order_send/', 'order_send', $appId, $appSecret, [
|
|
'order' => $orderPayload,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getOrderDetails(string $appId, string $appSecret, int $orderId): array
|
|
{
|
|
$safeOrderId = max(1, $orderId);
|
|
return $this->request('/order/' . $safeOrderId . '/', 'order/' . $safeOrderId, $appId, $appSecret, []);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getWaybill(string $appId, string $appSecret, int $orderId): array
|
|
{
|
|
$safeOrderId = max(1, $orderId);
|
|
return $this->request('/waybill/' . $safeOrderId . '/', 'waybill/' . $safeOrderId, $appId, $appSecret, []);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function getPoints(string $appId, string $appSecret, string $type = 'parcel_locker'): array
|
|
{
|
|
$safeType = trim($type) !== '' ? trim($type) : 'parcel_locker';
|
|
$route = 'points/' . $safeType;
|
|
$response = $this->request('/' . $route . '/', $route, $appId, $appSecret, []);
|
|
$points = $response['response']['points'] ?? $response['points'] ?? [];
|
|
return is_array($points) ? $points : [];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $request
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function request(string $endpointPath, string $route, string $appId, string $appSecret, array $request): array
|
|
{
|
|
$normalizedRoute = trim($route, " \t\n\r\0\x0B/");
|
|
if ($normalizedRoute === '') {
|
|
throw new ApaczkaApiException('Nie podano endpointu API Apaczka.');
|
|
}
|
|
$routeWithTrailingSlash = $normalizedRoute . '/';
|
|
|
|
$expires = (string) (time() + 60);
|
|
$requestJson = json_encode($request);
|
|
if (!is_string($requestJson)) {
|
|
throw new ApaczkaApiException('Nie mozna zakodowac payloadu Apaczka.');
|
|
}
|
|
|
|
$basePayload = [
|
|
'app_id' => trim($appId),
|
|
'request' => $requestJson,
|
|
'expires' => $expires,
|
|
];
|
|
$signatureVariants = $this->buildSignatureVariants(
|
|
trim((string) $basePayload['app_id']),
|
|
$endpointPath,
|
|
$routeWithTrailingSlash,
|
|
$requestJson,
|
|
$expires,
|
|
$appSecret
|
|
);
|
|
|
|
$lastSignatureError = null;
|
|
foreach ($signatureVariants as $signature) {
|
|
$payload = $basePayload;
|
|
$payload['signature'] = $signature;
|
|
[$decoded, $httpCode] = $this->executeRequest($endpointPath, $payload);
|
|
|
|
$status = (int) ($decoded['status'] ?? 0);
|
|
$message = $this->resolveErrorMessage($decoded);
|
|
$isSignatureMismatch = $status === 400 && stripos($message, 'signature') !== false;
|
|
if ($isSignatureMismatch) {
|
|
$lastSignatureError = [$decoded, $status, $message];
|
|
continue;
|
|
}
|
|
|
|
if ($httpCode < 200 || $httpCode >= 300) {
|
|
throw new ApaczkaApiException('API Apaczka HTTP ' . $httpCode . ': ' . $message);
|
|
}
|
|
if ($status !== 200) {
|
|
$responsePreview = substr(json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '', 0, 240);
|
|
throw new ApaczkaApiException(
|
|
'Blad API Apaczka (status ' . $status . '): ' . $message . '. Odpowiedz: ' . $responsePreview
|
|
);
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
if ($lastSignatureError !== null) {
|
|
[$decoded, $status, $message] = $lastSignatureError;
|
|
$responsePreview = substr(json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '', 0, 240);
|
|
throw new ApaczkaApiException(
|
|
'Blad API Apaczka (status ' . $status . '): ' . $message . '. Odpowiedz: ' . $responsePreview
|
|
);
|
|
}
|
|
|
|
throw new ApaczkaApiException('Blad API Apaczka.');
|
|
}
|
|
|
|
private function buildSignature(
|
|
string $appId,
|
|
string $route,
|
|
string $requestJson,
|
|
string $expires,
|
|
string $appSecret
|
|
): string
|
|
{
|
|
return hash_hmac('sha256', $appId . ':' . $route . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function buildSignatureVariants(
|
|
string $appId,
|
|
string $endpointPath,
|
|
string $route,
|
|
string $requestJson,
|
|
string $expires,
|
|
string $appSecret
|
|
): array {
|
|
$endpointTrimmed = trim($endpointPath, " \t\n\r\0\x0B/");
|
|
$endpointWithSlashes = '/' . $endpointTrimmed . '/';
|
|
$endpointWithTrailingSlash = $endpointTrimmed . '/';
|
|
|
|
$variants = [];
|
|
$variants[] = hash_hmac('sha256', $appId . ':' . $endpointWithTrailingSlash . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
|
$variants[] = hash_hmac('sha256', $appId . ':' . $route . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
|
$variants[] = hash_hmac('sha256', $appId . ':' . $endpointWithSlashes . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
|
$variants[] = hash_hmac('sha256', $appId . ':' . $endpointTrimmed . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
|
$variants[] = hash('sha256', $appId . ':' . $endpointWithTrailingSlash . ':' . $requestJson . ':' . $expires . ':' . trim($appSecret));
|
|
$variants[] = hash('sha256', $appId . ':' . $route . ':' . $requestJson . ':' . $expires . ':' . trim($appSecret));
|
|
$variants[] = hash('sha256', $appId . ':' . $endpointWithSlashes . ':' . $requestJson . ':' . $expires . ':' . trim($appSecret));
|
|
|
|
return array_values(array_unique($variants));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $payload
|
|
* @return array{0: array<string, mixed>, 1: int}
|
|
*/
|
|
private function executeRequest(string $endpointPath, array $payload): array
|
|
{
|
|
$url = rtrim(self::API_BASE_URL, '/') . '/' . ltrim($endpointPath, '/');
|
|
$ch = curl_init($url);
|
|
if ($ch === false) {
|
|
throw new ApaczkaApiException('Nie udalo sie zainicjowac polaczenia z API Apaczka.');
|
|
}
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => http_build_query($payload),
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Accept: application/json',
|
|
'Content-Type: application/x-www-form-urlencoded',
|
|
'User-Agent: orderPRO/1.0',
|
|
],
|
|
]);
|
|
|
|
$rawBody = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($ch);
|
|
unset($ch);
|
|
|
|
if ($rawBody === false) {
|
|
throw new ApaczkaApiException('Blad polaczenia z API Apaczka: ' . $curlError);
|
|
}
|
|
|
|
$normalizedBody = ltrim((string) $rawBody, "\xEF\xBB\xBF \t\n\r\0\x0B");
|
|
$decoded = json_decode($normalizedBody, true);
|
|
if (!is_array($decoded)) {
|
|
$snippet = substr(trim(strip_tags($normalizedBody)), 0, 180);
|
|
throw new ApaczkaApiException(
|
|
'Nieprawidlowa odpowiedz API Apaczka (brak JSON, HTTP ' . $httpCode . '). Fragment: ' . $snippet
|
|
);
|
|
}
|
|
|
|
return [$decoded, $httpCode];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $decoded
|
|
*/
|
|
private function resolveErrorMessage(array $decoded): string
|
|
{
|
|
$topMessage = trim((string) ($decoded['message'] ?? ''));
|
|
if ($topMessage !== '') {
|
|
return $topMessage;
|
|
}
|
|
|
|
$responseMessage = trim((string) ($decoded['response']['message'] ?? ''));
|
|
if ($responseMessage !== '') {
|
|
return $responseMessage;
|
|
}
|
|
|
|
$errorMessage = trim((string) ($decoded['error']['message'] ?? ''));
|
|
if ($errorMessage !== '') {
|
|
return $errorMessage;
|
|
}
|
|
|
|
return 'Blad API Apaczka.';
|
|
}
|
|
}
|