From 8b075e597011a89ca2ddec6f742a73303cf4935b Mon Sep 17 00:00:00 2001 From: Volodymyr Klymenko Date: Thu, 7 Feb 2019 20:23:02 +0200 Subject: [PATCH] Publication v.1.2.1 --- Api/Data/QuoteAttemptInterface.php | 83 +++++ Api/Data/QuoteInterface.php | 133 +++++++ Api/Data/QuoteStatusInterface.php | 49 +++ Api/Quote/AttemptRepositoryInterface.php | 35 ++ Api/Quote/CancelFacadeInterface.php | 40 +++ Api/QuoteManagementInterface.php | 50 +++ Api/QuoteRepositoryInterface.php | 35 ++ .../Adminhtml/System/Config/Fieldset/Hint.php | 52 +++ .../System/Config/Fieldset/Payment.php | 192 ++++++++++ Block/Monitoring/Buttons.php | 93 +++++ Block/Monitoring/View.php | 171 +++++++++ Controller/Adminhtml/Monitoring/Cancel.php | 87 +++++ Controller/Adminhtml/Monitoring/Index.php | 59 +++ Controller/Adminhtml/Monitoring/Restart.php | 88 +++++ Controller/Adminhtml/Monitoring/View.php | 87 +++++ Controller/Payment/Callback.php | 1 - Controller/Payment/Fallback.php | 86 +++-- Cron/CancelQuoteByAttempts.php | 227 ++++++++++++ Cron/ClearQuotesHistory.php | 114 ++++++ Cron/FetchOrderFromVipps.php | 336 ++++++++---------- Gateway/Exception/ExceptionFactory.php | 2 +- Gateway/Response/InitiateHandler.php | 47 ++- Gateway/Transaction/ShippingDetails.php | 4 +- Gateway/Transaction/Transaction.php | 50 +-- Model/Adminhtml/Source/Cancellation/Type.php | 55 +++ Model/Helper/Utility.php | 42 +++ Model/Order/Cancellation/Config.php | 132 +++++++ Model/OrderPlace.php | 198 ++++++----- Model/Quote.php | 182 ++++++++++ Model/Quote/AddressUpdater.php | 27 +- Model/Quote/Attempt.php | 87 +++++ Model/Quote/AttemptManagement.php | 94 +++++ Model/Quote/AttemptRepository.php | 86 +++++ Model/Quote/CancelFacade.php | 93 +++++ Model/Quote/Command/ManualCancel.php | 85 +++++ Model/Quote/Command/ManualCancelFactory.php | 35 ++ Model/Quote/Command/Restart.php | 63 ++++ Model/Quote/Command/RestartFactory.php | 35 ++ Model/QuoteManagement.php | 120 +++++++ Model/QuoteRepository.php | 113 ++++++ Model/QuoteUpdater.php | 36 +- Model/ResourceModel/Quote.php | 46 +++ Model/ResourceModel/Quote/Attempt.php | 44 +++ .../Quote/Attempt/Collection.php | 42 +++ Model/ResourceModel/Quote/Collection.php | 42 +++ Model/ResourceModel/Quote/GridCollection.php | 59 +++ Setup/UpgradeData.php | 94 +++++ Setup/UpgradeSchema.php | 208 ++++++++++- .../{Profiling => }/Column/ShowAction.php | 9 +- Ui/Component/Column/Status.php | 72 ++++ composer.json | 2 +- etc/adminhtml/menu.xml | 43 ++- etc/adminhtml/system.xml | 21 +- etc/adminhtml/system/order_cancellation.xml | 63 ++++ etc/config.xml | 21 +- etc/crontab.xml | 6 + etc/di.xml | 45 ++- etc/extension_attributes.xml | 23 ++ etc/module.xml | 2 +- i18n/nb_NO.csv | 124 +++++-- phpcs.xml | 4 +- .../layout/vipps_monitoring_index.xml | 24 ++ .../layout/vipps_monitoring_view.xml | 32 ++ .../templates/monitoring/buttons.phtml | 77 ++++ .../adminhtml/templates/monitoring/view.phtml | 132 +++++++ .../templates/profiling/back_button.phtml | 4 +- view/adminhtml/templates/profiling/view.phtml | 55 ++- .../ui_component/vipps_monitoring.xml | 123 +++++++ .../ui_component/vipps_profiling.xml | 5 +- view/frontend/templates/button.phtml | 42 ++- 70 files changed, 4582 insertions(+), 486 deletions(-) create mode 100644 Api/Data/QuoteAttemptInterface.php create mode 100644 Api/Data/QuoteInterface.php create mode 100644 Api/Data/QuoteStatusInterface.php create mode 100644 Api/Quote/AttemptRepositoryInterface.php create mode 100644 Api/Quote/CancelFacadeInterface.php create mode 100644 Api/QuoteManagementInterface.php create mode 100644 Api/QuoteRepositoryInterface.php create mode 100644 Block/Adminhtml/System/Config/Fieldset/Hint.php create mode 100644 Block/Adminhtml/System/Config/Fieldset/Payment.php create mode 100644 Block/Monitoring/Buttons.php create mode 100644 Block/Monitoring/View.php create mode 100644 Controller/Adminhtml/Monitoring/Cancel.php create mode 100644 Controller/Adminhtml/Monitoring/Index.php create mode 100644 Controller/Adminhtml/Monitoring/Restart.php create mode 100644 Controller/Adminhtml/Monitoring/View.php create mode 100644 Cron/CancelQuoteByAttempts.php create mode 100644 Cron/ClearQuotesHistory.php create mode 100644 Model/Adminhtml/Source/Cancellation/Type.php create mode 100644 Model/Helper/Utility.php create mode 100644 Model/Order/Cancellation/Config.php create mode 100644 Model/Quote.php create mode 100644 Model/Quote/Attempt.php create mode 100644 Model/Quote/AttemptManagement.php create mode 100644 Model/Quote/AttemptRepository.php create mode 100644 Model/Quote/CancelFacade.php create mode 100644 Model/Quote/Command/ManualCancel.php create mode 100644 Model/Quote/Command/ManualCancelFactory.php create mode 100644 Model/Quote/Command/Restart.php create mode 100644 Model/Quote/Command/RestartFactory.php create mode 100644 Model/QuoteManagement.php create mode 100644 Model/QuoteRepository.php create mode 100644 Model/ResourceModel/Quote.php create mode 100644 Model/ResourceModel/Quote/Attempt.php create mode 100644 Model/ResourceModel/Quote/Attempt/Collection.php create mode 100644 Model/ResourceModel/Quote/Collection.php create mode 100644 Model/ResourceModel/Quote/GridCollection.php create mode 100644 Setup/UpgradeData.php rename Ui/Component/{Profiling => }/Column/ShowAction.php (90%) create mode 100644 Ui/Component/Column/Status.php create mode 100644 etc/adminhtml/system/order_cancellation.xml create mode 100644 etc/extension_attributes.xml create mode 100644 view/adminhtml/layout/vipps_monitoring_index.xml create mode 100644 view/adminhtml/layout/vipps_monitoring_view.xml create mode 100644 view/adminhtml/templates/monitoring/buttons.phtml create mode 100644 view/adminhtml/templates/monitoring/view.phtml create mode 100644 view/adminhtml/ui_component/vipps_monitoring.xml diff --git a/Api/Data/QuoteAttemptInterface.php b/Api/Data/QuoteAttemptInterface.php new file mode 100644 index 00000000..dd820a9d --- /dev/null +++ b/Api/Data/QuoteAttemptInterface.php @@ -0,0 +1,83 @@ +` node must be defined in `` node and contain some link. + */ +class Hint extends Template implements RendererInterface +{ + /** + * @param AbstractElement $element + * @return string + */ + public function render(AbstractElement $element) + { + $html = ''; + $label = $element->getLegend(); + $url = $element->getComment(); + + if ($label) { + $html .= sprintf('', $element->getHtmlId()); + $html .= '

'; + $html .= sprintf( + '%s', + $url, + $label + ); + $html .= '

'; + } + + return $html; + } +} diff --git a/Block/Adminhtml/System/Config/Fieldset/Payment.php b/Block/Adminhtml/System/Config/Fieldset/Payment.php new file mode 100644 index 00000000..f3231096 --- /dev/null +++ b/Block/Adminhtml/System/Config/Fieldset/Payment.php @@ -0,0 +1,192 @@ +_backendConfig = $backendConfig; + parent::__construct($context, $authSession, $jsHelper, $data); + } + + /** + * Add custom css class + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + */ + protected function _getFrontendClass($element) + { + $enabledString = $this->_isPaymentEnabled($element) ? ' enabled' : ''; + return parent::_getFrontendClass($element) . ' with-button' . $enabledString; + } + + /** + * Check whether current payment method is enabled + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return bool + */ + protected function _isPaymentEnabled($element) + { + $groupConfig = $element->getGroup(); + $activityPaths = isset($groupConfig['activity_path']) ? $groupConfig['activity_path'] : []; + + if (!is_array($activityPaths)) { + $activityPaths = [$activityPaths]; + } + + $isPaymentEnabled = false; + foreach ($activityPaths as $activityPath) { + $isPaymentEnabled = $isPaymentEnabled + || (bool)(string)$this->_backendConfig->getConfigDataValue($activityPath); + } + + return $isPaymentEnabled; + } + + /** + * Return header title part of html for payment solution + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function _getHeaderTitleHtml($element) + { + $html = '
'; + + $groupConfig = $element->getGroup(); + + $disabledAttributeString = $this->_isPaymentEnabled($element) ? '' : ' disabled="disabled"'; + $disabledClassString = $this->_isPaymentEnabled($element) ? '' : ' disabled'; + $htmlId = $element->getHtmlId(); + $html .= '
'; + + if (!empty($groupConfig['more_url'])) { + $html .= '' . __( + 'Learn More' + ) . ''; + } + if (!empty($groupConfig['demo_url'])) { + $html .= '' . __( + 'View Demo' + ) . ''; + } + + $html .= '
'; + $html .= '
' . $element->getLegend() . ''; + + if ($element->getComment()) { + $html .= '' . $element->getComment() . ''; + } + $html .= '
'; + $html .= '
'; + + return $html; + } + + /** + * Return header comment part of html for payment solution + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _getHeaderCommentHtml($element) + { + return ''; + } + + /** + * Get collapsed state on-load + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return false + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _isCollapseState($element) + { + return false; + } + + /** + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _getExtraJs($element) + { + $script = "require(['jquery', 'prototype'], function(jQuery){ + window.vippsToggleSolution = function (id, url) { + var doScroll = false; + Fieldset.toggleCollapse(id, url); + if ($(this).hasClassName(\"open\")) { + $$(\".with-button button.button\").each(function(anotherButton) { + if (anotherButton != this && $(anotherButton).hasClassName(\"open\")) { + $(anotherButton).click(); + doScroll = true; + } + }.bind(this)); + } + if (doScroll) { + var pos = Element.cumulativeOffset($(this)); + window.scrollTo(pos[0], pos[1] - 45); + } + } + });"; + + return $this->_jsHelper->getScript($script); + } +} diff --git a/Block/Monitoring/Buttons.php b/Block/Monitoring/Buttons.php new file mode 100644 index 00000000..0b83d605 --- /dev/null +++ b/Block/Monitoring/Buttons.php @@ -0,0 +1,93 @@ +restartFactory = $restartFactory; + $this->registry = $registry; + $this->cancelFactory = $cancelFactory; + } + + /** + * @return bool + */ + public function isRestartVisible() + { + $restart = $this->restartFactory->create($this->getVippsQuote()); + + return $restart->isAllowed(); + } + + /** + * @return \Vipps\Payment\Api\Data\QuoteInterface + */ + public function getVippsQuote() + { + return $this->registry->registry('vipps_quote'); + } + + /** + * @return bool + */ + public function isCancelVisible() + { + $cancel = $this->cancelFactory->create($this->getVippsQuote()); + + return $cancel->isAllowed(); + } +} diff --git a/Block/Monitoring/View.php b/Block/Monitoring/View.php new file mode 100644 index 00000000..58fd3880 --- /dev/null +++ b/Block/Monitoring/View.php @@ -0,0 +1,171 @@ +vippsQuoteRepository = $vippsQuoteRepository; + $this->quoteRepository = $quoteRepository; + $this->registry = $registry; + $this->priceHelper = $priceHelper; + $this->attemptRepository = $attemptRepository; + $this->status = $status; + } + + /** + * @return Data + */ + public function getPriceHelper() + { + return $this->priceHelper; + } + + /** + * @return \Magento\Quote\Api\Data\CartInterface + */ + public function getQuote() + { + try { + return $this->quoteRepository->get($this->getVippsQuote()->getQuoteId()); + } catch (\Exception $e) { + $this->quoteLoadingError = $e->getMessage(); + } + } + + /** + * @return \Vipps\Payment\Api\Data\QuoteInterface + */ + public function getVippsQuote() + { + return $this->registry->registry('vipps_quote'); + } + + /** + * Quote loading error. + * + * @return string + */ + public function getQuoteLoadingError() + { + return $this->quoteLoadingError; + } + + /** + * @return \Vipps\Payment\Model\ResourceModel\Quote\Attempt\Collection + */ + public function getAttempts() + { + return $this + ->attemptRepository + ->getByVippsQuote($this->getVippsQuote()) + ->load(); + } + + /** + * @param string $code + * @return string + */ + public function getStatusLabel($code) + { + return $this->status->getLabel($code); + } + + /** + * Retrieve formatting date + * + * @param null|string|\DateTimeInterface $date + * @param int $format + * @param bool $showTime + * @param null|string $timezone + * @return string + */ + public function formatDate( //@codingStandardsIgnoreLine + $date = null, + $format = \IntlDateFormatter::MEDIUM, + $showTime = true, + $timezone = null + ) { + return parent::formatDate($date, $format, $showTime, $timezone); + } +} diff --git a/Controller/Adminhtml/Monitoring/Cancel.php b/Controller/Adminhtml/Monitoring/Cancel.php new file mode 100644 index 00000000..8e254067 --- /dev/null +++ b/Controller/Adminhtml/Monitoring/Cancel.php @@ -0,0 +1,87 @@ +quoteRepository = $quoteRepository; + $this->manualCancelFactory = $manualCancelFactory; + } + + /** + * @return ResponseInterface|ResultInterface|Page + */ + public function execute() + { + try { + $this + ->getManualCancelCommand() + ->execute(); + } catch (\Throwable $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + + return $this + ->resultRedirectFactory + ->create() + ->setUrl($this->_redirect->getRefererUrl()); + } + + /** + * @return \Vipps\Payment\Model\Quote\Command\ManualCancel + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getManualCancelCommand() + { + $vippsQuote = $this + ->quoteRepository + ->load($this->getRequest()->getParam('entity_id')); + + return $this->manualCancelFactory->create($vippsQuote); + } +} diff --git a/Controller/Adminhtml/Monitoring/Index.php b/Controller/Adminhtml/Monitoring/Index.php new file mode 100644 index 00000000..905139a9 --- /dev/null +++ b/Controller/Adminhtml/Monitoring/Index.php @@ -0,0 +1,59 @@ +resultPageFactory = $resultPageFactory; + } + + /** + * Display grid. + * + * @return Page + */ + public function execute() + { + /** @var Page $resultPage */ + $resultPage = $this->resultPageFactory->create(); + $resultPage->setActiveMenu('Vipps_Payment::vipps_monitoring'); + $resultPage->getConfig()->getTitle()->prepend(__('Quote Monitoring')); + return $resultPage; + } +} diff --git a/Controller/Adminhtml/Monitoring/Restart.php b/Controller/Adminhtml/Monitoring/Restart.php new file mode 100644 index 00000000..00936d40 --- /dev/null +++ b/Controller/Adminhtml/Monitoring/Restart.php @@ -0,0 +1,88 @@ +quoteRepository = $quoteRepository; + $this->restartFactory = $restartFactory; + } + + /** + * @return ResponseInterface|ResultInterface|Page + */ + public function execute() + { + try { + $this + ->getRestart() + ->execute(); + } catch (\Throwable $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + + return $this + ->resultRedirectFactory + ->create() + ->setUrl($this->_redirect->getRefererUrl()); + } + + /** + * @return \Vipps\Payment\Model\Quote\Command\Restart + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getRestart() + { + $vippsQuote = $this + ->quoteRepository + ->load($this->getRequest()->getParam('entity_id')); + + return $this->restartFactory->create($vippsQuote); + } +} diff --git a/Controller/Adminhtml/Monitoring/View.php b/Controller/Adminhtml/Monitoring/View.php new file mode 100644 index 00000000..becceea4 --- /dev/null +++ b/Controller/Adminhtml/Monitoring/View.php @@ -0,0 +1,87 @@ +resultPageFactory = $resultPageFactory; + $this->quoteRepository = $quoteRepository; + $this->registry = $registry; + } + + /** + * @return ResponseInterface|ResultInterface|Page + */ + public function execute() + { + try { + $vippsQuote = $this->quoteRepository->load($this->getRequest()->getParam('entity_id')); + $this->registry->register('vipps_quote', $vippsQuote); + } catch (\Throwable $e) { + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $this->messageManager->addErrorMessage($e->getMessage()); + $resultRedirect = $this->resultRedirectFactory->create(); + $resultRedirect->setPath('*/*'); + + return $resultRedirect; + } + + $resultPage = $this->resultPageFactory->create(); + $resultPage->setActiveMenu('Vipps_Payment::vipps_monitoring'); + return $resultPage; + } +} diff --git a/Controller/Payment/Callback.php b/Controller/Payment/Callback.php index 4196f8d9..dac35ec2 100755 --- a/Controller/Payment/Callback.php +++ b/Controller/Payment/Callback.php @@ -118,7 +118,6 @@ public function execute() $requestData = $this->jsonDecoder->unserialize($this->getRequest()->getContent()); $this->authorize($requestData); - $transaction = $this->transactionBuilder->setData($requestData)->build(); $this->orderPlace->execute($this->getQuote($requestData), $transaction); diff --git a/Controller/Payment/Fallback.php b/Controller/Payment/Fallback.php index 61803929..cdf4cc04 100755 --- a/Controller/Payment/Fallback.php +++ b/Controller/Payment/Fallback.php @@ -13,26 +13,38 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ + namespace Vipps\Payment\Controller\Payment; -use Magento\Framework\{ - Controller\ResultFactory, Controller\ResultInterface, Exception\CouldNotSaveException, - Exception\LocalizedException, Exception\NoSuchEntityException, Exception\InputException, - Session\SessionManagerInterface, Controller\Result\Redirect, App\Action\Action, App\Action\Context, - App\ResponseInterface -}; -use Vipps\Payment\{ - Api\CommandManagerInterface, Gateway\Exception\MerchantException, Gateway\Request\Initiate\MerchantDataBuilder, - Model\OrderLocator, Model\OrderPlace, Gateway\Transaction\TransactionBuilder, Model\QuoteLocator, - Model\Gdpr\Compliance -}; -use Magento\Quote\{ - Api\Data\CartInterface, Api\CartRepositoryInterface, Model\Quote -}; use Magento\Checkout\Model\Session; +use Magento\Framework\{App\Action\Action, + App\Action\Context, + App\ResponseInterface, + Controller\Result\Redirect, + Controller\ResultFactory, + Controller\ResultInterface, + Exception\CouldNotSaveException, + Exception\InputException, + Exception\LocalizedException, + Exception\NoSuchEntityException, + Session\SessionManagerInterface}; +use Magento\Quote\{Api\CartRepositoryInterface, Api\Data\CartInterface, Model\Quote}; use Magento\Sales\Api\Data\OrderInterface; -use Zend\Http\Response as ZendResponse; use Psr\Log\LoggerInterface; +use Vipps\Payment\{Api\CommandManagerInterface, + Api\Data\QuoteStatusInterface, + Gateway\Exception\MerchantException, + Gateway\Request\Initiate\MerchantDataBuilder, + Gateway\Transaction\TransactionBuilder, + Model\Gdpr\Compliance, + Model\OrderLocator, + Model\OrderPlace, + Model\Quote as VippsQuote, + Model\Quote\Attempt, + Model\Quote\AttemptManagement, + Model\QuoteLocator, + Model\QuoteManagement}; +use Zend\Http\Response as ZendResponse; /** * Class Fallback @@ -95,6 +107,14 @@ class Fallback extends Action * @var Compliance */ private $gdprCompliance; + /** + * @var QuoteManagement + */ + private $vippsQuoteManagement; + /** + * @var AttemptManagement + */ + private $attemptManagement; /** * Fallback constructor. @@ -106,6 +126,8 @@ class Fallback extends Action * @param OrderPlace $orderPlace * @param CartRepositoryInterface $cartRepository * @param QuoteLocator $quoteLocator + * @param QuoteManagement $vippsQuoteManagement + * @param AttemptManagement $attemptManagement * @param OrderLocator $orderLocator * @param Compliance $compliance * @param LoggerInterface $logger @@ -120,6 +142,8 @@ public function __construct( OrderPlace $orderPlace, CartRepositoryInterface $cartRepository, QuoteLocator $quoteLocator, + QuoteManagement $vippsQuoteManagement, + AttemptManagement $attemptManagement, OrderLocator $orderLocator, Compliance $compliance, LoggerInterface $logger @@ -134,10 +158,13 @@ public function __construct( $this->orderLocator = $orderLocator; $this->logger = $logger; $this->gdprCompliance = $compliance; + $this->vippsQuoteManagement = $vippsQuoteManagement; + $this->attemptManagement = $attemptManagement; } /** * @return ResponseInterface|Redirect|ResultInterface + * @throws CouldNotSaveException */ public function execute() { @@ -148,25 +175,36 @@ public function execute() $quote = $this->getQuote(); $order = $this->getOrder(); - + $vippsQuote = $this->vippsQuoteManagement->getByQuote($quote); + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_PROCESSING); + $attempt = $this->attemptManagement->createAttempt($vippsQuote); if (!$order) { - $order = $this->placeOrder($quote); + $order = $this->placeOrder($quote, $vippsQuote, $attempt); } - + $attemptMessage = __('Placed'); + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_PLACED); $this->updateCheckoutSession($quote, $order); /** @var ZendResponse $result */ $resultRedirect->setPath('checkout/onepage/success', ['_secure' => true]); } catch (LocalizedException $e) { $this->logger->critical($e->getMessage()); $this->messageManager->addErrorMessage($e->getMessage()); + $attemptMessage = $e->getMessage(); $resultRedirect->setPath('checkout/onepage/failure', ['_secure' => true]); } catch (\Exception $e) { + $attemptMessage = $e->getMessage(); $this->logger->critical($e->getMessage()); $this->messageManager->addErrorMessage(__('An error occurred during payment status update.')); $resultRedirect->setPath('checkout/onepage/failure', ['_secure' => true]); } finally { $compliant = $this->gdprCompliance->process($this->getRequest()->getRequestString()); $this->logger->debug($compliant); + + if (isset($attempt)) { + $attempt->setMessage($attemptMessage); + $this->attemptManagement->save($attempt); + $this->vippsQuoteManagement->save($vippsQuote); + } } return $resultRedirect; } @@ -239,13 +277,19 @@ private function getOrder() /** * @param CartInterface $quote * + * @param VippsQuote $vippsQuote + * @param Attempt $attempt * @return OrderInterface|null * @throws CouldNotSaveException + * @throws InputException * @throws LocalizedException + * @throws MerchantException * @throws NoSuchEntityException - * @throws InputException + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Vipps\Payment\Gateway\Exception\VippsException + * @throws \Vipps\Payment\Gateway\Exception\WrongAmountException */ - private function placeOrder(CartInterface $quote) + private function placeOrder(CartInterface $quote, VippsQuote $vippsQuote, Attempt $attempt) { try { $response = $this->commandManager->getOrderStatus( @@ -253,6 +297,8 @@ private function placeOrder(CartInterface $quote) ); $transaction = $this->transactionBuilder->setData($response)->build(); if ($transaction->isTransactionAborted()) { + $attempt->setMessage('Transaction was cancelled in Vipps'); + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_CANCELED); $this->restoreQuote(); } $order = $this->orderPlace->execute($quote, $transaction); diff --git a/Cron/CancelQuoteByAttempts.php b/Cron/CancelQuoteByAttempts.php new file mode 100644 index 00000000..8ceb8457 --- /dev/null +++ b/Cron/CancelQuoteByAttempts.php @@ -0,0 +1,227 @@ +logger = $logger; + $this->storeManager = $storeManager; + $this->scopeCodeResolver = $scopeCodeResolver; + $this->cancellationConfig = $cancellationConfig; + $this->cancellationFacade = $cancellationFacade; + $this->vippsQuoteCollectionFactory = $vippsQuoteCollectionFactory; + $this->cartRepository = $cartRepository; + $this->attemptManagement = $attemptManagement; + } + + /** + * Create orders from Vipps that are not created in Magento yet + * + * @throws CouldNotSaveException + */ + public function execute() + { + try { + $currentStore = $this->storeManager->getStore()->getId(); + $currentPage = 1; + do { + $quoteCollection = $this->createCollection($currentPage); + $this->logger->debug('Fetched quote collection to cancel'); + foreach ($quoteCollection as $quote) { + $this->processQuote($quote); + usleep(1000000); //delay for 1 second + } + $currentPage++; + } while ($currentPage <= $quoteCollection->getLastPageNumber()); + } finally { + $this->storeManager->setCurrentStore($currentStore); + } + } + + /** + * Get vipps quote collection to cancel. + * Conditions are: + * number of attempts greater than allowed + * + * @param $currentPage + * + * @return VippsQuoteCollection + */ + private function createCollection($currentPage) + { + /** @var VippsQuoteCollection $collection */ + $collection = $this->vippsQuoteCollectionFactory->create(); + + $collection + ->setPageSize(self::COLLECTION_PAGE_SIZE) + ->setCurPage($currentPage) + ->addFieldToFilter( + 'attempts', + ['gteq' => $this->cancellationConfig->getAttemptsMaxCount()] + ); + + // Filter processing cancelled quotes. + $collection->addFieldToFilter( + QuoteStatusInterface::FIELD_STATUS, + ['in' => [ + QuoteStatusInterface::STATUS_NEW, + QuoteStatusInterface::STATUS_PLACE_FAILED, + QuoteStatusInterface::STATUS_PROCESSING + ]] + ); + + return $collection; + } + + /** + * Main process + * + * @param QuoteInterface $vippsQuote + * + * @throws CouldNotSaveException + */ + private function processQuote(QuoteInterface $vippsQuote) + { + $this->logger->info('Start quote cancelling', ['vipps_quote_id' => $vippsQuote->getId()]); + + try { + $this->prepareEnv($vippsQuote); + + if ($this->cancellationConfig->isAutomatic($vippsQuote->getStoreId())) { + $quote = $this->cartRepository->get($vippsQuote->getQuoteId()); + + $attempt = $this->attemptManagement->createAttempt($vippsQuote, true); + + $attempt + ->setMessage(__( + 'Max number of attempts reached (%1)', + $this->cancellationConfig->getAttemptsMaxCount() + )); + + $this + ->cancellationFacade + ->cancel($vippsQuote, $quote); + } + } catch (\Throwable $e) { + $this->logger->critical($e->getMessage(), ['quote_id' => $vippsQuote->getId()]); + + if (isset($attempt)) { + $attempt->setMessage($e->getMessage()); + $this->attemptManagement->save($attempt); + } + } + } + + /** + * Prepare environment. + * + * @param QuoteInterface $quote + */ + private function prepareEnv(QuoteInterface $quote) + { + // set quote store as current store + $this->scopeCodeResolver->clean(); + $this->storeManager->setCurrentStore($quote->getStoreId()); + } +} diff --git a/Cron/ClearQuotesHistory.php b/Cron/ClearQuotesHistory.php new file mode 100644 index 00000000..f4e738ee --- /dev/null +++ b/Cron/ClearQuotesHistory.php @@ -0,0 +1,114 @@ +logger = $logger; + $this->cancellationConfig = $cancellationConfig; + $this->vippsQuoteCollectionFactory = $vippsQuoteCollectionFactory; + $this->dateTimeFactory = $dateTimeFactory; + } + + /** + * Clear old vipps quote history. + */ + public function execute() + { + $days = $this->cancellationConfig->getQuoteStoragePeriod(); + + if (!$days) { + $this->logger->debug('No days interval installed to remove quotes information'); + return; + } + + $dateRemoveTo = $this->dateTimeFactory->create(); + + try { + $dateRemoveTo->sub(new \DateInterval("P{$days}D")); //@codingStandardsIgnoreLine + $dateTimeFormatted = $dateRemoveTo->format(Mysql::DATETIME_FORMAT); + + $this->logger->debug('Remove quotes information till ' . $dateTimeFormatted); + + /** @var VippsQuoteCollection $collection */ + $collection = $this->vippsQuoteCollectionFactory->create(); + + $collection->addFieldToFilter('updated_at', ['lt' => $dateTimeFormatted]); + + $query = $collection + ->getSelect() + ->deleteFromSelect('main_table'); + + $collection->getConnection()->query($query); //@codingStandardsIgnoreLine + + $this->logger->debug('Deleted records: ' . $query); + } catch (\Throwable $exception) { + $this->logger->error($exception->getMessage()); + } + } +} diff --git a/Cron/FetchOrderFromVipps.php b/Cron/FetchOrderFromVipps.php index 53c249dd..75d422aa 100644 --- a/Cron/FetchOrderFromVipps.php +++ b/Cron/FetchOrderFromVipps.php @@ -13,24 +13,31 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ + namespace Vipps\Payment\Cron; -use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Config\ScopeCodeResolver; -use Magento\Framework\Exception\{CouldNotSaveException, NoSuchEntityException, AlreadyExistsException, InputException}; -use Magento\Quote\Api\{CartRepositoryInterface, Data\CartInterface}; -use Magento\Quote\Model\{ResourceModel\Quote\Collection, ResourceModel\Quote\CollectionFactory, Quote}; +use Magento\Framework\Exception\{AlreadyExistsException, CouldNotSaveException, InputException, NoSuchEntityException}; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Quote\Api\{Data\CartInterface}; +use Magento\Quote\Model\{QuoteRepository, ResourceModel\Quote\CollectionFactory}; use Magento\Sales\Api\Data\OrderInterface; -use Vipps\Payment\{ - Api\CommandManagerInterface, +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; +use Vipps\Payment\{Api\CommandManagerInterface, + Api\Data\QuoteStatusInterface, Gateway\Exception\VippsException, Gateway\Transaction\Transaction, + Gateway\Transaction\TransactionBuilder, + Model\Order\Cancellation\Config, Model\OrderPlace, - Gateway\Transaction\TransactionBuilder -}; + Model\Quote as VippsQuote, + Model\Quote\AttemptManagement, + Model\QuoteRepository as VippsQuoteRepository, + Model\ResourceModel\Quote\Collection as VippsQuoteCollection, + Model\ResourceModel\Quote\CollectionFactory as VippsQuoteCollectionFactory}; use Vipps\Payment\Gateway\Exception\WrongAmountException; -use Psr\Log\LoggerInterface; -use Magento\Framework\Exception\LocalizedException; /** * Class FetchOrderStatus @@ -44,16 +51,6 @@ class FetchOrderFromVipps */ const COLLECTION_PAGE_SIZE = 100; - /** - * @var string - */ - const MAX_NUMBER_OF_ATTEMPTS = 3; - - /** - * @var CollectionFactory - */ - private $quoteCollectionFactory; - /** * @var CommandManagerInterface */ @@ -74,11 +71,6 @@ class FetchOrderFromVipps */ private $logger; - /** - * @var CartRepositoryInterface - */ - private $cartRepository; - /** * @var StoreManagerInterface */ @@ -89,43 +81,84 @@ class FetchOrderFromVipps */ private $scopeCodeResolver; + /** + * @var Config + */ + private $cancellationConfig; + + /** + * @var AttemptManagement + */ + private $attemptManagement; + + /** + * @var VippsQuoteCollectionFactory + */ + private $vippsQuoteCollectionFactory; + + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var VippsQuoteRepository + */ + private $vippsQuoteRepository; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * FetchOrderFromVipps constructor. - * * @param CollectionFactory $quoteCollectionFactory + * @param VippsQuoteCollectionFactory $vippsQuoteCollectionFactory + * @param VippsQuoteRepository $vippsQuoteRepository + * @param QuoteRepository $quoteRepository * @param CommandManagerInterface $commandManager * @param TransactionBuilder $transactionBuilder * @param OrderPlace $orderManagement - * @param CartRepositoryInterface $cartRepository * @param LoggerInterface $logger * @param StoreManagerInterface $storeManager * @param ScopeCodeResolver $scopeCodeResolver + * @param Config $cancellationConfig + * @param DateTimeFactory $dateTimeFactory + * @param AttemptManagement $attemptManagement */ public function __construct( - CollectionFactory $quoteCollectionFactory, + VippsQuoteCollectionFactory $vippsQuoteCollectionFactory, + VippsQuoteRepository $vippsQuoteRepository, + QuoteRepository $quoteRepository, CommandManagerInterface $commandManager, TransactionBuilder $transactionBuilder, OrderPlace $orderManagement, - CartRepositoryInterface $cartRepository, LoggerInterface $logger, StoreManagerInterface $storeManager, - ScopeCodeResolver $scopeCodeResolver + ScopeCodeResolver $scopeCodeResolver, + Config $cancellationConfig, + DateTimeFactory $dateTimeFactory, + AttemptManagement $attemptManagement ) { - $this->quoteCollectionFactory = $quoteCollectionFactory; $this->commandManager = $commandManager; $this->transactionBuilder = $transactionBuilder; $this->orderPlace = $orderManagement; - $this->cartRepository = $cartRepository; $this->logger = $logger; $this->storeManager = $storeManager; $this->scopeCodeResolver = $scopeCodeResolver; + $this->cancellationConfig = $cancellationConfig; + $this->attemptManagement = $attemptManagement; + $this->vippsQuoteCollectionFactory = $vippsQuoteCollectionFactory; + $this->quoteRepository = $quoteRepository; + $this->vippsQuoteRepository = $vippsQuoteRepository; + $this->dateTimeFactory = $dateTimeFactory; } /** * Create orders from Vipps that are not created in Magento yet * - * @throws NoSuchEntityException - * @throws \Exception + * @throws CouldNotSaveException */ public function execute() { @@ -133,128 +166,108 @@ public function execute() $currentStore = $this->storeManager->getStore()->getId(); $currentPage = 1; do { - $quoteCollection = $this->createCollection($currentPage); - $this->logger->debug(sprintf( - 'Fetched payment details, page: "%s", quotes: "%s"', - $currentPage, - $quoteCollection->count() //@codingStandardsIgnoreLine - )); - foreach ($quoteCollection as $quote) { - $this->processQuote($quote); + $vippsQuoteCollection = $this->createCollection($currentPage); + $this->logger->debug('Fetched payment details'); + /** @var VippsQuote $vippsQuote */ + foreach ($vippsQuoteCollection as $vippsQuote) { + $this->processQuote($vippsQuote); usleep(1000000); //delay for 1 second } $currentPage++; - } while ($currentPage <= $quoteCollection->getLastPageNumber()); + } while ($currentPage <= $vippsQuoteCollection->getLastPageNumber()); } finally { $this->storeManager->setCurrentStore($currentStore); } } /** - * Main process - * - * @param Quote $quote + * @param $currentPage * - * @throws NoSuchEntityException - * @throws \Exception + * @return VippsQuoteCollection */ - private function processQuote(Quote $quote) + private function createCollection($currentPage) { - try { - $order = null; - $transaction = null; - $currentException = null; + /** @var VippsQuoteCollection $collection */ + $collection = $this->vippsQuoteCollectionFactory->create(); + + $collection + ->setPageSize(self::COLLECTION_PAGE_SIZE) + ->setCurPage($currentPage) + ->addFieldToFilter( + 'attempts', + [ + ['lt' => $this->cancellationConfig->getAttemptsMaxCount()], + ['null' => 1] + ] + ) + ->addFieldToFilter( + QuoteStatusInterface::FIELD_STATUS, + ['in' => [QuoteStatusInterface::STATUS_NEW, QuoteStatusInterface::STATUS_PROCESSING]] + ); // Filter new and place failed quotes. - $this->prepareEnv($quote); + return $collection; + } - $transaction = $this->fetchOrderStatus($quote->getReservedOrderId()); + /** + * @param VippsQuote $vippsQuote + * @throws CouldNotSaveException + */ + private function processQuote(VippsQuote $vippsQuote) + { + $vippsQuoteStatus = QuoteStatusInterface::STATUS_PROCESSING; + $attemptMessage = __('Waiting while customer accept payment'); + + try { + // Register new attempt. + $attempt = $this->attemptManagement->createAttempt($vippsQuote); + $this->prepareEnv($vippsQuote); + // Get Magento Quote for processing. + $quote = $this->quoteRepository->get($vippsQuote->getQuoteId()); + $transaction = $this->fetchOrderStatus($vippsQuote->getReservedOrderId()); if ($transaction->isTransactionAborted()) { - $this->cancelQuote($quote, $transaction, 'canceled on vipps side'); + $attemptMessage = __('Transaction was cancelled in Vipps'); + $vippsQuoteStatus = QuoteStatusInterface::STATUS_CANCELED; } else { $order = $this->placeOrder($quote, $transaction); + if ($order) { + $vippsQuoteStatus = QuoteStatusInterface::STATUS_PLACED; + $attemptMessage = __('Placed'); + } + + if ($transaction->isInitiate() && $this->isQuoteExpired($vippsQuote)) { + $vippsQuoteStatus = QuoteStatusInterface::STATUS_EXPIRED; + $attemptMessage = __('Transaction has been expired'); + } } } catch (\Throwable $e) { - $currentException = $e; //@codingStandardsIgnoreLine - $this->logger->critical($e->getMessage() . ', quote id = ' . $quote->getId()); + $vippsQuoteStatus = $this->isMaxAttemptsReached($vippsQuote) + ? QuoteStatusInterface::STATUS_PLACE_FAILED : QuoteStatusInterface::STATUS_PROCESSING; + + $this->logger->critical($e->getMessage(), ['vipps_quote_id' => $vippsQuote->getId()]); + $attemptMessage = $e->getMessage(); } finally { - if ($order) { - // if order exists - nothing to do, all good - return; - } - /** @var Quote $quote */ - $quote = $this->cartRepository->get($quote->getEntityId()); - if (!$quote->getReservedOrderId()) { - // if quote does not have reserved order id - such quote will not be processed next time - return; - } + $vippsQuote->setStatus($vippsQuoteStatus); + $this->vippsQuoteRepository->save($vippsQuote); - // count not success (order not created) attempts of this process - if ($this->countAttempts($quote) >= $this->getMaxNumberOfAttempts()) { - $this->cancelQuote( - $quote, - $transaction, - sprintf( - 'canceled after "%s" attempts, last error "%s"', - $this->getMaxNumberOfAttempts(), - $currentException ? $currentException->getMessage() : 'n/a' - ) - ); - return; + if (isset($attempt)) { + $attempt->setMessage($attemptMessage); + $this->attemptManagement->save($attempt); } } } /** - * @param Quote|CartInterface $quote + * Prepare environment. * - * @return int - * @throws LocalizedException + * @param VippsQuote $quote */ - private function countAttempts($quote) - { - $additionalInfo = $quote->getPayment()->getAdditionalInformation(); - $attempts = (int)($additionalInfo['vipps']['attempts'] ?? 0); - - $additionalInfo['vipps']['attempts'] = ++$attempts; - $quote->getPayment()->setAdditionalInformation($additionalInfo); - - if ($attempts < $this->getMaxNumberOfAttempts()) { - $this->cartRepository->save($quote); - } - - return $attempts; - } - - /** - * @return int - */ - private function getMaxNumberOfAttempts() - { - return (int)self::MAX_NUMBER_OF_ATTEMPTS; - } - - /** - * @param Quote $quote - */ - private function prepareEnv(Quote $quote) + private function prepareEnv(VippsQuote $quote) { // set quote store as current store $this->scopeCodeResolver->clean(); - $this->storeManager->setCurrentStore($quote->getStore()->getId()); - } - /** - * @param Quote $quote - * @param \DateInterval $interval - * - * @return bool - */ - private function isQuoteExpired(Quote $quote, \DateInterval $interval) //@codingStandardsIgnoreLine - { - $quoteExpiredAt = (new \DateTime($quote->getUpdatedAt()))->add($interval); //@codingStandardsIgnoreLine - $isQuoteExpired = !$quoteExpiredAt->diff(new \DateTime())->invert; //@codingStandardsIgnoreLine - return $isQuoteExpired; + $this->storeManager->setCurrentStore($quote->getStoreId()); } /** @@ -298,76 +311,31 @@ private function placeOrder(CartInterface $quote, Transaction $transaction) } /** - * Cancel quote by setting reserved_order_id to null - * - * @param CartInterface|Quote $quote - * @param Transaction|null $transaction - * @param null $info + * Validate Vipps Quote expiration. * - * @throws LocalizedException + * @param $vippsQuote + * @return bool + * @throws \Exception */ - private function cancelQuote(CartInterface $quote, Transaction $transaction = null, $info = null) + private function isQuoteExpired($vippsQuote) { - $savedQuote = clone $quote; - $quote->setReservedOrderId(null); - - $additionalInformation = []; - if ($info instanceof \Exception) { - $additionalInformation = [ - 'cancel_reason_code' => $info->getCode(), - 'cancel_reason_phrase' => $info->getMessage() - ]; - } elseif (\is_string($info)) { - $additionalInformation['cancel_reason_phrase'] = $info; - } + $createdAt = $this->dateTimeFactory->create($vippsQuote->getCreatedAt()); - $additionalInformation = array_merge( - $additionalInformation, - [ - 'reserved_order_id' => $savedQuote->getReservedOrderId() - ] - ); - $payment = $quote->getPayment(); - $existingAdditionalInfo = $payment->getAdditionalInformation()['vipps'] ?? []; - $payment->setAdditionalInformation('vipps', array_merge($existingAdditionalInfo, $additionalInformation)); - - $this->cartRepository->save($quote); - - $this->logger->debug(sprintf( - 'Quote was canceled, id: "%s", reserved_order_id: "%s", cancel reason "%s"', - $quote->getId(), - $savedQuote->getReservedOrderId(), - $additionalInformation['cancel_reason_phrase'] - )); - - // cancel order on vipps side - if ($transaction && $transaction->isTransactionReserved()) { - $this->commandManager->cancel($savedQuote->getPayment()); - } + $interval = new \DateInterval("PT{$this->cancellationConfig->getInactivityTime()}M"); //@codingStandardsIgnoreLine + + $createdAt->add($interval); + + return !$createdAt->diff($this->dateTimeFactory->create())->invert; } /** - * @param $currentPage + * Check for attempts count. * - * @return Collection + * @param VippsQuote $vippsQuote + * @return bool */ - private function createCollection($currentPage) + private function isMaxAttemptsReached(VippsQuote $vippsQuote) { - /** @var Collection $collection */ - $collection = $this->quoteCollectionFactory->create(); - - $collection->setPageSize(self::COLLECTION_PAGE_SIZE); - $collection->setCurPage($currentPage); - $collection->addFieldToSelect(['entity_id', 'reserved_order_id', 'store_id', 'updated_at']); //@codingStandardsIgnoreLine - $collection->join( - ['p' => $collection->getTable('quote_payment')], - 'main_table.entity_id = p.quote_id', - ['p.method'] - ); - $collection->addFieldToFilter('p.method', ['eq' => 'vipps']); - $collection->addFieldToFilter('main_table.is_active', ['in' => ['0']]); - $collection->addFieldToFilter('main_table.updated_at', ['to' => date("Y-m-d H:i:s", time() - 300)]); // 5min - $collection->addFieldToFilter('main_table.reserved_order_id', ['neq' => '']); - return $collection; + return $vippsQuote->getAttempts() >= $this->cancellationConfig->getAttemptsMaxCount(); } } diff --git a/Gateway/Exception/ExceptionFactory.php b/Gateway/Exception/ExceptionFactory.php index de06ed0a..6ef3d126 100644 --- a/Gateway/Exception/ExceptionFactory.php +++ b/Gateway/Exception/ExceptionFactory.php @@ -73,7 +73,7 @@ public function create($errorCode, $errorMessage) 'code' => $errorCode ]); } -// $errorMessage = $this->getMessageByErrorCode($errorCode, $errorMessage); + $exceptionObject = new $groupName(__($errorMessage), null, (int)$errorCode); //@codingStandardsIgnoreLine return $exceptionObject; } diff --git a/Gateway/Response/InitiateHandler.php b/Gateway/Response/InitiateHandler.php index 59dca617..b943859c 100644 --- a/Gateway/Response/InitiateHandler.php +++ b/Gateway/Response/InitiateHandler.php @@ -13,17 +13,16 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ + namespace Vipps\Payment\Gateway\Response; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Checkout\Model\Type\Onepage; use Magento\Customer\Model\Session; -use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\{App\ResourceConnection, Session\SessionManagerInterface}; use Magento\Payment\Gateway\{Data\PaymentDataObjectInterface, Response\HandlerInterface}; -use Magento\Quote\{ - Api\CartRepositoryInterface, Model\Quote, Model\Quote\Payment -}; -use Vipps\Payment\Gateway\Request\SubjectReader; -use Magento\Checkout\Model\Type\Onepage; -use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Quote\{Api\CartRepositoryInterface, Model\Quote\Payment}; +use Vipps\Payment\{Gateway\Request\SubjectReader, Model\QuoteManagement}; /** * Class InitiateHandler @@ -52,6 +51,16 @@ class InitiateHandler implements HandlerInterface */ private $customerSession; + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var QuoteManagement + */ + private $vippsQuoteManagement; + /** * InitiateHandler constructor. * @@ -59,21 +68,27 @@ class InitiateHandler implements HandlerInterface * @param SubjectReader $subjectReader * @param CheckoutHelper $checkoutHelper * @param SessionManagerInterface $customerSession + * @param ResourceConnection $resourceConnection + * @param QuoteManagement $monitoringManagement */ public function __construct( CartRepositoryInterface $cartRepository, SubjectReader $subjectReader, CheckoutHelper $checkoutHelper, - SessionManagerInterface $customerSession + SessionManagerInterface $customerSession, + ResourceConnection $resourceConnection, + QuoteManagement $monitoringManagement ) { $this->cartRepository = $cartRepository; $this->subjectReader = $subjectReader; $this->checkoutHelper = $checkoutHelper; $this->customerSession = $customerSession; + $this->resourceConnection = $resourceConnection; + $this->vippsQuoteManagement = $monitoringManagement; } /** - * {@inheritdoc} + * Save quote payment method. * * @param array $handlingSubject * @param array $responseBody @@ -100,6 +115,18 @@ public function handle(array $handlingSubject, array $responseBody) //@codingSta $payment->setMethod('vipps'); $quote->setIsActive(false); - $this->cartRepository->save($quote); + $connection = $this->resourceConnection->getConnection(); + + try { + $connection->beginTransaction(); + + $this->cartRepository->save($quote); + $this->vippsQuoteManagement->create($quote); + + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw $e; + } } } diff --git a/Gateway/Transaction/ShippingDetails.php b/Gateway/Transaction/ShippingDetails.php index 6499c051..363ad700 100644 --- a/Gateway/Transaction/ShippingDetails.php +++ b/Gateway/Transaction/ShippingDetails.php @@ -124,12 +124,12 @@ public function getCountry() } /** - * @return string + * @return string|null */ public function getPostcode() { //Added condition to support zipCode in special cases - return $this->_data[self::ADDRESS][self::POST_CODE] ?? $this->_data[self::ADDRESS][self::ZIP_CODE]; + return $this->_data[self::ADDRESS][self::POST_CODE] ?? $this->_data[self::ADDRESS][self::ZIP_CODE] ?? null; } /** diff --git a/Gateway/Transaction/Transaction.php b/Gateway/Transaction/Transaction.php index b783838d..7eb03629 100644 --- a/Gateway/Transaction/Transaction.php +++ b/Gateway/Transaction/Transaction.php @@ -13,6 +13,7 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ + namespace Vipps\Payment\Gateway\Transaction; /** @@ -169,14 +170,6 @@ public function __construct( $this->shippingDetails = $shippingDetails; } - /** - * @return TransactionInfo - */ - public function getTransactionInfo() - { - return $this->transactionInfo; - } - /** * @return TransactionSummary */ @@ -185,14 +178,6 @@ public function getTransactionSummary() return $this->transactionSummary; } - /** - * @return TransactionLogHistory - */ - public function getTransactionLogHistory() - { - return $this->transactionLogHistory; - } - /** * @return null|UserDetails */ @@ -214,7 +199,25 @@ public function getShippingDetails() */ public function isExpressCheckout() { - return $this->userDetails === null ? false : true; + return $this->userDetails === null ? false : true; + } + + /** + * Is initiate transaction. + * + * @return bool + */ + public function isInitiate() + { + return $this->getTransactionInfo()->getStatus() === Transaction::TRANSACTION_STATUS_INITIATE; + } + + /** + * @return TransactionInfo + */ + public function getTransactionInfo() + { + return $this->transactionInfo; } /** @@ -229,11 +232,8 @@ public function isTransactionAborted() Transaction::TRANSACTION_STATUS_REJECTED, Transaction::TRANSACTION_STATUS_FAILED ]; - if (in_array($this->getTransactionInfo()->getStatus(), $abortedStatuses)) { - return true; - } - return false; + return in_array($this->getTransactionInfo()->getStatus(), $abortedStatuses); } /** @@ -264,4 +264,12 @@ public function getTransactionId() return $this->getTransactionInfo()->getTransactionId() ?: $this->getTransactionLogHistory()->getLastTransactionId(); } + + /** + * @return TransactionLogHistory + */ + public function getTransactionLogHistory() + { + return $this->transactionLogHistory; + } } diff --git a/Model/Adminhtml/Source/Cancellation/Type.php b/Model/Adminhtml/Source/Cancellation/Type.php new file mode 100644 index 00000000..3d4ed8c7 --- /dev/null +++ b/Model/Adminhtml/Source/Cancellation/Type.php @@ -0,0 +1,55 @@ + self::AUTOMATIC, + 'label' => __('Automatic'), + ], + [ + 'value' => self::MANUAL, + 'label' => __('Manual'), + ] + ]; + } +} diff --git a/Model/Helper/Utility.php b/Model/Helper/Utility.php new file mode 100644 index 00000000..22149b0e --- /dev/null +++ b/Model/Helper/Utility.php @@ -0,0 +1,42 @@ +getBillingAddress(); + $billingAddress->setShouldIgnoreValidation(true); + + if (!$quote->getIsVirtual()) { + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setShouldIgnoreValidation(true); + if (!$billingAddress->getEmail()) { + $billingAddress->setSameAsBilling(1); + } + } + } +} diff --git a/Model/Order/Cancellation/Config.php b/Model/Order/Cancellation/Config.php new file mode 100644 index 00000000..0ee96bfd --- /dev/null +++ b/Model/Order/Cancellation/Config.php @@ -0,0 +1,132 @@ +scopeConfig = $scopeConfig; + } + + /** + * @param int|null $storeId + * @return bool + */ + public function isAutomatic($storeId = null) + { + return $this->getType($storeId) === Type::AUTOMATIC; + } + + /** + * Cancellation type code. + * + * @param int|null $storeId + * @return string + */ + public function getType($storeId = null) + { + return $this->getStoreConfig(self::XML_PATH_TYPE, $storeId); + } + + /** + * Common method to return store config value. + * + * @param $path + * @param int|null $storeId + * @return mixed + */ + private function getStoreConfig($path, $storeId = null) + { + return $this->scopeConfig->getValue($path, ScopeInterface::SCOPE_STORE, $storeId); + } + + /** + * @param int|null $storeId + * @return bool + */ + public function isManual($storeId = null) + { + return $this->getType($storeId) === Type::MANUAL; + } + + /** + * Number of failed attempts. + * + * @return int + */ + public function getAttemptsMaxCount() + { + return $this->getStoreConfig(self::XML_PATH_ATTEMPT_COUNT); + } + + /** + * Return inactivity time in minutes. + * + * @return int + */ + public function getInactivityTime() + { + return $this->getStoreConfig(self::XML_PATH_INACTIVITY_TIME); + } + + /** + * Number of days to store quotes information. + * + * @return int + */ + public function getQuoteStoragePeriod() + { + return $this->getStoreConfig(self::XML_PATH_QUOTE_STORAGE_PERIOD); + } +} diff --git a/Model/OrderPlace.php b/Model/OrderPlace.php index cf6f33e1..55230673 100644 --- a/Model/OrderPlace.php +++ b/Model/OrderPlace.php @@ -13,30 +13,25 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ + namespace Vipps\Payment\Model; -use Magento\Framework\Exception\{ - CouldNotSaveException, NoSuchEntityException, AlreadyExistsException, InputException -}; +use Magento\Framework\Exception\{AlreadyExistsException, CouldNotSaveException, InputException, NoSuchEntityException}; use Magento\Framework\Exception\LocalizedException; -use Magento\Payment\Helper\Formatter; use Magento\Payment\Gateway\ConfigInterface; -use Magento\Sales\Api\{ - OrderManagementInterface, Data\OrderInterface, OrderRepositoryInterface -}; -use Magento\Sales\Model\{ - Order, Order\Payment\Transaction as PaymentTransaction, - Order\Payment\Processor, Order\Payment -}; -use Magento\Quote\Api\{ - CartRepositoryInterface, CartManagementInterface, Data\CartInterface -}; +use Magento\Payment\Helper\Formatter; +use Magento\Quote\Api\{CartManagementInterface, CartRepositoryInterface, Data\CartInterface}; use Magento\Quote\Model\Quote; +use Magento\Sales\Api\{Data\OrderInterface, OrderManagementInterface, OrderRepositoryInterface}; +use Magento\Sales\Model\{Order, + Order\Payment, + Order\Payment\Processor, + Order\Payment\Transaction as PaymentTransaction}; +use Psr\Log\LoggerInterface; use Vipps\Payment\Api\CommandManagerInterface; +use Vipps\Payment\Api\Data\QuoteStatusInterface; +use Vipps\Payment\Gateway\{Exception\VippsException, Transaction\Transaction}; use Vipps\Payment\Gateway\Exception\WrongAmountException; -use Vipps\Payment\Gateway\{ - Transaction\Transaction, Exception\VippsException -}; use Vipps\Payment\Model\Adminhtml\Source\PaymentAction; /** @@ -103,6 +98,16 @@ class OrderPlace */ private $commandManager; + /** + * @var QuoteManagement + */ + private $quoteManagement; + + /** + * @var LoggerInterface + */ + private $logger; + /** * OrderPlace constructor. * @@ -117,6 +122,8 @@ class OrderPlace * @param LockManager $lockManager * @param ConfigInterface $config * @param CommandManagerInterface $commandManager + * @param QuoteManagement $quoteManagement + * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -130,7 +137,9 @@ public function __construct( QuoteUpdater $quoteUpdater, LockManager $lockManager, ConfigInterface $config, - CommandManagerInterface $commandManager + CommandManagerInterface $commandManager, + QuoteManagement $quoteManagement, + LoggerInterface $logger ) { $this->orderRepository = $orderRepository; $this->cartRepository = $cartRepository; @@ -143,6 +152,8 @@ public function __construct( $this->lockManager = $lockManager; $this->config = $config; $this->commandManager = $commandManager; + $this->quoteManagement = $quoteManagement; + $this->logger = $logger; } /** @@ -171,7 +182,9 @@ public function execute(CartInterface $quote, Transaction $transaction) try { $order = $this->placeOrder($quote, $transaction); + if ($order) { + $this->updateVippsQuote($quote); $paymentAction = $this->config->getValue('vipps_payment_action'); switch ($paymentAction) { case PaymentAction::ACTION_AUTHORIZE_CAPTURE: @@ -188,6 +201,18 @@ public function execute(CartInterface $quote, Transaction $transaction) } } + /** + * Check can we place order or not based on transaction object + * + * @param Transaction $transaction + * + * @return bool + */ + private function canPlaceOrder(Transaction $transaction) + { + return $transaction->isTransactionReserved(); + } + /** * @param CartInterface $quote * @@ -207,29 +232,6 @@ private function acquireLock(CartInterface $quote) return false; } - /** - * @param $lockName - * - * @return bool - * @throws InputException - */ - private function releaseLock($lockName) - { - return $this->lockManager->unlock($lockName); - } - - /** - * Check can we place order or not based on transaction object - * - * @param Transaction $transaction - * - * @return bool - */ - private function canPlaceOrder(Transaction $transaction) - { - return $transaction->isTransactionReserved(); - } - /** * @param CartInterface $quote * @param Transaction $transaction @@ -264,15 +266,12 @@ private function placeOrder(CartInterface $quote, Transaction $transaction) $clonedQuote->getShippingAddress()->setCollectShippingRates(true); $clonedQuote->collectTotals(); - if ($this->validateAmount($clonedQuote, $transaction)) { - // set quote active, collect totals and place order - $clonedQuote->setIsActive(true); - $orderId = $this->cartManagement->placeOrder($clonedQuote->getId()); - $order = $this->orderRepository->get($orderId); - } else { - // cancel order on vipps side - $this->commandManager->cancel($clonedQuote->getPayment()); - } + $this->validateAmount($clonedQuote, $transaction); + + // set quote active, collect totals and place order + $clonedQuote->setIsActive(true); + $orderId = $this->cartManagement->placeOrder($clonedQuote->getId()); + $order = $this->orderRepository->get($orderId); } $clonedQuote->setReservedOrderId(null); @@ -294,38 +293,41 @@ private function prepareQuote($quote) } /** - * Authorize action + * Check if reserved Order amount in vipps is the same as in Magento. * - * @param OrderInterface $order + * @param CartInterface $quote * @param Transaction $transaction + * + * @return void + * @throws WrongAmountException */ - private function authorize(OrderInterface $order, Transaction $transaction) + private function validateAmount(CartInterface $quote, Transaction $transaction) { - if ($order->getState() !== Order::STATE_NEW) { - return; - } - - /** @var Payment $payment */ - $payment = $order->getPayment(); - $transactionId = $transaction->getTransactionId(); - $payment->setTransactionId($transactionId); - $payment->setIsTransactionClosed(false); - $payment->setTransactionAdditionalInfo( - PaymentTransaction::RAW_DETAILS, - $transaction->getTransactionInfo()->getData() - ); - - // preconditions - $totalDue = $order->getTotalDue(); - $baseTotalDue = $order->getBaseTotalDue(); + $quoteAmount = (int)($this->formatPrice($quote->getGrandTotal()) * 100); + $vippsAmount = (int)$transaction->getTransactionInfo()->getAmount(); - // do authorize - $this->processor->authorize($payment, false, $baseTotalDue); - // base amount will be set inside - $payment->setAmountAuthorized($totalDue); - $this->orderRepository->save($order); + if ($quoteAmount != $vippsAmount) { + throw new WrongAmountException( + __("Quote Grand Total {$quoteAmount} does not match Transaction Amount {$vippsAmount}") + ); + } + } - $this->notify($order); + /** + * Update vipps quote with success. + * + * @param CartInterface $cart + */ + private function updateVippsQuote(CartInterface $cart) + { + try { + $vippsQuote = $this->quoteManagement->getByQuote($cart); + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_PLACED); + $this->quoteManagement->save($vippsQuote); + } catch (\Throwable $e) { + // Order is submitted but failed to update Vipps Quote. It should not affect order flow. + $this->logger->error($e->getMessage()); + } } /** @@ -378,18 +380,48 @@ private function notify($order) } /** - * Check if reserved Order amount in vipps is the same as in Magento. + * Authorize action * - * @param CartInterface $quote + * @param OrderInterface $order * @param Transaction $transaction + */ + private function authorize(OrderInterface $order, Transaction $transaction) + { + if ($order->getState() !== Order::STATE_NEW) { + return; + } + + /** @var Payment $payment */ + $payment = $order->getPayment(); + $transactionId = $transaction->getTransactionId(); + $payment->setTransactionId($transactionId); + $payment->setIsTransactionClosed(false); + $payment->setTransactionAdditionalInfo( + PaymentTransaction::RAW_DETAILS, + $transaction->getTransactionInfo()->getData() + ); + + // preconditions + $totalDue = $order->getTotalDue(); + $baseTotalDue = $order->getBaseTotalDue(); + + // do authorize + $this->processor->authorize($payment, false, $baseTotalDue); + // base amount will be set inside + $payment->setAmountAuthorized($totalDue); + $this->orderRepository->save($order); + + $this->notify($order); + } + + /** + * @param $lockName * * @return bool + * @throws InputException */ - private function validateAmount(CartInterface $quote, Transaction $transaction) + private function releaseLock($lockName) { - $quoteAmount = (int)($this->formatPrice($quote->getGrandTotal()) * 100); - $vippsAmount = (int)$transaction->getTransactionInfo()->getAmount(); - - return $quoteAmount == $vippsAmount; + return $this->lockManager->unlock($lockName); } } diff --git a/Model/Quote.php b/Model/Quote.php new file mode 100644 index 00000000..6f4da54f --- /dev/null +++ b/Model/Quote.php @@ -0,0 +1,182 @@ +setData(self::QUOTE_ID, $quoteId); + } + + /** + * @param string|null $reservedOrderId Null for backward compatibility. + * @return self + */ + public function setReservedOrderId($reservedOrderId = '') + { + return $this->setData(self::RESERVED_ORDER_ID, $reservedOrderId); + } + + /** + * @return int + */ + public function getQuoteId() + { + return $this->getData(self::QUOTE_ID); + } + + /** + * @return string + */ + public function getReservedOrderId() + { + return $this->getData(self::RESERVED_ORDER_ID); + } + + /** + * @param string $createdAt + * @return self + */ + public function setCreatedAt(string $createdAt) + { + return $this->setData(self::CREATED_AT, $createdAt); + } + + /** + * @param string $updatedAt + * @return self + */ + public function setUpdatedAt(string $updatedAt) + { + return $this->setData(self::UPDATED_AT, $updatedAt); + } + + /** + * @return string + */ + public function getCreatedAt() + { + return $this->getData(self::CREATED_AT); + } + + /** + * @return string + */ + public function getUpdatedAt() + { + return $this->getData(self::UPDATED_AT); + } + + /** + * @return int + */ + public function getEntityId() + { + return $this->getData(self::ENTITY_ID); + } + + /** + * Increment attempts. + * + * @return Quote + */ + public function incrementAttempt() + { + return $this->setAttempts($this->getAttempts() + 1); + } + + /** + * @param int $attempts + * @return self + */ + public function setAttempts(int $attempts) + { + return $this->setData(self::ATTEMPTS, $attempts); + } + + /** + * @return int + */ + public function getAttempts() + { + return $this->getData(self::ATTEMPTS); + } + + /** + * Clear attempts. + * @return Quote + */ + public function clearAttempts() + { + return $this->setAttempts(0); + } + + /** + * @param string $status + * @return Quote + */ + public function setStatus(string $status) + { + return $this->setData(self::FIELD_STATUS, $status); + } + + /** + * @return string + */ + public function getStatus() + { + return $this->getData(self::FIELD_STATUS); + } + + /** + * @param int $storeId + * @return self + */ + public function setStoreId(int $storeId) + { + return $this->setData(self::STORE_ID, $storeId); + } + + /** + * @return int + */ + public function getStoreId() + { + return $this->getData(self::STORE_ID); + } + + /** + * Constructor. + */ + protected function _construct() //@codingStandardsIgnoreLine + { + $this->_init(QuoteResource::class); + } +} diff --git a/Model/Quote/AddressUpdater.php b/Model/Quote/AddressUpdater.php index eb04a7e4..c5a2e332 100644 --- a/Model/Quote/AddressUpdater.php +++ b/Model/Quote/AddressUpdater.php @@ -16,20 +16,30 @@ namespace Vipps\Payment\Model\Quote; -use \Magento\Braintree\Model\Paypal\Helper\AbstractHelper; - -use Magento\Quote\{ - Model\Quote, Model\Quote\Address -}; - +use Magento\Quote\{Model\Quote, Model\Quote\Address}; use Vipps\Payment\Gateway\Transaction\ShippingDetails; +use Vipps\Payment\Model\Helper\Utility; /** * Class AddressUpdater * @package Vipps\Payment\Model\Quote */ -class AddressUpdater extends AbstractHelper +class AddressUpdater { + /** + * @var Utility + */ + private $utility; + + /** + * AddressUpdater constructor. + * @param Utility $utility + */ + public function __construct(Utility $utility) + { + $this->utility = $utility; + } + /** * Update quote addresses from source address. * @@ -40,8 +50,7 @@ class AddressUpdater extends AbstractHelper public function fromSourceAddress(Quote $quote, Address $sourceAddress) { $quote->setMayEditShippingAddress(false); - $this->disabledQuoteAddressValidation($quote); - + $this->utility->disabledQuoteAddressValidation($quote); $this->updateQuoteAddresses($quote, $sourceAddress); } diff --git a/Model/Quote/Attempt.php b/Model/Quote/Attempt.php new file mode 100644 index 00000000..1dbba3ee --- /dev/null +++ b/Model/Quote/Attempt.php @@ -0,0 +1,87 @@ +setData(self::PARENT_ID, $parentId); + } + + /** + * @param string $message + * @return QuoteAttemptInterface + */ + public function setMessage(string $message) + { + return $this->setData(self::MESSAGE, $message); + } + + /** + * @param string $createdAt + * @return QuoteAttemptInterface + */ + public function setCreatedAt(string $createdAt) + { + return $this->setData(self::CREATED_AT, $createdAt); + } + + /** + * @return int + */ + public function getParentId() + { + return $this->getData(self::PARENT_ID); + } + + /** + * @return string + */ + public function getMessage() + { + return $this->getData(self::MESSAGE); + } + + /** + * @return string + */ + public function getCreatedAt() + { + return $this->getData(self::CREATED_AT); + } + + /** + * Constructor. + */ + protected function _construct() //@codingStandardsIgnoreLine + { + $this->_init(AttemptResource::class); + } +} diff --git a/Model/Quote/AttemptManagement.php b/Model/Quote/AttemptManagement.php new file mode 100644 index 00000000..b4f4c3e6 --- /dev/null +++ b/Model/Quote/AttemptManagement.php @@ -0,0 +1,94 @@ +attemptFactory = $attemptFactory; + $this->quoteMonitorRepository = $quoteRepository; + $this->attemptRepository = $attemptRepository; + } + + /** + * Create new saved attempt. Increment attempt count. Fill it with message later. + * + * @param VippsQuote $quote + * @param bool $ignoreIncrement + * @return Attempt + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function createAttempt(VippsQuote $quote, $ignoreIncrement = false) + { + $attempt = $this + ->attemptFactory + ->create(['data' => ['parent_id' => $quote->getId()]]) + ->setDataChanges(true); + + // Saving attempt right immediately after creation cause it's already happened. + $this->attemptRepository->save($attempt); + + if (!$ignoreIncrement) { + // Increase attempt counter. + $quote->incrementAttempt(); + $this->quoteMonitorRepository->save($quote); + } + + return $attempt; + } + + /** + * @param Attempt $attempt + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function save(Attempt $attempt) + { + $this->attemptRepository->save($attempt); + } +} diff --git a/Model/Quote/AttemptRepository.php b/Model/Quote/AttemptRepository.php new file mode 100644 index 00000000..b15f170d --- /dev/null +++ b/Model/Quote/AttemptRepository.php @@ -0,0 +1,86 @@ +resource = $resource; + $this->collectionFactory = $collectionFactory; + } + + /** + * @param QuoteAttemptInterface $attempt + * @return QuoteAttemptInterface + * @throws CouldNotSaveException + */ + public function save(QuoteAttemptInterface $attempt) + { + try { + $this->resource->save($attempt); + + return $attempt; + } catch (\Exception $e) { + throw new CouldNotSaveException( + __( + 'Could not save Vipps Quote Attempt: %1', + $e->getMessage() + ), + $e + ); + } + } + + /** + * @param QuoteInterface $quote + * @return AttemptResource\Collection + */ + public function getByVippsQuote(QuoteInterface $quote) + { + /** @var \Vipps\Payment\Model\ResourceModel\Quote\Attempt\Collection $collection */ + $collection = $this->collectionFactory->create(); + $collection + ->addFieldToFilter('parent_id', ['eq' => $quote->getEntityId()]) + ->setOrder('created_at'); + + return $collection; + } +} diff --git a/Model/Quote/CancelFacade.php b/Model/Quote/CancelFacade.php new file mode 100644 index 00000000..90dbb3da --- /dev/null +++ b/Model/Quote/CancelFacade.php @@ -0,0 +1,93 @@ +commandManager = $commandManager; + $this->quoteRepository = $quoteRepository; + $this->attemptManagement = $attemptManagement; + } + + /** + * vipps_monitoring extension attribute requires to be loaded in the quote. + * + * @param QuoteInterface $vippsQuote + * @param CartInterface $quote + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Throwable + */ + public function cancel( + QuoteInterface $vippsQuote, + CartInterface $quote + ) { + try { + $attempt = $this->attemptManagement->createAttempt($vippsQuote); + // cancel order on vipps side + $this->commandManager->cancel($quote->getPayment()); + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_CANCELED); + $attempt->setMessage('The order has been canceled.'); + } catch (\Throwable $exception) { + // Log the exception + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_CANCEL_FAILED); + $attempt->setMessage($exception->getMessage()); + throw $exception; + } finally { + if (isset($attempt)) { + $this->attemptManagement->save($attempt); + } + $this->quoteRepository->save($vippsQuote); + } + } +} diff --git a/Model/Quote/Command/ManualCancel.php b/Model/Quote/Command/ManualCancel.php new file mode 100644 index 00000000..c4c1bccd --- /dev/null +++ b/Model/Quote/Command/ManualCancel.php @@ -0,0 +1,85 @@ +vippsQuote = $vippsQuote; + $this->config = $config; + $this->cartRepository = $cartRepository; + $this->cancelFacade = $cancelFacade; + } + + /** + * Verify is Quote Processing allowed for restart. + * + * @return bool + */ + public function isAllowed() + { + return in_array( + $this->vippsQuote->getStatus(), + [QuoteStatusInterface::STATUS_PLACE_FAILED, QuoteStatusInterface::STATUS_CANCEL_FAILED], + true + ); + } + + /** + * @throws LocalizedException + */ + public function execute() + { + try { + $quote = $this->cartRepository->get($this->vippsQuote->getQuoteId()); + + $this + ->cancelFacade + ->cancel($this->vippsQuote, $quote); + } catch (\Throwable $exception) { + throw new LocalizedException(__('Failed to cancel the order. Please contact support team.')); + } + } +} diff --git a/Model/Quote/Command/ManualCancelFactory.php b/Model/Quote/Command/ManualCancelFactory.php new file mode 100644 index 00000000..94d71974 --- /dev/null +++ b/Model/Quote/Command/ManualCancelFactory.php @@ -0,0 +1,35 @@ +objectManager = $objectManager; + } + + /** + * @param QuoteInterface $vippsQuote + * @return ManualCancel + */ + public function create(QuoteInterface $vippsQuote) + { + return $this->objectManager->create(ManualCancel::class, ['vippsQuote' => $vippsQuote]); //@codingStandardsIgnoreLine + } +} diff --git a/Model/Quote/Command/Restart.php b/Model/Quote/Command/Restart.php new file mode 100644 index 00000000..0b45a4a1 --- /dev/null +++ b/Model/Quote/Command/Restart.php @@ -0,0 +1,63 @@ +vippsQuote = $vippsQuote; + $this->quoteRepository = $quoteRepository; + } + + /** + * Verify is Quote Processing allowed for restart. + * + * @return bool + */ + public function isAllowed() + { + return in_array( + $this->vippsQuote->getStatus(), + [QuoteStatusInterface::STATUS_PLACE_FAILED, QuoteStatusInterface::STATUS_EXPIRED], + true + ); + } + + /** + * Mark Vipps Quote as ready for restart. + * + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function execute() + { + $this + ->vippsQuote + ->clearAttempts() + ->setStatus(QuoteStatusInterface::STATUS_PROCESSING); + + $this->quoteRepository->save($this->vippsQuote); + } +} diff --git a/Model/Quote/Command/RestartFactory.php b/Model/Quote/Command/RestartFactory.php new file mode 100644 index 00000000..5ff771e9 --- /dev/null +++ b/Model/Quote/Command/RestartFactory.php @@ -0,0 +1,35 @@ +objectManager = $objectManager; + } + + /** + * @param QuoteInterface $vippsQuote + * @return Restart + */ + public function create(QuoteInterface $vippsQuote) + { + return $this->objectManager->create(Restart::class, ['vippsQuote' => $vippsQuote]); //@codingStandardsIgnoreLine + } +} diff --git a/Model/QuoteManagement.php b/Model/QuoteManagement.php new file mode 100644 index 00000000..448fb424 --- /dev/null +++ b/Model/QuoteManagement.php @@ -0,0 +1,120 @@ +quoteFactory = $quoteFactory; + $this->quoteRepository = $quoteRepository; + } + + /** + * @param CartInterface $cart + * @return QuoteInterface + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function create(CartInterface $cart) + { + /** @var Quote $monitoringQuote */ + $monitoringQuote = $this->quoteFactory->create(); + + $monitoringQuote + ->setQuoteId($cart->getId()) + ->setStoreId($cart->getStoreId()) + ->setReservedOrderId($cart->getReservedOrderId()); + + return $this->quoteRepository->save($monitoringQuote); + } + + /** + * Loads Vipps monitoring as extension attribute. + * + * @param CartInterface $quote + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function loadExtensionAttribute(CartInterface $quote) + { + if ($extensionAttributes = $quote->getExtensionAttributes()) { + if (!$extensionAttributes->getVippsQuote()) { + $monitoringQuote = $this->getByQuote($quote); + + $extensionAttributes->setVippsQuote($monitoringQuote); + } + } + } + + /** + * @param CartInterface $cart + * @return Quote + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function getByQuote(CartInterface $cart) + { + /** @var Quote $monitoringQuote */ + try { + $monitoringQuote = $this->quoteRepository->loadByQuote($cart->getId()); + } catch (NoSuchEntityException $exception) { + // Setup default values for backward compatibility with current quotes. + $monitoringQuote = $this->quoteFactory->create() + ->setQuoteId($cart->getId()) + ->setReservedOrderId($cart->getReservedOrderId()); + + // Backward compatibility for old quotes paid with vipps. + $this->quoteRepository->save($monitoringQuote); + } + + return $monitoringQuote; + } + + /** + * @param Quote $quote + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function save(Quote $quote) + { + $this->quoteRepository->save($quote); + } +} diff --git a/Model/QuoteRepository.php b/Model/QuoteRepository.php new file mode 100644 index 00000000..74781e40 --- /dev/null +++ b/Model/QuoteRepository.php @@ -0,0 +1,113 @@ +quoteResource = $quoteResource; + $this->quoteFactory = $quoteFactory; + } + + /** + * Save monitoring record + * + * @param QuoteInterface $quote + * @return QuoteInterface + * @throws CouldNotSaveException + */ + public function save(QuoteInterface $quote) + { + try { + $this->quoteResource->save($quote); + + return $quote; + } catch (\Exception $e) { + throw new CouldNotSaveException( + __( + 'Could not save Vipps Quote: %1', + $e->getMessage() + ), + $e + ); + } + } + + /** + * Load monitoring quote by quote. + * + * @param $quoteId + * @throws NoSuchEntityException + */ + public function loadByQuote($quoteId) + { + $monitoringQuote = $this->quoteFactory->create(); + + $this->quoteResource->load($monitoringQuote, $quoteId, 'quote_id'); + + if (!$monitoringQuote->getId()) { + throw NoSuchEntityException::singleField('quote_id', $quoteId); + } + + return $monitoringQuote; + } + + /** + * @param int $monitoringQuoteId + * @return Quote + * @throws NoSuchEntityException + */ + public function load(int $monitoringQuoteId) + { + $monitoringQuote = $this->quoteFactory->create(); + + $this->quoteResource->load($monitoringQuote, $monitoringQuoteId); + + if (!$monitoringQuote->getId()) { + throw NoSuchEntityException::singleField('entity_id', $monitoringQuoteId); + } + + return $monitoringQuote; + } +} diff --git a/Model/QuoteUpdater.php b/Model/QuoteUpdater.php index 0db464ac..b459ee19 100644 --- a/Model/QuoteUpdater.php +++ b/Model/QuoteUpdater.php @@ -13,23 +13,20 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ + namespace Vipps\Payment\Model; -use Magento\Quote\{ - Api\CartRepositoryInterface, Api\Data\CartInterface, Model\Quote, Model\Quote\Address -}; -use Magento\Braintree\Model\Paypal\Helper\AbstractHelper; +use Magento\Quote\{Api\CartRepositoryInterface, Api\Data\CartInterface, Model\Quote}; use Vipps\Payment\Gateway\Command\PaymentDetailsProvider; use Vipps\Payment\Gateway\Exception\VippsException; -use Vipps\Payment\Gateway\Transaction\{ - ShippingDetails, Transaction, TransactionBuilder -}; +use Vipps\Payment\Gateway\Transaction\{Transaction, TransactionBuilder}; +use Vipps\Payment\Model\Helper\Utility; /** * Class QuoteUpdater * @package Vipps\Payment\Model\Helper */ -class QuoteUpdater extends AbstractHelper +class QuoteUpdater { /** * @var CartRepositoryInterface @@ -45,6 +42,10 @@ class QuoteUpdater extends AbstractHelper * @var TransactionBuilder */ private $transactionBuilder; + /** + * @var Utility + */ + private $utility; /** * QuoteUpdater constructor. @@ -52,15 +53,18 @@ class QuoteUpdater extends AbstractHelper * @param CartRepositoryInterface $cartRepository * @param PaymentDetailsProvider $paymentDetailsProvider * @param TransactionBuilder $transactionBuilder + * @param Utility $utility */ public function __construct( CartRepositoryInterface $cartRepository, PaymentDetailsProvider $paymentDetailsProvider, - TransactionBuilder $transactionBuilder + TransactionBuilder $transactionBuilder, + Utility $utility ) { $this->cartRepository = $cartRepository; $this->paymentDetailsProvider = $paymentDetailsProvider; $this->transactionBuilder = $transactionBuilder; + $this->utility = $utility; } /** @@ -81,7 +85,7 @@ public function execute(CartInterface $quote) $quote->setMayEditShippingMethod(true); $this->updateQuoteAddress($quote, $transaction); - $this->disabledQuoteAddressValidation($quote); + $this->utility->disabledQuoteAddressValidation($quote); /** * Unset shipping assignment to prevent from saving / applying outdated data @@ -123,6 +127,11 @@ private function updateShippingAddress(Quote $quote, Transaction $transaction) $shippingAddress->setShippingMethod($shippingDetails->getShippingMethodId()); $shippingAddress->setShippingAmount($shippingDetails->getShippingCost()); + // try to obtain postCode one more time if it is not done before + if (!$shippingAddress->getPostcode() && $shippingDetails->getPostcode()) { + $shippingAddress->setPostcode($shippingDetails->getPostcode()); + } + //We do not save user address from vipps in Magento $shippingAddress->setSaveInAddressBook(false); $shippingAddress->setSameAsBilling(true); @@ -137,11 +146,18 @@ private function updateBillingAddress(Quote $quote, Transaction $transaction) { $userDetails = $transaction->getUserDetails(); $billingAddress = $quote->getBillingAddress(); + $shippingDetails = $transaction->getShippingDetails(); $billingAddress->setLastname($userDetails->getLastName()); $billingAddress->setFirstname($userDetails->getFirstName()); $billingAddress->setEmail($userDetails->getEmail()); $billingAddress->setTelephone($userDetails->getMobileNumber()); + + // try to obtain postCode one more time if it is not done before + if (!$billingAddress->getPostcode() && $shippingDetails->getPostcode()) { + $billingAddress->setPostcode($shippingDetails->getPostcode()); + } + //We do not save user address from vipps in Magento $billingAddress->setSaveInAddressBook(false); $billingAddress->setSameAsBilling(false); diff --git a/Model/ResourceModel/Quote.php b/Model/ResourceModel/Quote.php new file mode 100644 index 00000000..7be6882d --- /dev/null +++ b/Model/ResourceModel/Quote.php @@ -0,0 +1,46 @@ +_init(self::TABLE_NAME, self::INDEX_FIELD); + } +} diff --git a/Model/ResourceModel/Quote/Attempt.php b/Model/ResourceModel/Quote/Attempt.php new file mode 100644 index 00000000..5750deb1 --- /dev/null +++ b/Model/ResourceModel/Quote/Attempt.php @@ -0,0 +1,44 @@ +_init(self::TABLE_NAME, self::INDEX_FIELD); + } +} diff --git a/Model/ResourceModel/Quote/Attempt/Collection.php b/Model/ResourceModel/Quote/Attempt/Collection.php new file mode 100644 index 00000000..81e4db5c --- /dev/null +++ b/Model/ResourceModel/Quote/Attempt/Collection.php @@ -0,0 +1,42 @@ +_init(AttemptModel::class, AttemptResource::class); + } +} diff --git a/Model/ResourceModel/Quote/Collection.php b/Model/ResourceModel/Quote/Collection.php new file mode 100644 index 00000000..1d9aeec3 --- /dev/null +++ b/Model/ResourceModel/Quote/Collection.php @@ -0,0 +1,42 @@ +_init(QuoteModel::class, QuoteResource::class); + } +} diff --git a/Model/ResourceModel/Quote/GridCollection.php b/Model/ResourceModel/Quote/GridCollection.php new file mode 100644 index 00000000..11e7e41a --- /dev/null +++ b/Model/ResourceModel/Quote/GridCollection.php @@ -0,0 +1,59 @@ +quoteCollectionFactory = $collectionFactory; + } + + /** + * Data updates on the module upgrade. + * + * @param ModuleDataSetupInterface $setup Setup interface. + * @param ModuleContextInterface $context Module context. + * @throws \Zend_Db_Exception + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) // @codingStandardsIgnoreLine + { + $installer = $setup; + $installer->startSetup(); + + if (version_compare($context->getVersion(), '1.2.0', '<')) { + $this->fillVippsQuotes($setup); + } + + $installer->endSetup(); + } + + /** + * Fill Vipps quote tables for currently unprocessed quotes. + * + * @param ModuleDataSetupInterface $installer + * @return void + */ + private function fillVippsQuotes(ModuleDataSetupInterface $installer) + { + $connection = $installer->getConnection(); + $tableName = $connection->getTableName('vipps_quote'); + + /** @var Collection $collection */ + $collection = $this->quoteCollectionFactory->create(); + + $collection + ->addFieldToSelect('entity_id', 'quote_id') + ->addFieldToSelect('reserved_order_id') + ->join( //@codingStandardsIgnoreLine + ['p' => $collection->getTable('quote_payment')], //@codingStandardsIgnoreLine + 'main_table.entity_id = p.quote_id and p.method = "vipps"', + [] + ) + // Taking all old-style quotes that were valid for processing. + // Adding Vipps quote monitoring records for them. + ->addFieldToFilter('main_table.is_active', ['in' => ['0']]) + ->addFieldToFilter('main_table.updated_at', ['to' => date("Y-m-d H:i:s", time() - 300)])// 5 min + ->addFieldToFilter('main_table.reserved_order_id', ['neq' => '']); + + $updateSql = $connection + ->insertFromSelect( //@codingStandardsIgnoreLine + $collection->getSelect(), + $tableName, + ['quote_id', 'reserved_order_id'] + ); + + $connection->query($updateSql); //@codingStandardsIgnoreLine + } +} diff --git a/Setup/UpgradeSchema.php b/Setup/UpgradeSchema.php index 80472d72..6ac5ce5f 100644 --- a/Setup/UpgradeSchema.php +++ b/Setup/UpgradeSchema.php @@ -16,14 +16,17 @@ namespace Vipps\Payment\Setup; -use Magento\Framework\Setup\{SchemaSetupInterface, UpgradeSchemaInterface, ModuleContextInterface}; use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\{ModuleContextInterface, SchemaSetupInterface, UpgradeSchemaInterface}; class UpgradeSchema implements UpgradeSchemaInterface // @codingStandardsIgnoreLine { /** - * @param SchemaSetupInterface $setup - * @param ModuleContextInterface $context + * Schema changes on the module upgrade. + * + * @param SchemaSetupInterface $setup Setup interface. + * @param ModuleContextInterface $context Module context. + * @throws \Zend_Db_Exception */ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) // @codingStandardsIgnoreLine { @@ -31,22 +34,199 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $installer->startSetup(); if (version_compare($context->getVersion(), '1.1.0', '<')) { - $tableName = $installer->getTable('vipps_payment_jwt'); - $installer->getConnection()->addColumn( + $this->addPaymentJwtScope($installer); + } + + if (version_compare($context->getVersion(), '1.2.0', '<')) { + $this->createVippsQuoteTable($installer); + $this->createVippsAttemptsTable($installer); + $this->addStatusToQuote($installer); + } + + if (version_compare($context->getVersion(), '1.2.1', '<')) { + $this->addStoreIdToQuote($installer); + } + + $installer->endSetup(); + } + + /** + * @param SchemaSetupInterface $installer + */ + private function addPaymentJwtScope(SchemaSetupInterface $installer) + { + $tableName = $installer->getTable('vipps_payment_jwt'); + $installer->getConnection()->addColumn( + $tableName, + 'scope', + [ + 'type' => Table::TYPE_TEXT, + 'length' => 8, + 'after' => 'token_id', + 'nullable' => false, + 'default' => 'default', + 'comment' => 'Scope' + ] + ); + $installer->getConnection()->truncateTable($tableName); + } + + /** + * Install Vipps quote monitoring table. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createVippsQuoteTable(SchemaSetupInterface $installer) + { + $connection = $installer->getConnection(); + + $table = $connection->newTable($connection->getTableName('vipps_quote')) + ->addColumn( + 'entity_id', + Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'Entity Id' + )->addColumn( + 'quote_id', + Table::TYPE_INTEGER, + null, + ['nullable' => true, 'unsigned' => true], + 'Quote Id' + )->addColumn( + 'reserved_order_id', + Table::TYPE_TEXT, + 32, + ['nullable' => false, 'default' => ''], + 'Order Increment Id' + )->addColumn( + 'attempts', + Table::TYPE_INTEGER, + 3, + ['nullable' => false, 'default' => '0'], + 'Attempts Number' + ) + ->addColumn( + 'created_at', + Table::TYPE_TIMESTAMP, + null, + ['default' => Table::TIMESTAMP_INIT, 'nullable' => false], + 'Created at' + )->addColumn( + 'updated_at', + Table::TYPE_TIMESTAMP, + null, + ['default' => Table::TIMESTAMP_INIT_UPDATE, 'nullable' => false], + 'Updated at' + ) + ->addIndex($installer->getIdxName('vipps_quote', 'quote_id'), 'quote_id') + ->addForeignKey( + $installer->getFkName('vipps_quote', 'quote_id', 'quote', 'entity_id'), + 'quote_id', + 'quote', + 'entity_id', + Table::ACTION_SET_NULL + ); + + $installer->getConnection()->createTable($table); + } + + /** + * Install Quote submitting attempts table. + * + * @param SchemaSetupInterface $installer Schema installer. + * @throws \Zend_Db_Exception + */ + private function createVippsAttemptsTable(SchemaSetupInterface $installer) + { + $connection = $installer->getConnection(); + + $table = $connection->newTable($connection->getTableName('vipps_quote_attempt')) + ->addColumn( + 'entity_id', + Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'Entity Id' + )->addColumn( + 'parent_id', + Table::TYPE_INTEGER, + null, + ['nullable' => false, 'unsigned' => true], + 'Vipps Quote Id' + )->addColumn( + 'message', + Table::TYPE_TEXT, + null, + [], + 'Message' + )->addColumn( + 'created_at', + Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => Table::TIMESTAMP_INIT], + 'Created at' + ) + ->addIndex($installer->getIdxName('vipps_quote_attempts', 'parent_id'), 'parent_id') + ->addForeignKey( + $installer->getFkName('vipps_quote_attempts', 'parent_id', 'vipps_quote', 'entity_id'), + 'parent_id', + 'vipps_quote', + 'entity_id', + Table::ACTION_CASCADE + ); + + $installer->getConnection()->createTable($table); + } + + /** + * Create cancellation table. + * + * @param SchemaSetupInterface $installer + */ + private function addStatusToQuote(SchemaSetupInterface $installer) + { + $connection = $installer->getConnection(); + $tableName = $connection->getTableName('vipps_quote'); + + $connection + ->addColumn( $tableName, - 'scope', + 'status', [ - 'type' => Table::TYPE_TEXT, - 'length' => 8, - 'after' => 'token_id', + 'type' => Table::TYPE_TEXT, + 'length' => 20, 'nullable' => false, - 'default' => 'default', - 'comment' => 'Scope' + 'comment' => 'Status', + 'after' => 'reserved_order_id', + 'default' => 'new' ] ); - $installer->getConnection()->truncateTable($tableName); - } + } - $installer->endSetup(); + /** + * Create cancellation table. + * + * @param SchemaSetupInterface $installer + */ + private function addStoreIdToQuote(SchemaSetupInterface $installer) + { + $connection = $installer->getConnection(); + $tableName = $connection->getTableName('vipps_quote'); + + $connection + ->addColumn( + $tableName, + 'store_id', + [ + 'type' => Table::TYPE_SMALLINT, + 'length' => 5, + 'nullable' => false, + 'comment' => 'Store ID', + 'after' => 'quote_id', + 'default' => '0' + ] + ); } } diff --git a/Ui/Component/Profiling/Column/ShowAction.php b/Ui/Component/Column/ShowAction.php similarity index 90% rename from Ui/Component/Profiling/Column/ShowAction.php rename to Ui/Component/Column/ShowAction.php index a7c61456..e61a8d11 100644 --- a/Ui/Component/Profiling/Column/ShowAction.php +++ b/Ui/Component/Column/ShowAction.php @@ -13,7 +13,8 @@ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ -namespace Vipps\Payment\Ui\Component\Profiling\Column; + +namespace Vipps\Payment\Ui\Component\Column; use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; @@ -60,12 +61,14 @@ public function __construct( public function prepareDataSource(array $dataSource) { if (isset($dataSource['data']['items'])) { + $path = $this->getData('config/urlPath') ?? '#'; + foreach ($dataSource['data']['items'] as & $item) { if (isset($item['entity_id'])) { $item[$this->getData('name')] = [ 'edit' => [ - 'href' => $this->urlBuilder - ->getUrl('vipps/profiling/view', ['entity_id' => $item['entity_id']]), + 'href' => $this->urlBuilder + ->getUrl($path, ['entity_id' => $item['entity_id']]), 'label' => __('Show') ] ]; diff --git a/Ui/Component/Column/Status.php b/Ui/Component/Column/Status.php new file mode 100644 index 00000000..c2834f95 --- /dev/null +++ b/Ui/Component/Column/Status.php @@ -0,0 +1,72 @@ + '', 'label' => '