diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bbc2df76c..d6e593536 100755 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -39,7 +39,7 @@ jobs: - run: composer install - name: Run PHP-CS-Fixer - run: vendor/bin/php-cs-fixer fix --dry-run + uses: prestashopcorp/github-action-php-cs-fixer@master phpstan: name: PHPStan diff --git a/classes/Adapter/BillingAdapter.php b/classes/Adapter/BillingAdapter.php new file mode 100644 index 000000000..b59d79b1f --- /dev/null +++ b/classes/Adapter/BillingAdapter.php @@ -0,0 +1,51 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\Module\PsxMarketingWithGoogle\Adapter; + +use PrestaShop\Module\PsxMarketingWithGoogle\Http\HttpClient; + +class BillingAdapter +{ + private const BILLING_URL = 'https://billing-api.distribution.prestashop.net/v1/'; + + /** + * @var string + */ + private $jwt; + + public function __construct($jwt) + { + $this->jwt = $jwt; + } + + public function getCurrentSubscription($shopId, $productId) + { + $httpClient = new HttpClient(self::BILLING_URL); + $httpClient->setHeaders([ + 'Accept: application/json', + 'Authorization: Bearer ' . $this->jwt, + 'Content-Type: application/json', + 'User-Agent : module-lib-billing v3 (' . $productId . ')', + ]); + + return $httpClient->get('/customers/' . $shopId . '/subscriptions/' . $productId); + } +} diff --git a/classes/Http/HttpClient.php b/classes/Http/HttpClient.php new file mode 100644 index 000000000..13834bc60 --- /dev/null +++ b/classes/Http/HttpClient.php @@ -0,0 +1,245 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\PsxMarketingWithGoogle\Http; + +use RuntimeException; + +class HttpClient +{ + private $curl; + /** @var array */ + private $headers = []; + /** @var array */ + private $options = []; + /** @var string */ + private $baseUrl = ''; + + /** + * Constructor initializes cURL + * + * @param string $baseUrl Optional base URL for all requests + * + * @throws RuntimeException if cURL extension is not loaded + */ + public function __construct(string $baseUrl = '') + { + if (!extension_loaded('curl')) { + throw new RuntimeException('cURL extension is not loaded'); + } + + $this->baseUrl = rtrim($baseUrl, '/'); + $this->curl = curl_init(); + + // Set default options + $this->setDefaultOptions(); + } + + /** + * Set default cURL options + */ + private function setDefaultOptions(): void + { + $this->options = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + ]; + } + + /** + * Set custom headers for the request + * + * @param array $headers + * + * @return self + */ + public function setHeaders(array $headers): self + { + $this->headers = $headers; + + return $this; + } + + /** + * Add a single header + * + * @param string $name + * @param string $value + * + * @return self + */ + public function addHeader(string $name, string $value): self + { + $this->headers[] = "$name: $value"; + + return $this; + } + + /** + * Set custom cURL options + * + * @param array $options + * + * @return self + */ + public function setOptions(array $options): self + { + $this->options = $options + $this->options; + + return $this; + } + + /** + * Execute HTTP request + * + * @param string $method HTTP method + * @param string $url URL endpoint + * @param array|string|null $data Request data + * + * @return Response + * + * @throws RuntimeException on cURL errors + */ + public function request(string $method, string $url, $data = null) + { + $url = $this->baseUrl . '/' . ltrim($url, '/'); + + $options = $this->options + [ + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => strtoupper($method), + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => $this->headers, + ]; + + if ($data !== null) { + if (is_array($data)) { + $data = http_build_query($data); + } + + if ($method === 'GET') { + /* @phpstan-ignore-next-line */ + $options[CURLOPT_URL] .= '?' . $data; + } else { + $options[CURLOPT_POSTFIELDS] = $data; + } + } + + curl_setopt_array($this->curl, $options); + + $response = curl_exec($this->curl); + + if ($response === false) { + throw new \RuntimeException(sprintf('cURL error (%s): %s', curl_errno($this->curl), curl_error($this->curl))); + } + + $headerSize = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); + $httpCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + + // Split response into headers and body + $headerStr = substr((string) $response, 0, $headerSize); + $body = substr((string) $response, $headerSize); + $error = curl_error($this->curl); + + // Parse headers + $headers = []; + foreach (explode("\r\n", $headerStr) as $line) { + if (preg_match('/^([^:]+):(.+)$/', $line, $matches)) { + $headers[trim($matches[1])] = trim($matches[2]); + } + } + + return new Response( + $httpCode, + $body, + $headers, + $error + ); + } + + /** + * Convenience method for GET requests + * + * @param string $url + * @param array $params Query parameters + * + * @return Response + */ + public function get(string $url, array $params = []) + { + return $this->request('GET', $url, $params); + } + + /** + * Convenience method for POST requests + * + * @param string $url + * @param array|string $data + * + * @return Response + */ + public function post(string $url, $data = []) + { + return $this->request('POST', $url, $data); + } + + /** + * Convenience method for PUT requests + * + * @param string $url + * @param array|string $data + * + * @return Response + */ + public function put(string $url, $data = []) + { + return $this->request('PUT', $url, $data); + } + + /** + * Convenience method for DELETE requests + * + * @param string $url + * @param array $params + * + * @return Response + */ + public function delete(string $url, array $params = []) + { + return $this->request('DELETE', $url, $params); + } + + /** + * Destructor closes cURL connection + */ + public function __destruct() + { + if ($this->curl) { + curl_close($this->curl); + } + } +} diff --git a/classes/Http/Response.php b/classes/Http/Response.php new file mode 100644 index 000000000..85d6c35d8 --- /dev/null +++ b/classes/Http/Response.php @@ -0,0 +1,83 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\PsxMarketingWithGoogle\Http; + +class Response +{ + /** @var int */ + private $statusCode; + /** @var string */ + private $body; + /** @var array */ + private $headers; + /** @var string|null */ + private $error; + + /** + * @param int $statusCode + * @param string $body + * @param array $headers + * @param string|null $error + * + **/ + public function __construct($statusCode, $body, $headers = [], $error = null) + { + $this->statusCode = $statusCode; + $this->body = $body; + $this->headers = $headers; + $this->error = $error; + } + + /** + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * @return string|null + */ + public function getError() + { + return $this->error; + } +} diff --git a/controllers/admin/AdminPsxMktgWithGoogleModuleController.php b/controllers/admin/AdminPsxMktgWithGoogleModuleController.php index e2bf3e070..faaacf47e 100644 --- a/controllers/admin/AdminPsxMktgWithGoogleModuleController.php +++ b/controllers/admin/AdminPsxMktgWithGoogleModuleController.php @@ -18,6 +18,7 @@ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 */ +use PrestaShop\Module\PsxMarketingWithGoogle\Adapter\BillingAdapter; use PrestaShop\Module\PsxMarketingWithGoogle\Adapter\ConfigurationAdapter; use PrestaShop\Module\PsxMarketingWithGoogle\Config\Config; use PrestaShop\Module\PsxMarketingWithGoogle\Config\Env; @@ -29,7 +30,6 @@ use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; use PrestaShop\PsAccountsInstaller\Installer\Facade\PsAccounts; use PrestaShopCorp\Billing\Presenter\BillingPresenter; -use PrestaShopCorp\Billing\Services\BillingService; class AdminPsxMktgWithGoogleModuleController extends ModuleAdminController { @@ -112,10 +112,10 @@ public function initContent() // Load the context for PrestaShop Billing $billingFacade = $this->module->getService(BillingPresenter::class); - $billingService = $this->module->getService(BillingService::class); + $billingAdapter = new BillingAdapter($tokenPsAccounts); $partnerLogo = $this->module->getLocalPath() . 'logo.png'; - $currentSubscription = $billingService->getCurrentSubscription(); - + $fetchSubscriptions = $billingAdapter->getCurrentSubscription($shopIdPsAccounts, $this->module->name); + $currentSubscription = json_decode($fetchSubscriptions->getBody(), true); // PrestaShop Billing Media::addJsDef($billingFacade->present([ 'logo' => $partnerLogo, @@ -125,7 +125,7 @@ public function initContent() 'emailSupport' => 'no-reply@prestashop.com', ])); Media::addJsDef([ - 'psBillingSubscription' => (!empty($currentSubscription['success']) ? $currentSubscription['body'] : null), + 'psBillingSubscription' => $fetchSubscriptions->getStatusCode() === 200 ? $currentSubscription : null, ]); } catch (Exception $e) { $shopIdPsAccounts = null;