Newer
Older
TillQliro / Model / Api / Service.php
@Jonas Jonsson Jonas Jonsson on 2 Apr 2024 8 KB Initial
<?php
/**
 * Copyright © Qliro AB. All rights reserved.
 * See LICENSE.txt for license details.
 */

namespace Qliro\QliroOne\Model\Api;

use GuzzleHttp\Exception\ClientException;
use Psr\Http\Message\ResponseInterface;
use Qliro\QliroOne\Model\Config;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use GuzzleHttp\TransferStats;
use Magento\Framework\Serialize\Serializer\Json;
use Qliro\QliroOne\Model\Exception\TerminalException;
use Qliro\QliroOne\Model\Logger\Manager;

/**
 * QliroOne API Service implementation
 */
class Service implements \Qliro\QliroOne\Api\ApiServiceInterface
{
    const METHOD_GET = 'GET';
    const METHOD_POST = 'POST';
    const METHOD_PUT = 'PUT';

    const HEADER_CONTENT_TYPE = 'Content-Type';
    const HEADER_CONTENT_TYPE_JSON = 'application/json';
    const AUTHENTICATION_PREFIX = 'Qliro';
    const HEADER_AUTHENTICATION = 'Authorization';
    const QLIRO_SANDBOX_API_URL = 'https://pago.qit.nu';
    const QLIRO_PROD_API_URL = 'https://payments.qit.nu';

    /**
     * @var Config
     */
    private $config;

    /**
     * @var Client
     */
    private $client;

    /**
     * @var \Magento\Framework\Serialize\Serializer\Json
     */
    private $json;

    /**
     * @var \Qliro\QliroOne\Model\Logger\Manager
     */
    private $logManager;

    /**
     * @var float
     */
    private $duration;

    /**
     * Inject dependencies
     *
     * @param \Qliro\QliroOne\Model\Config $config
     * @param \GuzzleHttp\Client $client
     * @param \Magento\Framework\Serialize\Serializer\Json $json
     * @param \Qliro\QliroOne\Model\Logger\Manager $logManager
     */
    public function __construct(
        Config $config,
        Client $client,
        Json $json,
        Manager $logManager
    ) {
        $this->config = $config;
        $this->client = $client;
        $this->json = $json;
        $this->logManager = $logManager;
    }

    /**
     * Perform GET request
     *
     * @param string $endpoint
     * @param array $data
     * @param int|null $storeId
     * @return array
     * @throws \InvalidArgumentException
     * @throws \Qliro\QliroOne\Model\Exception\TerminalException
     */
    public function get($endpoint, $data = [], $storeId = null)
    {
        $this->applyParams($endpoint, $data);

        return $this->call(self::METHOD_GET, $endpoint, $data, $storeId);
    }

    /**
     * Perform POST request
     *
     * @param string $endpoint
     * @param array $data
     * @param int|null $storeId
     * @return array
     * @throws \InvalidArgumentException
     * @throws \Qliro\QliroOne\Model\Exception\TerminalException
     */
    public function post($endpoint, $data = [], $storeId = null)
    {
        return $this->call(self::METHOD_POST, $endpoint, $data, $storeId);
    }

    /**
     * Perform PUT request
     *
     * @param string $endpoint
     * @param array $data
     * @param int|null $storeId
     * @return array
     * @throws \InvalidArgumentException
     * @throws \Qliro\QliroOne\Model\Exception\TerminalException
     */
    public function put($endpoint, $data = [], $storeId = null)
    {
        $this->applyParams($endpoint, $data);

        return $this->call(self::METHOD_PUT, $endpoint, $data, $storeId);
    }

    /**
     * Replace all placeholders within endpoint from the $params array
     *
     * @param string $endpoint
     * @param array $params
     */
    private function applyParams(&$endpoint, &$params)
    {
        foreach ($params as $key => $value) {
            if (!is_scalar($value)) {
                continue;
            }
            $modifiedEndpoint = str_replace('{' . $key . '}', $value, $endpoint);

            if ($modifiedEndpoint !== $endpoint) {
                unset($params[$key]);
                $endpoint = $modifiedEndpoint;
            }
        }

        $endpoint = preg_replace('/\{[^}]+\}/', '*', $endpoint);
    }

