From 8d7b7164f8653eb4be0d0fc61b17fbdb262d0002 Mon Sep 17 00:00:00 2001 From: mandan2 <61560082+mandan2@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:29:51 +0200 Subject: [PATCH 1/2] PIPRES-319: Lock process adapter (#839) --- src/Config/Config.php | 2 + src/Exception/Code/ExceptionCode.php | 3 + src/Infrastructure/Adapter/Lock.php | 79 +++++++++++++++++++ .../Exception/CouldNotHandleLocking.php | 33 ++++++++ .../Infrastructure/Adapter/LockTest.php | 74 +++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 src/Infrastructure/Adapter/Lock.php create mode 100644 src/Infrastructure/Exception/CouldNotHandleLocking.php create mode 100644 tests/Integration/Infrastructure/Adapter/LockTest.php diff --git a/src/Config/Config.php b/src/Config/Config.php index 1509c77ba..6f348ef25 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -282,6 +282,8 @@ class Config const APPLE_PAY_DIRECT_ORDER_CREATION_MAX_WAIT_RETRIES = 10; const BANCONTACT_ORDER_CREATION_MAX_WAIT_RETRIES = 600; + public const LOCK_TIME_TO_LIVE = 60; + /** @var array */ public static $methods = [ 'banktransfer' => 'Bank', diff --git a/src/Exception/Code/ExceptionCode.php b/src/Exception/Code/ExceptionCode.php index cdd7ddb31..d7f21cef6 100644 --- a/src/Exception/Code/ExceptionCode.php +++ b/src/Exception/Code/ExceptionCode.php @@ -8,6 +8,9 @@ class ExceptionCode public const INFRASTRUCTURE_FAILED_TO_INSTALL_ORDER_STATE = 1001; public const INFRASTRUCTURE_UNKNOWN_ERROR = 1002; + public const INFRASTRUCTURE_LOCK_EXISTS = 1003; + public const INFRASTRUCTURE_LOCK_ON_ACQUIRE_IS_MISSING = 1004; + public const INFRASTRUCTURE_LOCK_ON_RELEASE_IS_MISSING = 1005; public const FAILED_TO_FIND_CUSTOMER_ADDRESS = 2001; diff --git a/src/Infrastructure/Adapter/Lock.php b/src/Infrastructure/Adapter/Lock.php new file mode 100644 index 000000000..8c3978734 --- /dev/null +++ b/src/Infrastructure/Adapter/Lock.php @@ -0,0 +1,79 @@ +lockFactory = new LockFactoryV4($store); + + return; + } + + // Symfony 3.4+ + $this->lockFactory = new LockFactoryV3($store); + } + + /** + * @throws CouldNotHandleLocking + */ + public function create(string $resource, int $ttl = Config::LOCK_TIME_TO_LIVE, bool $autoRelease = true): void + { + if ($this->lock) { + throw CouldNotHandleLocking::lockExists(); + } + + $this->lock = $this->lockFactory->createLock($resource, $ttl, $autoRelease); + } + + /** + * @throws CouldNotHandleLocking + */ + public function acquire(bool $blocking = false): bool + { + if (!$this->lock) { + throw CouldNotHandleLocking::lockOnAcquireIsMissing(); + } + + return $this->lock->acquire($blocking); + } + + /** + * @throws CouldNotHandleLocking + */ + public function release(): void + { + if (!$this->lock) { + throw CouldNotHandleLocking::lockOnReleaseIsMissing(); + } + + $this->lock->release(); + + $this->lock = null; + } + + public function __destruct() + { + try { + $this->release(); + } catch (CouldNotHandleLocking $exception) { + return; + } + } +} diff --git a/src/Infrastructure/Exception/CouldNotHandleLocking.php b/src/Infrastructure/Exception/CouldNotHandleLocking.php new file mode 100644 index 000000000..c6c23fe58 --- /dev/null +++ b/src/Infrastructure/Exception/CouldNotHandleLocking.php @@ -0,0 +1,33 @@ +getService(Lock::class); + + $lock->create('test-lock-name'); + + $this->assertTrue($lock->acquire()); + + $lock->release(); + } + + public function testItSuccessfullyLocksResourceFromAnotherProcess(): void + { + /** @var Lock $lock */ + $lock = $this->getService(Lock::class); + + $lock->create('test-lock-name'); + + $this->assertTrue($lock->acquire()); + + /** @var Lock $newLock */ + $newLock = $this->getService(Lock::class); + + $newLock->create('test-lock-name'); + + $this->assertFalse($newLock->acquire()); + } + + public function testItUnsuccessfullyCompletesLockFlowFailedToCreateLockWithMissingLock(): void + { + /** @var Lock $lock */ + $lock = $this->getService(Lock::class); + + $this->expectException(CouldNotHandleLocking::class); + $this->expectExceptionCode(ExceptionCode::INFRASTRUCTURE_LOCK_EXISTS); + + $lock->create('test-lock-name'); + $lock->create('test-lock-name'); + } + + public function testItUnsuccessfullyCompletesLockFlowFailedToAcquireLockWithMissingLock(): void + { + /** @var Lock $lock */ + $lock = $this->getService(Lock::class); + + $this->expectException(CouldNotHandleLocking::class); + $this->expectExceptionCode(ExceptionCode::INFRASTRUCTURE_LOCK_ON_ACQUIRE_IS_MISSING); + + $lock->acquire(); + } + + public function testItUnsuccessfullyCompletesLockFlowFailedToReleaseLockWithMissingLock(): void + { + /** @var Lock $lock */ + $lock = $this->getService(Lock::class); + + $this->expectException(CouldNotHandleLocking::class); + $this->expectExceptionCode(ExceptionCode::INFRASTRUCTURE_LOCK_ON_RELEASE_IS_MISSING); + + $lock->release(); + } +} From 905bd7d6afbd8b93b6c99f139439623c0de3edde Mon Sep 17 00:00:00 2001 From: mandan2 <61560082+mandan2@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:29:00 +0200 Subject: [PATCH 2/2] PIPRES-319: Webhook controller protection (#841) * PIPRES-319: Webhook controller protection * phpstan and test fix * warning fix * stan fix * webhook thrown exception improvements * removed comment * csfixer --- controllers/front/payment.php | 2 +- controllers/front/return.php | 2 +- .../front/subscriptionUpdateWebhook.php | 92 ++++++++++-- controllers/front/subscriptionWebhook.php | 72 ++++++--- controllers/front/webhook.php | 140 +++++++++++------- src/Controller/AbstractMollieController.php | 103 +++++++++++++ src/Infrastructure/Response/JsonResponse.php | 41 +++++ src/Infrastructure/Response/Response.php | 21 +++ src/Logger/PrestaLogger.php | 22 ++- tests/Unit/Factory/SubscriptionDataTest.php | 24 +-- 10 files changed, 425 insertions(+), 94 deletions(-) create mode 100644 src/Infrastructure/Response/JsonResponse.php create mode 100644 src/Infrastructure/Response/Response.php diff --git a/controllers/front/payment.php b/controllers/front/payment.php index 042842b53..62119cd9b 100644 --- a/controllers/front/payment.php +++ b/controllers/front/payment.php @@ -34,7 +34,7 @@ */ class MolliePaymentModuleFrontController extends ModuleFrontController { - const FILE_NAME = 'payment'; + private const FILE_NAME = 'payment'; /** @var bool */ public $ssl = true; diff --git a/controllers/front/return.php b/controllers/front/return.php index e9b8de776..3d8fcc59b 100644 --- a/controllers/front/return.php +++ b/controllers/front/return.php @@ -32,7 +32,7 @@ class MollieReturnModuleFrontController extends AbstractMollieController /** @var Mollie */ public $module; - const FILE_NAME = 'return'; + private const FILE_NAME = 'return'; /** @var bool */ public $ssl = true; diff --git a/controllers/front/subscriptionUpdateWebhook.php b/controllers/front/subscriptionUpdateWebhook.php index 3d62c9706..704434d0a 100644 --- a/controllers/front/subscriptionUpdateWebhook.php +++ b/controllers/front/subscriptionUpdateWebhook.php @@ -10,8 +10,12 @@ * @codingStandardsIgnoreStart */ +use Mollie\Adapter\ToolsAdapter; use Mollie\Controller\AbstractMollieController; use Mollie\Errors\Http\HttpStatusCode; +use Mollie\Handler\ErrorHandler\ErrorHandler; +use Mollie\Infrastructure\Response\JsonResponse; +use Mollie\Logger\PrestaLoggerInterface; use Mollie\Subscription\Handler\SubscriptionPaymentMethodUpdateHandler; if (!defined('_PS_VERSION_')) { @@ -20,6 +24,8 @@ class MollieSubscriptionUpdateWebhookModuleFrontController extends AbstractMollieController { + private const FILE_NAME = 'subscriptionUpdateWebhook'; + /** @var Mollie */ public $module; /** @var bool */ @@ -40,29 +46,89 @@ protected function displayMaintenancePage() public function initContent() { - if (Configuration::get(Mollie\Config\Config::MOLLIE_DEBUG_LOG)) { - PrestaShopLogger::addLog('Mollie incoming subscription webhook: ' . Tools::file_get_contents('php://input')); - } + /** @var PrestaLoggerInterface $logger */ + $logger = $this->module->getService(PrestaLoggerInterface::class); - exit($this->executeWebhook()); - } + /** @var ErrorHandler $errorHandler */ + $errorHandler = $this->module->getService(ErrorHandler::class); - protected function executeWebhook() - { - $transactionId = Tools::getValue('id'); - $subscriptionId = Tools::getValue('subscription_id'); + /** @var ToolsAdapter $tools */ + $tools = $this->module->getService(ToolsAdapter::class); + + $logger->info(sprintf('%s - Controller called', self::FILE_NAME)); + + if (!$this->module->getApiClient()) { + $logger->error(sprintf('Unauthorized in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Unauthorized', self::FILE_NAME), + HttpStatusCode::HTTP_UNAUTHORIZED + )); + } + + $transactionId = (string) $tools->getValue('id'); if (!$transactionId) { - $this->respond('failed', HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY, 'Missing transaction id'); + $logger->error(sprintf('Missing transaction id in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Missing transaction id', self::FILE_NAME), + HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY + )); } + + $subscriptionId = (string) $tools->getValue('subscription_id'); + if (!$subscriptionId) { - $this->respond('failed', HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY, 'Missing subscription id'); + $logger->error(sprintf('Missing subscription id in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Missing subscription id', self::FILE_NAME), + HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY + )); + } + + $lockResult = $this->applyLock(sprintf( + '%s-%s-%s', + self::FILE_NAME, + $transactionId, + $subscriptionId + )); + + if (!$lockResult->isSuccessful()) { + $logger->error(sprintf('Resource conflict in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Resource conflict', self::FILE_NAME), + HttpStatusCode::HTTP_CONFLICT + )); } /** @var SubscriptionPaymentMethodUpdateHandler $subscriptionPaymentMethodUpdateHandler */ $subscriptionPaymentMethodUpdateHandler = $this->module->getService(SubscriptionPaymentMethodUpdateHandler::class); - $subscriptionPaymentMethodUpdateHandler->handle($transactionId, $subscriptionId); - return 'OK'; + try { + $subscriptionPaymentMethodUpdateHandler->handle($transactionId, $subscriptionId); + } catch (\Throwable $exception) { + $logger->error('Failed to handle subscription update', [ + 'Exception message' => $exception->getMessage(), + 'Exception code' => $exception->getCode(), + ]); + + $errorHandler->handle($exception, null, false); + + $this->releaseLock(); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Failed to handle subscription update', self::FILE_NAME), + $exception->getCode() + )); + } + + $this->releaseLock(); + + $logger->info(sprintf('%s - Controller action ended', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::success([])); } } diff --git a/controllers/front/subscriptionWebhook.php b/controllers/front/subscriptionWebhook.php index 433e6b290..1c524a507 100644 --- a/controllers/front/subscriptionWebhook.php +++ b/controllers/front/subscriptionWebhook.php @@ -10,9 +10,11 @@ * @codingStandardsIgnoreStart */ +use Mollie\Adapter\ToolsAdapter; use Mollie\Controller\AbstractMollieController; use Mollie\Errors\Http\HttpStatusCode; use Mollie\Handler\ErrorHandler\ErrorHandler; +use Mollie\Infrastructure\Response\JsonResponse; use Mollie\Logger\PrestaLoggerInterface; use Mollie\Subscription\Handler\RecurringOrderHandler; @@ -22,6 +24,8 @@ class MollieSubscriptionWebhookModuleFrontController extends AbstractMollieController { + private const FILE_NAME = 'subscriptionWebhook'; + /** @var Mollie */ public $module; /** @var bool */ @@ -42,29 +46,54 @@ protected function displayMaintenancePage() public function initContent() { - if (Configuration::get(Mollie\Config\Config::MOLLIE_DEBUG_LOG)) { - PrestaShopLogger::addLog('Mollie incoming subscription webhook: ' . Tools::file_get_contents('php://input')); - } + /** @var PrestaLoggerInterface $logger */ + $logger = $this->module->getService(PrestaLoggerInterface::class); - exit($this->executeWebhook()); - } + /** @var ErrorHandler $errorHandler */ + $errorHandler = $this->module->getService(ErrorHandler::class); - protected function executeWebhook() - { - $transactionId = Tools::getValue('id'); + /** @var ToolsAdapter $tools */ + $tools = $this->module->getService(ToolsAdapter::class); + + $logger->info(sprintf('%s - Controller called', self::FILE_NAME)); + + if (!$this->module->getApiClient()) { + $logger->error(sprintf('Unauthorized in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Unauthorized', self::FILE_NAME), + HttpStatusCode::HTTP_UNAUTHORIZED + )); + } + + $transactionId = (string) $tools->getValue('id'); if (!$transactionId) { - $this->respond('failed', HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY, 'Missing transaction id'); + $logger->error(sprintf('Missing transaction id in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Missing transaction id', self::FILE_NAME), + HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY + )); } - /** @var RecurringOrderHandler $recurringOrderHandler */ - $recurringOrderHandler = $this->module->getService(RecurringOrderHandler::class); + $lockResult = $this->applyLock(sprintf( + '%s-%s', + self::FILE_NAME, + $transactionId + )); - /** @var ErrorHandler $errorHandler */ - $errorHandler = $this->module->getService(ErrorHandler::class); + if (!$lockResult->isSuccessful()) { + $logger->error(sprintf('Resource conflict in %s', self::FILE_NAME)); - /** @var PrestaLoggerInterface $logger */ - $logger = $this->module->getService(PrestaLoggerInterface::class); + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Resource conflict', self::FILE_NAME), + HttpStatusCode::HTTP_CONFLICT + )); + } + + /** @var RecurringOrderHandler $recurringOrderHandler */ + $recurringOrderHandler = $this->module->getService(RecurringOrderHandler::class); try { $recurringOrderHandler->handle($transactionId); @@ -76,9 +105,18 @@ protected function executeWebhook() $errorHandler->handle($exception, null, false); - $this->respond('failed', HttpStatusCode::HTTP_BAD_REQUEST); + $this->releaseLock(); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Failed to handle recurring order', self::FILE_NAME), + $exception->getCode() + )); } - $this->respond('OK'); + $this->releaseLock(); + + $logger->info(sprintf('%s - Controller action ended', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::success([])); } } diff --git a/controllers/front/webhook.php b/controllers/front/webhook.php index 699298883..fa2710aa3 100644 --- a/controllers/front/webhook.php +++ b/controllers/front/webhook.php @@ -10,11 +10,12 @@ * @codingStandardsIgnoreStart */ -use Mollie\Api\Exceptions\ApiException; -use Mollie\Config\Config; +use Mollie\Adapter\ToolsAdapter; use Mollie\Controller\AbstractMollieController; use Mollie\Errors\Http\HttpStatusCode; use Mollie\Handler\ErrorHandler\ErrorHandler; +use Mollie\Infrastructure\Response\JsonResponse; +use Mollie\Logger\PrestaLoggerInterface; use Mollie\Service\TransactionService; use Mollie\Utility\TransactionUtility; @@ -24,6 +25,8 @@ class MollieWebhookModuleFrontController extends AbstractMollieController { + private const FILE_NAME = 'webhook'; + /** @var Mollie */ public $module; /** @var bool */ @@ -42,82 +45,117 @@ protected function displayMaintenancePage() { } - /** - * @throws ApiException - * @throws PrestaShopDatabaseException - * @throws PrestaShopException - */ - public function initContent() + public function initContent(): void { - if ((int) Configuration::get(Config::MOLLIE_DEBUG_LOG) === Config::DEBUG_LOG_ALL) { - PrestaShopLogger::addLog('Mollie incoming webhook: ' . Tools::file_get_contents('php://input')); + /** @var PrestaLoggerInterface $logger */ + $logger = $this->module->getService(PrestaLoggerInterface::class); + + /** @var ErrorHandler $errorHandler */ + $errorHandler = $this->module->getService(ErrorHandler::class); + + /** @var ToolsAdapter $tools */ + $tools = $this->module->getService(ToolsAdapter::class); + + $logger->info(sprintf('%s - Controller called', self::FILE_NAME)); + + if (!$this->module->getApiClient()) { + $logger->error(sprintf('Unauthorized in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Unauthorized', self::FILE_NAME), + HttpStatusCode::HTTP_UNAUTHORIZED + )); + } + + $transactionId = (string) $tools->getValue('id'); + + if (!$transactionId) { + $logger->error(sprintf('Missing transaction id %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Missing transaction id', self::FILE_NAME), + HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY + )); + } + + $lockResult = $this->applyLock(sprintf( + '%s-%s', + self::FILE_NAME, + $transactionId + )); + + if (!$lockResult->isSuccessful()) { + $logger->error(sprintf('Resource conflict in %s', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Resource conflict', self::FILE_NAME), + HttpStatusCode::HTTP_CONFLICT + )); } try { - exit($this->executeWebhook()); + $this->executeWebhook($transactionId); } catch (\Throwable $exception) { - PrestaShopLogger::addLog('Error occurred: ' . $exception->getMessage(), 3, null, 'Mollie'); + $logger->error('Failed to handle webhook', [ + 'Exception message' => $exception->getMessage(), + 'Exception code' => $exception->getCode(), + ]); + + $errorHandler->handle($exception, $exception->getCode(), false); + + $this->releaseLock(); + + $this->ajaxResponse(JsonResponse::error( + $this->module->l('Failed to handle webhook', self::FILE_NAME), + $exception->getCode() + )); } + + $this->releaseLock(); + + $logger->info(sprintf('%s - Controller action ended', self::FILE_NAME)); + + $this->ajaxResponse(JsonResponse::success([])); } /** - * @return string - * - * @throws ApiException - * @throws PrestaShopDatabaseException - * @throws PrestaShopException + * @throws Throwable */ - protected function executeWebhook() + protected function executeWebhook(string $transactionId): void { /** @var TransactionService $transactionService */ $transactionService = $this->module->getService(TransactionService::class); - /** @var ErrorHandler $errorHandler */ - $errorHandler = $this->module->getService(ErrorHandler::class); + if (TransactionUtility::isOrderTransaction($transactionId)) { + $transaction = $this->module->getApiClient()->orders->get($transactionId, ['embed' => 'payments']); + } else { + $transaction = $this->module->getApiClient()->payments->get($transactionId); - $transactionId = Tools::getValue('id'); - if (!$transactionId) { - $this->respond('failed', HttpStatusCode::HTTP_UNPROCESSABLE_ENTITY, 'Missing transaction id'); + if ($transaction->orderId) { + $transaction = $this->module->getApiClient()->orders->get($transaction->orderId, ['embed' => 'payments']); + } } - if (!$this->module->getApiClient()) { - $this->respond('failed', HttpStatusCode::HTTP_UNAUTHORIZED, 'API key is missing or incorrect'); - } + $cartId = $transaction->metadata->cart_id ?? 0; - try { - if (TransactionUtility::isOrderTransaction($transactionId)) { - $transaction = $this->module->getApiClient()->orders->get($transactionId, ['embed' => 'payments']); - } else { - $transaction = $this->module->getApiClient()->payments->get($transactionId); - if ($transaction->orderId) { - $transaction = $this->module->getApiClient()->orders->get($transaction->orderId, ['embed' => 'payments']); - } - } - $metaData = $transaction->metadata; - $cartId = $metaData->cart_id ?? 0; - $this->setContext($cartId); - $payment = $transactionService->processTransaction($transaction); - } catch (\Throwable $e) { - $errorHandler->handle($e, $e->getCode(), false); - $this->respond('failed', $e->getCode(), $e->getMessage()); - } + if (!$cartId) { + // TODO webhook structure will change, no need to create custom exception for one time usage - /* @phpstan-ignore-next-line */ - if (is_string($payment)) { - return $payment; + throw new \Exception(sprintf('Missing Cart ID. Transaction ID: [%s]', $transactionId), HttpStatusCode::HTTP_NOT_FOUND); } - return 'OK'; + $this->setContext($cartId); + + $transactionService->processTransaction($transaction); } - private function setContext(int $cartId) + private function setContext(int $cartId): void { - if (!$cartId) { - return; - } $cart = new Cart($cartId); + $this->context->currency = new Currency($cart->id_currency); $this->context->customer = new Customer($cart->id_customer); + $this->context->cart = $cart; } } diff --git a/src/Controller/AbstractMollieController.php b/src/Controller/AbstractMollieController.php index 2b2a98909..11f144842 100644 --- a/src/Controller/AbstractMollieController.php +++ b/src/Controller/AbstractMollieController.php @@ -13,9 +13,28 @@ use Mollie\Errors\Error; use Mollie\Errors\Http\HttpStatusCode; +use Mollie\Infrastructure\Adapter\Lock; +use Mollie\Infrastructure\Response\JsonResponse; +use Mollie\Infrastructure\Response\Response; +use Mollie\Logger\PrestaLoggerInterface; class AbstractMollieController extends \ModuleFrontControllerCore { + private const FILE_NAME = 'AbstractMollieController'; + + /** @var Lock */ + private $lock; + + /** @var \Mollie */ + public $module; + + public function __construct() + { + parent::__construct(); + + $this->lock = $this->module->getService(Lock::class); + } + protected function respond($status, $statusCode = HttpStatusCode::HTTP_OK, $message = ''): void { http_response_code($statusCode); @@ -31,8 +50,92 @@ protected function respond($status, $statusCode = HttpStatusCode::HTTP_OK, $mess protected function ajaxRender($value = null, $controller = null, $method = null): void { + // TODO remove this later parent::ajaxRender($value, $controller, $method); exit; } + + protected function ajaxResponse($value, $controller = null, $method = null): void + { + /** @var PrestaLoggerInterface $logger */ + $logger = $this->module->getService(PrestaLoggerInterface::class); + + if ($value instanceof JsonResponse) { + if ($value->getStatusCode() === JsonResponse::HTTP_INTERNAL_SERVER_ERROR) { + $logger->error('Failed to return valid response', [ + 'context' => [ + 'response' => $value->getContent(), + ], + ]); + } + + http_response_code($value->getStatusCode()); + + $value = $value->getContent(); + } + + try { + $this->ajaxRender($value, $controller, $method); + } catch (\Throwable $exception) { + $logger->error('Could not return ajax response', [ + 'response' => json_encode($value ?: []), + 'Exception message' => $exception->getMessage(), + 'Exception code' => $exception->getCode(), + ]); + } + + exit; + } + + protected function applyLock(string $resource): Response + { + /** @var PrestaLoggerInterface $logger */ + $logger = $this->module->getService(PrestaLoggerInterface::class); + + try { + $this->lock->create($resource); + + if (!$this->lock->acquire()) { + $logger->error('Lock resource conflict', [ + 'resource' => $resource, + ]); + + return Response::respond( + $this->module->l('Resource conflict', self::FILE_NAME), + Response::HTTP_CONFLICT + ); + } + } catch (\Throwable $exception) { + $logger->error('Failed to lock process', [ + 'Exception message' => $exception->getMessage(), + 'Exception code' => $exception->getCode(), + ]); + + return Response::respond( + $this->module->l('Internal error', self::FILE_NAME), + Response::HTTP_INTERNAL_SERVER_ERROR + ); + } + + return Response::respond( + '', + Response::HTTP_OK + ); + } + + protected function releaseLock(): void + { + /** @var PrestaLoggerInterface $logger */ + $logger = $this->module->getService(PrestaLoggerInterface::class); + + try { + $this->lock->release(); + } catch (\Throwable $exception) { + $logger->error('Failed to release process', [ + 'Exception message' => $exception->getMessage(), + 'Exception code' => $exception->getCode(), + ]); + } + } } diff --git a/src/Infrastructure/Response/JsonResponse.php b/src/Infrastructure/Response/JsonResponse.php new file mode 100644 index 000000000..f568dcebf --- /dev/null +++ b/src/Infrastructure/Response/JsonResponse.php @@ -0,0 +1,41 @@ + true, + 'errors' => [], + 'data' => $data, + ], $status); + } + + /** + * @param string|array $error + */ + public static function error($error, int $status = 400): self + { + if (!is_array($error)) { + $error = [$error]; + } + + return new self([ + 'success' => false, + 'errors' => $error, + 'data' => [], + ], $status); + } +} diff --git a/src/Infrastructure/Response/Response.php b/src/Infrastructure/Response/Response.php new file mode 100644 index 000000000..a97cbbfdb --- /dev/null +++ b/src/Infrastructure/Response/Response.php @@ -0,0 +1,21 @@ +configuration = $configuration; + } + public function emergency($message, array $context = []) { throw new NotImplementedException('not implemented method'); @@ -33,11 +45,15 @@ public function critical($message, array $context = []) public function error($message, array $context = []) { + if ((int) $this->configuration->get(Config::MOLLIE_DEBUG_LOG) === Config::DEBUG_LOG_NONE) { + return; + } + $uniqueMessage = sprintf('Log ID (%s) | %s', uniqid('', true), $message); \PrestaShopLogger::addLog( $this->getMessageWithContext($uniqueMessage, $context), - 2 + 3 ); } @@ -53,6 +69,10 @@ public function notice($message, array $context = []) public function info($message, array $context = []) { + if ((int) $this->configuration->get(Config::MOLLIE_DEBUG_LOG) !== Config::DEBUG_LOG_ALL) { + return; + } + $uniqueMessage = sprintf('Log ID (%s) | %s', uniqid('', true), $message); \PrestaShopLogger::addLog( diff --git a/tests/Unit/Factory/SubscriptionDataTest.php b/tests/Unit/Factory/SubscriptionDataTest.php index edbe8e30c..88bec26bf 100644 --- a/tests/Unit/Factory/SubscriptionDataTest.php +++ b/tests/Unit/Factory/SubscriptionDataTest.php @@ -5,7 +5,7 @@ namespace Mollie\Tests\Unit\Factory; use Mollie; -use Mollie\Adapter\Link; +use Mollie\Adapter\Context; use Mollie\Repository\MolCustomerRepository; use Mollie\Repository\PaymentMethodRepository; use Mollie\Subscription\Constants\IntervalConstant; @@ -13,6 +13,7 @@ use Mollie\Subscription\DTO\Object\Amount; use Mollie\Subscription\DTO\Object\Interval; use Mollie\Subscription\Factory\CreateSubscriptionDataFactory; +use Mollie\Subscription\Provider\SubscriptionCarrierDeliveryPriceProvider; use Mollie\Subscription\Provider\SubscriptionDescriptionProvider; use Mollie\Subscription\Provider\SubscriptionIntervalProvider; use Mollie\Subscription\Repository\CombinationRepository; @@ -58,6 +59,12 @@ public function testBuildSubscriptionData(string $customerId, float $totalAmount ] ); + $context = $this->createMock(Context::class); + $context->expects($this->once())->method('getModuleLink')->willReturn('example-link'); + + $subscriptionCarrierDeliveryPriceProvider = $this->createMock(SubscriptionCarrierDeliveryPriceProvider::class); + $subscriptionCarrierDeliveryPriceProvider->expects($this->once())->method('getPrice')->willReturn(10.00); + $subscriptionDataFactory = new CreateSubscriptionDataFactory( $customerRepositoryMock, $subscriptionIntervalProviderMock, @@ -65,8 +72,9 @@ public function testBuildSubscriptionData(string $customerId, float $totalAmount $currencyAdapterMock, new CombinationRepository(), $paymentMethodRepositoryMock, - new Link(), - new Mollie() + new Mollie(), + $context, + $subscriptionCarrierDeliveryPriceProvider ); $customerMock = $this->createMock('Customer'); @@ -95,7 +103,7 @@ public function subscriptionDataProvider() { $subscriptionDto = new SubscriptionDataDTO( 'testCustomerId', - new Amount(19.99, 'EUR'), + new Amount(29.99, 'EUR'), new Interval(1, IntervalConstant::DAY), 'subscription-' . self::TEST_ORDER_REFERENCE ); @@ -113,16 +121,12 @@ public function subscriptionDataProvider() ] ); - $link = new Link(); - $subscriptionDto->setWebhookUrl($link->getModuleLink( - 'mollie', - 'subscriptionWebhook' - )); + $subscriptionDto->setWebhookUrl('example-link'); return [ 'first example' => [ 'customer id' => 'testCustomerId', - 'total paid amount' => 19.99, + 'total paid amount' => 29.99, 'description' => 'subscription-' . self::TEST_ORDER_REFERENCE, 'expected result' => $subscriptionDto, ],