Files
orderPRO/src/Modules/Settings/ApaczkaApiClient.php
Jacek Pyziak 3c27c4e54a feat(06-sonarqube-quality): introduce typed exception hierarchy (S112 fix)
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>
2026-03-13 11:04:52 +01:00

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.';
}
}