Files
wyczarujprezent.pl/modules/inpostizi/controllers/front/backend.php
Jacek Pyziak 4066f6fa31 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.
2025-09-14 14:38:09 +02:00

422 lines
14 KiB
PHP

<?php
use izi\prestashop\BasketApp\Exception\BasketAppException;
use izi\prestashop\Controller\Api\BasketController;
use izi\prestashop\Controller\Api\OrderController;
use izi\prestashop\Controller\Api\ProductController;
use izi\prestashop\Controller\WidgetController;
use izi\prestashop\Http\Client\ModuleVersionInfoProvidingClient;
use izi\prestashop\Http\Exception\HttpExceptionInterface;
use izi\prestashop\Http\Exception\ServerException;
use izi\prestashop\MerchantApi\Exception\ApiException;
use izi\prestashop\MerchantApi\Exception\BadGatewayException;
use izi\prestashop\MerchantApi\Exception\InternalServerErrorException;
use izi\prestashop\MerchantApi\Exception\ServiceUnavailableException;
use izi\prestashop\MerchantApi\Firewall\MerchantApiAuthenticator;
use izi\prestashop\OAuth2\Exception\OAuth2ExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Debug\ErrorHandler as LegacyErrorHandler;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class InpostIziBackendModuleFrontController extends ModuleFrontController
{
private const MERCHANT_ROUTES = [
[
'path' => '/inpost/v2/izi/merchant/basket/binding-key',
'methods' => ['GET'],
'controller' => [WidgetController::class, 'getBindingKey'],
],
[
'path' => '/inpost/v2/izi/merchant/order/confirmation-url',
'methods' => ['GET'],
'controller' => [WidgetController::class, 'getOrderConfirmationUrl'],
],
];
private const API_ROUTES = [
[
'path' => '/inpost/v1/izi/products',
'methods' => ['GET'],
'controller' => [ProductController::class, 'getProducts'],
],
[
'path' => '/inpost/v1/izi/order',
'methods' => ['POST'],
'controller' => [OrderController::class, 'create'],
],
[
'path' => '/inpost/v1/izi/order/{orderId}',
'methods' => ['GET'],
'prefix' => '/inpost/v1/izi/order/',
'regex' => '#^/inpost/v1/izi/order/(?<orderId>\d+)$#',
'controller' => [OrderController::class, 'get'],
],
[
'path' => '/inpost/v1/izi/order/{orderId}/event',
'methods' => ['POST'],
'prefix' => '/inpost/v1/izi/order/',
'regex' => '#^/inpost/v1/izi/order/(?<orderId>\d+)/event$#',
'controller' => [OrderController::class, 'update'],
],
[
'path' => '/inpost/v1/izi/basket/{basketId}',
'methods' => ['GET'],
'prefix' => '/inpost/v1/izi/basket/',
'regex' => '#^/inpost/v1/izi/basket/(?<basketId>.+)$#',
'controller' => [BasketController::class, 'get'],
],
[
'path' => '/inpost/v1/izi/basket/{basketId}/confirmation',
'methods' => ['POST'],
'prefix' => '/inpost/v1/izi/basket/',
'regex' => '#^/inpost/v1/izi/basket/(?<basketId>.+)/confirmation$#',
'controller' => [BasketController::class, 'confirm'],
],
[
'path' => '/inpost/v1/izi/basket/{basketId}/event',
'methods' => ['POST'],
'prefix' => '/inpost/v1/izi/basket/',
'regex' => '#^/inpost/v1/izi/basket/(?<basketId>.+)/event$#',
'controller' => [BasketController::class, 'update'],
],
[
'path' => '/inpost/v1/izi/basket/{basketId}/binding',
'methods' => ['DELETE'],
'prefix' => '/inpost/v1/izi/basket/',
'regex' => '#^/inpost/v1/izi/basket/(?<basketId>.+)/binding$#',
'controller' => [BasketController::class, 'deleteBinding'],
],
[
'path' => '/inpost/v1/izi/basket/{basketId}/binding/delete',
'methods' => ['POST'],
'prefix' => '/inpost/v1/izi/basket/',
'regex' => '#^/inpost/v1/izi/basket/(?<basketId>.+)/binding/delete$#',
'controller' => [BasketController::class, 'deleteBinding'],
],
[
'path' => '/inpost/v1/izi/basket/product/{productId}',
'methods' => ['POST'],
'prefix' => '/inpost/v1/izi/basket/product/',
'regex' => '#^/inpost/v1/izi/basket/product/(?<productId>.+)$#',
'controller' => [BasketController::class, 'addProduct'],
],
];
/**
* @var InPostIzi
*/
public $module;
protected $content_only = true;
private $outputBuffer = '';
public function postProcess()
{
$this->registerErrorHandler();
ob_start([$this, 'handleOutput'], 1);
$request = $this->module->getCurrentRequest();
$request->attributes->set('_inpost_izi_shop_id', (int) $this->context->shop->id);
$response = $this->handle($request);
Response::closeOutputBuffers(0, false);
$response->send();
exit;
}
/**
* @param Country $defaultCountry
*/
protected function geolocationManagement($defaultCountry): bool
{
return false;
}
protected function displayRestrictedCountryPage(): void
{
}
// TODO use own Sf Kernel?
private function handle(Request $request): Response
{
$path = $this->getPath($request);
try {
if (!str_contains($path, '/merchant/')) {
return $this->handleApiRequest($request, $path);
}
$response = $this->handleCustomerRequest($request, $path);
$response->prepare($request);
$this->context->cookie->write();
return $response;
} catch (Throwable $throwable) {
http_response_code(500);
$this->logError($throwable);
throw $throwable;
}
}
private function handleCustomerRequest(Request $request, string $path): Response
{
[$controller, $params] = $this->resolveController($request, $path, self::MERCHANT_ROUTES);
return null === $controller
? $this->createNotFoundResponse($request, $path)
: $this->callController($controller, $request, $params);
}
private function handleApiRequest(Request $request, string $path): Response
{
$method = $request->getMethod();
/** @var LoggerInterface $logger */
$logger = $this->module->get('inpost.izi.merchant_api_logger');
$logger->info('Request "{method} {path}"', [
'method' => $method,
'path' => $path,
]);
if ($body = $request->getContent()) {
$logger->debug('Request body: "{body}"', ['body' => $body]);
}
try {
$this->module->get(MerchantApiAuthenticator::class)->authenticate($request);
[$controller, $params] = $this->resolveController($request, $path, self::API_ROUTES);
/** @var JsonResponse $response */
$response = null === $controller
? $this->createNotFoundResponse($request, $path, true)
: $this->callController($controller, $request, $params);
} catch (Throwable $throwable) {
$response = $this->handleApiError($throwable);
}
$logger->info('Response "{method} {path}": {status_code}', [
'method' => $method,
'path' => $path,
'status_code' => $response->getStatusCode(),
]);
if (!$response->isSuccessful()) {
$logger->error('Response body: "{body}"', ['body' => $response->getContent()]);
} elseif ($body = $response->getContent()) {
$logger->debug('Response body: "{body}"', ['body' => $body]);
}
$response->headers->set(ModuleVersionInfoProvidingClient::HEADER_NAME, $this->module->version);
return $response;
}
private function handleApiError(Throwable $throwable): Response
{
$exception = $this->convertApiError($throwable);
if ($exception instanceof InternalServerErrorException && $previous = $exception->getPrevious()) {
$this->logError($previous);
}
return new JsonResponse([
'error_code' => $exception->getErrorCode(),
'error_message' => $exception->getMessage(),
], $exception->getStatusCode());
}
private function convertApiError(Throwable $throwable): ApiException
{
if ($throwable instanceof ApiException) {
return $throwable;
}
if ($throwable instanceof NetworkExceptionInterface) {
$message = $throwable instanceof OAuth2ExceptionInterface
? 'Could not connect to the authorization server'
: 'Could not connect to the basket app API';
return BadGatewayException::create($throwable, $message);
}
if ($throwable instanceof OAuth2ExceptionInterface) {
return BadGatewayException::create($throwable, 'Could not obtain access token');
}
if ($throwable instanceof BasketAppException) {
return BadGatewayException::create($throwable, sprintf('Basket app API error: "%s"', $throwable->getError()->getCode()));
}
if ($throwable instanceof ServerException && 503 === $throwable->getCode()) {
return ServiceUnavailableException::create($throwable, 'Basket app API is unavailable');
}
if ($throwable instanceof HttpExceptionInterface) {
return BadGatewayException::create($throwable, sprintf('Unexpected basket app API response status code: %d', $throwable->getCode()));
}
return InternalServerErrorException::create($throwable);
}
private function getPath(Request $request): string
{
if (null === $path = $request->query->get('path')) {
return '/';
}
$path = rawurldecode($path);
if ('/' !== $path[0]) {
$path = '/' . $path;
}
return rtrim($path, '/');
}
private function logError(Throwable $throwable): void
{
$this->module->getLogger()->error('Error processing request: {error}', ['error' => $throwable]);
}
private function resolveController(Request $request, string $path, array $routes): array
{
$method = $request->getMethod();
foreach ($routes as $route) {
if (isset($route['methods']) && !in_array($method, $route['methods'], true)) {
continue;
}
if (isset($route['prefix']) && !str_starts_with($path, $route['prefix'])) {
continue;
}
if (!isset($route['regex']) && $path !== $route['path']) {
continue;
}
if (isset($route['regex']) && !preg_match($route['regex'], $path, $params)) {
continue;
}
return [$route['controller'], $params ?? []];
}
return [null, []];
}
private function callController(array $controller, Request $request, array $pathParams): Response
{
$arguments = $this->resolveControllerArguments($controller, $request, $pathParams);
$controller = [$this->createController($controller[0]), $controller[1]];
return $controller(...$arguments);
}
private function createController(string $className)
{
try {
return $this->module->get($className);
} catch (ServiceNotFoundException $e) {
return new $className();
}
}
private function resolveControllerArguments(array $controller, Request $request, array $pathParams): array
{
$reflection = new ReflectionMethod($controller[0], $controller[1]);
return array_map(function (ReflectionParameter $param) use ($request, $pathParams) {
return $this->resolveControllerArgument($param, $request, $pathParams);
}, $reflection->getParameters());
}
private function resolveControllerArgument(ReflectionParameter $param, Request $request, array $pathParams)
{
$type = $param->getType();
if (null !== $type && Request::class === $type->getName()) {
return $request;
}
$paramName = $param->getName();
if (isset($pathParams[$paramName])) {
return $pathParams[$paramName];
}
if ($param->isDefaultValueAvailable()) {
return $param->getDefaultValue();
}
throw new LogicException(sprintf('Cannot determine controller parameter value for argument "%s".', $paramName));
}
private function createNotFoundResponse(Request $request, string $path, bool $json = false): Response
{
$message = sprintf('No route found for "%s %s"', $request->getMethod(), $path);
return $json || in_array('application/json', $request->getAcceptableContentTypes(), true)
? new JsonResponse([
'error_code' => 'NOT_FOUND',
'error_message' => $message,
], 404)
: new Response($message, 404);
}
private function registerErrorHandler(): void
{
$handler = class_exists(ErrorHandler::class)
? ErrorHandler::register()
: LegacyErrorHandler::register();
$handler->throwAt(E_ERROR, true);
$handler->setDefaultLogger($this->module->getLogger(), [
E_DEPRECATED => LogLevel::DEBUG,
E_NOTICE => LogLevel::DEBUG,
E_STRICT => LogLevel::DEBUG,
E_WARNING => LogLevel::DEBUG,
E_USER_WARNING => LogLevel::DEBUG,
E_USER_ERROR => LogLevel::CRITICAL,
E_RECOVERABLE_ERROR => LogLevel::CRITICAL,
E_ERROR => LogLevel::CRITICAL,
]);
}
private function handleOutput(string $buffer, int $phase): string
{
if (PHP_OUTPUT_HANDLER_FINAL & $phase && '' !== $this->outputBuffer) {
$this->module->getLogger()->warning('Output buffer content: "{buffer}".', [
'buffer' => $this->outputBuffer,
]);
$this->outputBuffer = '';
}
if ('' === $buffer) {
return '';
}
$this->outputBuffer .= $buffer;
if (PHP_OUTPUT_HANDLER_START & $phase) {
$this->module->getLogger()->warning(sprintf(
"Output started before sending response.\n[stacktrace]\n%s\n",
(new \Exception())->getTraceAsString()
));
}
return $buffer;
}
}