Add InPost Pay integration to admin templates

- Created a new template for the cart rule form with custom label, switch, and choice widgets.
- Implemented the InPost Pay block in the order details template for displaying delivery method, APM, and VAT invoice request.
- Added legacy support for the order details template to maintain compatibility with older PrestaShop versions.
This commit is contained in:
2025-09-14 14:38:09 +02:00
parent d895f86a03
commit 4066f6fa31
1086 changed files with 76598 additions and 6 deletions

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client\Adapter;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
use GuzzleHttp\Exception as GuzzleExceptions;
use GuzzleHttp\Message\RequestInterface as GuzzleRequest;
use GuzzleHttp\Message\ResponseInterface as GuzzleResponse;
use Nyholm\Psr7\Response;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class Guzzle5Adapter implements ClientInterface
{
private $client;
public function __construct(?GuzzleClientInterface $client = null)
{
$this->client = $client ?: new GuzzleClient();
}
/**
* {@inheritDoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
$guzzleRequest = $this->createRequest($request);
try {
$response = $this->client->send($guzzleRequest);
} catch (GuzzleExceptions\TransferException $exception) {
if (!$exception instanceof GuzzleExceptions\RequestException || !$exception->hasResponse()) {
throw $this->handleException($exception, $request);
}
$response = $exception->getResponse();
}
return $this->createResponse($response);
}
private function createRequest(RequestInterface $request): GuzzleRequest
{
$body = (string) $request->getBody();
return $this->client->createRequest($request->getMethod(), (string) $request->getUri(), [
'exceptions' => false,
'allow_redirects' => false,
'version' => $request->getProtocolVersion(),
'headers' => $request->getHeaders(),
'body' => '' === $body ? null : $body,
]);
}
private function createResponse(GuzzleResponse $response): ResponseInterface
{
$body = $response->getBody();
return new Response(
$response->getStatusCode(),
$response->getHeaders(),
isset($body) ? $body->detach() : null,
$response->getProtocolVersion(),
$response->getReasonPhrase()
);
}
private function handleException(GuzzleExceptions\TransferException $exception, RequestInterface $request): ClientExceptionInterface
{
return $exception instanceof GuzzleExceptions\ConnectException
? NetworkException::fromGuzzleException($exception, $request)
: RequestException::fromGuzzleException($exception, $request);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client\Adapter;
use GuzzleHttp\Exception\ConnectException;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Message\RequestInterface;
final class NetworkException extends \RuntimeException implements NetworkExceptionInterface
{
private $request;
public function __construct(RequestInterface $request, string $message = '', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
public static function fromGuzzleException(ConnectException $exception, RequestInterface $request): self
{
return new self($request, $exception->getMessage(), 0, $exception);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client\Adapter;
use GuzzleHttp\Exception\TransferException;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
final class RequestException extends \InvalidArgumentException implements RequestExceptionInterface
{
private $request;
public function __construct(RequestInterface $request, string $message = '', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
public static function fromGuzzleException(TransferException $exception, RequestInterface $request): self
{
return new self($request, $exception->getMessage(), 0, $exception);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client;
use izi\prestashop\OAuth2\AuthorizationProviderInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class AuthorizingClient implements ClientInterface
{
/**
* @var ClientInterface
*/
private $client;
/**
* @var AuthorizationProviderInterface
*/
private $authorizationProvider;
/**
* @var string[]
*/
private $scopes;
/**
* @param string[] $scopes scopes to request from the authorization server
*/
public function __construct(ClientInterface $client, AuthorizationProviderInterface $authorizationProvider, array $scopes = [])
{
$this->client = $client;
$this->authorizationProvider = $authorizationProvider;
$this->scopes = $scopes;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$request = $this->authorize($request);
$response = $this->client->sendRequest($request);
if (401 !== $response->getStatusCode()) {
return $response;
}
$request = $this->authorize($request, true);
return $this->client->sendRequest($request);
}
private function authorize(RequestInterface $request, bool $renewAccessToken = false): RequestInterface
{
return $this->authorizationProvider
->getAccessToken($renewAccessToken, $this->scopes)
->authorize($request);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client\Factory;
use Psr\Http\Client\ClientInterface;
interface ClientFactoryInterface
{
public function create(): ClientInterface;
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client\Factory;
use GuzzleHttp\Client;
use izi\prestashop\Http\Client\Adapter\Guzzle5Adapter;
use Psr\Http\Client\ClientInterface;
class GuzzleClientFactory implements ClientFactoryInterface
{
/**
* @var int
*/
private $timeout;
public function __construct(int $timeout = 10)
{
$this->timeout = $timeout;
}
public function create(): ClientInterface
{
if (!class_exists(Client::class)) {
throw new \RuntimeException(sprintf('Class %s does not exist', Client::class));
}
if (is_subclass_of(Client::class, ClientInterface::class)) {
return new Client([
'connect_timeout' => 3.,
'timeout' => $this->timeout,
]);
}
$client = new Client([
'defaults' => [
'connect_timeout' => 3.,
'timeout' => $this->timeout,
],
]);
return new Guzzle5Adapter($client);
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
final class LoggingClient implements ClientInterface
{
/**
* @var ClientInterface
*/
private $client;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var array
*/
private $options;
public function __construct(ClientInterface $client, LoggerInterface $logger, array $options = [])
{
$this->client = $client;
$this->logger = $logger;
$this->options = $options;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$this->logRequest($request);
try {
$response = $this->client->sendRequest($request);
$this->logResponse($request, $response);
return $response;
} catch (\Throwable $throwable) {
$this->logError($request, $throwable);
throw $throwable;
}
}
private function logRequest(RequestInterface $request): void
{
$this->logger->info('Request: "{method} {uri}"', [
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
]);
if ('' !== $body = (string) $request->getBody()) {
$this->logger->debug('Request body: "{body}"', ['body' => $body]);
}
}
private function logResponse(RequestInterface $request, ResponseInterface $response): void
{
$context = [
'status_code' => $statusCode = $response->getStatusCode(),
'uri' => (string) $request->getUri(),
];
if (400 <= $statusCode) {
if ('' !== $body = (string) $response->getBody()) {
$context['body'] = $body;
}
$this->logger->error('Response: "{status_code} {uri}"', $context);
} elseif (300 <= $statusCode) {
$context['location'] = $response->getHeaderLine('location');
$this->logger->info('Response: "{status_code} {uri}"', $context);
} else {
$this->logger->info('Response: "{status_code} {uri}"', $context);
$this->logResponseBody($response);
}
}
private function logError(RequestInterface $request, \Throwable $throwable): void
{
if ($throwable instanceof NetworkExceptionInterface) {
$this->logger->error('Network error for {uri}: "{message}"', [
'uri' => (string) $throwable->getRequest()->getUri(),
'message' => $throwable->getMessage(),
]);
} else {
$this->logger->critical('Unexpected error for {uri}: {error}', [
'uri' => (string) $request->getUri(),
'error' => $throwable,
]);
}
}
private function logResponseBody(ResponseInterface $response): void
{
if ('' === $body = (string) $response->getBody()) {
return;
}
if (!isset($this->options['max_response_body_size']) || strlen($body) <= $this->options['max_response_body_size']) {
$this->logger->debug('Response body: "{body}"', ['body' => $body]);
return;
}
$this->logger->debug('Response body: "{body}"', [
'body' => \Tools::substr($body, 0, $this->options['max_response_body_size']) . '...',
'body_size' => $response->getBody()->getSize(),
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Client;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class ModuleVersionInfoProvidingClient implements ClientInterface
{
public const HEADER_NAME = 'inpay-plugin-version';
/**
* @var ClientInterface
*/
private $client;
/**
* @var \Module
*/
private $module;
public function __construct(ClientInterface $client, \Module $module)
{
$this->client = $client;
$this->module = $module;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$request = $request->withHeader(self::HEADER_NAME, $this->module->version);
return $this->client->sendRequest($request);
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Exception;
class ClientException extends \RuntimeException implements HttpExceptionInterface
{
use HttpExceptionTrait;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
interface HttpExceptionInterface
{
public function getRequest(): RequestInterface;
public function getResponse(): ResponseInterface;
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @mixin \Exception
*/
trait HttpExceptionTrait
{
/**
* @var RequestInterface
*/
private $request;
/**
* @var ResponseInterface
*/
private $response;
public function __construct(RequestInterface $request, ResponseInterface $response)
{
$this->request = $request;
$this->response = $response;
$statusCode = $response->getStatusCode();
$message = sprintf('HTTP %d returned for "%s".', $statusCode, $request->getUri());
parent::__construct($message, $statusCode);
}
public function getRequest(): RequestInterface
{
return $this->request;
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Exception;
class RedirectionException extends \RuntimeException implements HttpExceptionInterface
{
use HttpExceptionTrait;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Exception;
class ServerException extends \RuntimeException implements HttpExceptionInterface
{
use HttpExceptionTrait;
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class EventStreamResponse extends StreamedResponse
{
public function __construct(?callable $callback = null, int $status = 200, array $headers = [])
{
$headers = array_merge($headers, [
'X-Accel-Buffering' => 'no',
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-store',
'Connection' => 'keep-alive',
'Access-Control-Expose-Headers' => 'X-Events',
]);
parent::__construct($callback, $status, $headers);
}
public function sendContent(): self
{
if ($this->streamed) {
return $this;
}
@ini_set('zlib.output_compression', '0');
@ini_set('implicit_flush', '1');
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', '1');
}
gc_enable();
ob_implicit_flush(80000 <= PHP_VERSION_ID ? true : 1);
session_write_close();
parent::sendContent();
return $this;
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Response;
/**
* @template T of (string|\Stringable)
*/
final class ServerSentEvent
{
/**
* @var string|null
*/
private $id;
/**
* @var string|null
*/
private $event;
/**
* @var T|null
*/
private $data;
/**
* @var int|null
*/
private $retry;
/**
* @var string|null
*/
private $comment;
/**
* @var string|null
*/
private $message;
/**
* @param T|null $data
*/
public function __construct(?string $id, ?string $event, $data, ?int $retry, ?string $comment)
{
if (null !== $retry && 0 >= $retry) {
throw new \DomainException('Delay should be greater than 0.');
}
$this->id = $id;
$this->event = $event;
$this->data = $data;
$this->retry = $retry;
$this->comment = $comment;
}
public static function builder(): ServerSentEventBuilder
{
return new ServerSentEventBuilder();
}
public function getMessage(): string
{
if (isset($this->message)) {
return $this->message;
}
$lines = [];
if (null !== $this->comment) {
$lines[] = ": $this->comment";
}
if (null !== $this->id) {
$lines[] = "id: $this->id";
}
if (null !== $this->event) {
$lines[] = "event: $this->event";
}
if (null !== $this->data) {
$data = (string) $this->data;
$lines[] = "data: $data";
}
if (null !== $this->retry) {
$lines[] = "retry: $this->retry";
}
return $this->message = implode(PHP_EOL, $lines) . str_repeat(PHP_EOL, 2);
}
public function __toString(): string
{
return $this->getMessage();
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Response;
/**
* @template T of (string|\Stringable)
*/
final class ServerSentEventBuilder
{
/**
* @var string|null
*/
private $id;
/**
* @var string|null
*/
private $event;
/**
* @var T|null
*/
private $data;
/**
* @var int|null
*/
private $retry;
/**
* @var string|null
*/
private $comment;
public static function create(): self
{
return new self();
}
public function setId(?string $id): self
{
$this->id = $id;
return $this;
}
public function setEventName(?string $name): self
{
$this->event = $name;
return $this;
}
/**
* @param T|null $data
*/
public function setData($data): self
{
$this->data = $data;
return $this;
}
public function setRetry(?int $delayMs): self
{
$this->retry = $delayMs;
return $this;
}
public function setComment(?string $comment): self
{
$this->comment = $comment;
return $this;
}
/**
* @return ServerSentEvent<T>
*/
public function build(): ServerSentEvent
{
return new ServerSentEvent($this->id, $this->event, $this->data, $this->retry, $this->comment);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Http\Util;
final class UriResolver
{
public static function resolve(string $uri, string $baseUri): string
{
$uriParts = parse_url($uri);
if (!empty($uriParts['host'])) {
return $uri;
}
$baseUriParts = parse_url($baseUri);
$uriParts['path'] = self::resolvePath($baseUriParts, $uriParts);
$uriParts += $baseUriParts;
return http_build_url($uriParts);
}
private static function resolvePath(array $baseUri, array $uri): string
{
$basePath = $baseUri['path'] ?? null;
$path = $uri['path'] ?? null;
if (null === $path) {
$path = $basePath;
} elseif ('/' !== $path[0]) {
if (null === $basePath) {
$path = '/' . $path;
} else {
$segments = explode('/', $basePath);
array_splice($segments, -1, 1, [$path]);
$path = implode('/', $segments);
}
}
return empty($path) ? '/' : $path;
}
}