    /**
     * Perform an API call
     *
     * @param string $method
     * @param string $endpoint
     * @param array $body
     * @param int|null $storeId
     * @return array
     * @throws \InvalidArgumentException
     * @throws \Qliro\QliroOne\Model\Exception\TerminalException
     */
    private function call($method, $endpoint, $body = [], $storeId = null)
    {
        $this->logManager->setMark('REST API');

        if ($method === self::METHOD_GET) {
            $payload = '';
            $options[RequestOptions::QUERY] = $body;
        } else {
            if (!isset($body['MerchantApiKey'])) {
                $body['MerchantApiKey'] = $this->config->getMerchantApiKey($storeId);
            }
            $payload = $this->json->serialize($body);
            $options[RequestOptions::BODY] = $payload;
        }

        $headers = [
            self::HEADER_CONTENT_TYPE => self::HEADER_CONTENT_TYPE_JSON,
            self::HEADER_AUTHENTICATION => $this->getAuthenticationToken($payload, $method, $storeId)
        ];

        $options[RequestOptions::HEADERS] = $headers;
        $options[RequestOptions::ON_STATS] = [$this, 'receiveStats'];

        $this->duration = 0.0;
        $endpointUri = $this->prepareEndpointUri($endpoint, $storeId);

        $this->logManager->debug(
            '>>> {method} {endpoint}',
            [
                'method' => $method,
                'endpoint' => $endpoint,
                'extra' => [
                    'uri' => $endpointUri,
                    'body' => $body,
                ],
            ]
        );

        try {
            $response = $this->client->request($method, $endpointUri, $options);
            $responseData = $this->getResponseData($response);

            $this->logManager->debug(
                '<<< Result in {duration} seconds',
                [
                    'duration' => $this->duration,
                    'extra' => [
                        'uri' => $endpointUri,
                        'request' => $body,
                        'status_code' => $response->getStatusCode(),
                        'response' => $responseData,
                    ]
                ]
            );
        } catch (\Exception $exception) {
            $exceptionData = [
                'exception' => $exception->getMessage(),
                'uri' => $endpointUri,
                'request' => $body,
            ];

            if ($exception instanceof ClientException) {
                $response = $exception->getResponse();

                $exceptionData = array_merge($exceptionData, [
                    'status_code' => $response->getStatusCode(),
                    'error_reason' => $response->getReasonPhrase(),
                    'response' => $this->getResponseData($response),
                ]);
            }

            $this->logManager->error(
                '<<< Exception after {duration} seconds',
                [
                    'duration' => $this->duration,
                    'extra' => $exceptionData
                ]
            );

            throw new TerminalException($exception->getMessage(), $exception->getCode(), $exception);
        } finally {
            $this->logManager->setMark(null);
        }

        return $responseData;
    }

    /**
     * @param \GuzzleHttp\TransferStats $stats
     */
    public function receiveStats(TransferStats $stats)
    {
        $this->duration = $stats->getTransferTime();
    }

    /**
     * Prepare a full URI to the endpoint
     *
     * @param string $endpoint
     * @param int|null $storeId
     * @return string
     */
    private function prepareEndpointUri($endpoint, $storeId = null)
    {
        $baseUri = $this->config->getApiType($storeId) === 'prod' ? self::QLIRO_PROD_API_URL : self::QLIRO_SANDBOX_API_URL;

        return implode('/', [$baseUri, trim($endpoint, '/')]);
    }

    /**
     * @param string $body
     * @param string $method
     * @param int|null $storeId
     * @return string
     */
    private function getAuthenticationToken($body, $method = self::METHOD_POST, $storeId = null)
    {
        if ($method === self::METHOD_GET) {
            $body = '';
        }

        $secret = $this->config->getMerchantApiSecret($storeId);
        $secretString = base64_encode(hash('sha256', $body . $secret, true));
        $token = trim(implode(' ', [self::AUTHENTICATION_PREFIX, $secretString]));

        return $token;
    }

    /**
     * Get and decode request data
     *
     * @param \Psr\Http\Message\ResponseInterface $response
     * @return array
     */
    private function getResponseData(ResponseInterface $response): array
    {
        $responseString = (string)$response->getBody();

        try {
            $responseData = $responseString ? (array)$this->json->unserialize($responseString) : [];
        } catch (\InvalidArgumentException $exception) {
            $responseData = [];
        }

        return $responseData;
    }
}