From 0a59dcb2bac310b164cc92f87dc862d7352111a1 Mon Sep 17 00:00:00 2001 From: Oleh Date: Tue, 11 Jun 2024 15:01:43 +0300 Subject: [PATCH] Epayment api + Mobilepay (#185) * Migrate to ecomm api. * Switcher from ecomm api to epayment. * Update labels for mobile ecomm, allow NOK, EUR currencies. * Extend graphql for pwa mobilepay. * Add license. * Update mobilepay logo. * Update unit tests. * Update phpcs config. * Always capture when mobilepay is selected. * Update links and notes at configuration. * Unlock authorize and capture for mobilepay. * Fix implicit convert to int. --------- Co-authored-by: Oleg Malichenko --- Api/Fallback/AuthoriseInterface.php | 27 + Api/Payment/CommandManagerInterface.php | 77 +++ Api/Transaction/PaymentDetailsInterface.php | 10 + Block/Express/Button.php | 12 +- Controller/Payment/Fallback.php | 73 ++- Controller/Payment/InitExpress.php | 10 +- Controller/Payment/InitRegular.php | 29 +- Controller/Payment/Klarna/InitRegular.php | 34 +- Cron/CancelQuoteByAttempts.php | 62 +-- Cron/FetchOrderFromVipps.php | 77 +-- Gateway/Command/PaymentDetailsProvider.php | 5 +- Gateway/Config/Config.php | 48 +- Gateway/Response/TransactionHandler.php | 2 +- Gateway/Transaction/TransactionBuilder.php | 2 +- .../Validator/AvailabilityValidatorProxy.php | 39 ++ GatewayEpayment/Command/CancelCommand.php | 150 ++++++ GatewayEpayment/Command/CaptureCommand.php | 232 +++++++++ GatewayEpayment/Command/CommandManager.php | 89 ++++ GatewayEpayment/Command/GatewayCommand.php | 206 ++++++++ .../Command/PaymentCommandManager.php | 89 ++++ .../Command/PaymentDetailsProvider.php | 93 ++++ .../Command/PaymentsPostCommand.php | 243 +++++++++ GatewayEpayment/Command/RefundCommand.php | 243 +++++++++ GatewayEpayment/Data/Aggregate.php | 75 +++ GatewayEpayment/Data/AggregateFactory.php | 72 +++ GatewayEpayment/Data/Amount.php | 52 ++ GatewayEpayment/Data/BillingDetails.php | 122 +++++ GatewayEpayment/Data/Customer.php | 52 ++ GatewayEpayment/Data/InitSession.php | 63 +++ GatewayEpayment/Data/InitSessionBuilder.php | 72 +++ GatewayEpayment/Data/Payment.php | 225 +++++++++ GatewayEpayment/Data/PaymentBuilder.php | 88 ++++ GatewayEpayment/Data/PaymentDetails.php | 89 ++++ GatewayEpayment/Data/PaymentEventLog.php | 39 ++ GatewayEpayment/Data/PaymentEventLog/Item.php | 137 ++++++ .../Data/PaymentEventLogBuilder.php | 95 ++++ GatewayEpayment/Data/PaymentMethod.php | 39 ++ GatewayEpayment/Data/Profile.php | 52 ++ GatewayEpayment/Data/Session.php | 136 ++++++ GatewayEpayment/Data/SessionBuilder.php | 130 +++++ GatewayEpayment/Data/ShippingDetails.php | 134 +++++ .../Exception/AuthenticationException.php | 24 + GatewayEpayment/Exception/VippsException.php | 26 + .../Exception/WrongAmountException.php | 24 + GatewayEpayment/Http/Client/CheckoutCurl.php | 182 +++++++ .../Http/Client/ClientInterface.php | 70 +++ GatewayEpayment/Http/Client/PaymentCurl.php | 195 ++++++++ GatewayEpayment/Http/Transfer.php | 190 ++++++++ GatewayEpayment/Http/TransferBuilder.php | 170 +++++++ GatewayEpayment/Http/TransferFactory.php | 160 ++++++ .../Http/TransferFactoryInterface.php | 33 ++ GatewayEpayment/Http/TransferInterface.php | 88 ++++ .../Model/PaymentEventLogProvider.php | 76 +++ GatewayEpayment/Model/PaymentProvider.php | 78 +++ .../Model/TransactionProcessor.php | 461 ++++++++++++++++++ .../AuthorisationTypeBuilder.php | 36 ++ .../Request/DefaultDataBuilder.php | 26 + .../GetSession/ReferenceDataBuilder.php | 40 ++ .../GetSession/SessionIdDataBuilder.php | 40 ++ .../InitSession/AddressFieldsDataBuilder.php | 40 ++ .../InitSession/ConfigurationDataBuilder.php | 63 +++ .../InitSession/ContactFieldsDataBuilder.php | 40 ++ .../InitSession/CustomerDataBuilder.php | 69 +++ .../Request/InitSession/InitPreprocessor.php | 58 +++ .../InitSession/MerchantDataBuilder.php | 64 +++ .../InitSession/TransactionDataBuilder.php | 60 +++ .../Request/ModificationDataBuilder.php | 72 +++ .../Request/Payment/AmountBuilder.php | 72 +++ .../Request/ReferenceDataBuilder.php | 48 ++ .../Request/SendReceipt/BottomLineBuilder.php | 44 ++ .../SendReceipt/GenericDataBuilder.php | 37 ++ .../Request/SendReceipt/OrderLinesBuilder.php | 110 +++++ GatewayEpayment/Request/SubjectReader.php | 39 ++ .../Response/GetSessionHandler.php | 13 + .../Response/InitSessionHandler.php | 125 +++++ .../Response/Payment/PostHandler.php | 123 +++++ .../Response/Payment/TransactionHandler.php | 92 ++++ .../Validator/AvailabilityValidator.php | 72 +++ .../Validator/CancelTransactionValidator.php | 69 +++ .../Validator/CaptureTransactionValidator.php | 69 +++ .../Validator/InitiateValidator.php | 41 ++ GatewayEpayment/Validator/OrderValidator.php | 71 +++ .../Validator/ReferenceValidator.php | 81 +++ .../Validator/RefundTransactionValidator.php | 69 +++ GraphQl/Resolver/GetPaymentLabel.php | 46 ++ Model/Adminhtml/Source/PaymentAction.php | 4 +- Model/Checkout/ConfigProvider.php | 13 +- Model/CommandPoolProxy.php | 28 ++ Model/Config/ConfigVersionPool.php | 29 ++ Model/Config/Source/Version.php | 39 ++ Model/CurrencyValidator.php | 2 +- Model/Fallback/Authorise/Commerce.php | 31 ++ Model/Fallback/Authorise/Epayment.php | 29 ++ Model/Fallback/AuthoriseProxy.php | 35 ++ Model/Method/Vipps.php | 35 +- Model/PaymentEventLogProvider.php | 77 +++ Model/PaymentProvider.php | 78 +++ Model/Profiling/Profiler.php | 2 +- Model/Quote/CancelFacade.php | 67 +-- Model/TokenProvider.php | 12 +- Model/Transaction/PaymentDetailsProxy.php | 28 ++ Model/Transaction/StatusVisitor.php | 79 +++ Model/TransactionProcessor.php | 79 +-- Observer/CheckoutSubmitAllAfter.php | 2 +- .../Gateway/Command/GatewayCommandTest.php | 7 +- Test/Unit/Model/TokenProviderTest.php | 39 +- composer.json | 2 +- etc/adminhtml/system.xml | 19 +- etc/adminhtml/system/express_checkout.xml | 3 + etc/config.xml | 3 + etc/di.xml | 430 +++++++++++++++- etc/graphql/di.xml | 7 + etc/schema.graphqls | 8 +- i18n/nb_NO.csv | 10 +- phpcs.xml | 4 +- view/adminhtml/web/styles.css | 2 +- ...Vipps_MobilePay_Logo_Primary_RGB_Black.png | Bin 0 -> 27033 bytes ...Vipps_MobilePay_Logo_Primary_RGB_White.png | Bin 0 -> 25398 bytes view/base/web/images/mobilepay_logo.png | Bin 0 -> 1914 bytes view/base/web/images/vipps_logo_rgb.png | Bin .../js/view/payment/method-renderer/vipps.js | 21 +- view/frontend/web/template/payment/vipps.html | 8 +- 122 files changed, 7975 insertions(+), 387 deletions(-) create mode 100644 Api/Fallback/AuthoriseInterface.php create mode 100644 Api/Payment/CommandManagerInterface.php create mode 100644 Api/Transaction/PaymentDetailsInterface.php create mode 100644 Gateway/Validator/AvailabilityValidatorProxy.php create mode 100644 GatewayEpayment/Command/CancelCommand.php create mode 100644 GatewayEpayment/Command/CaptureCommand.php create mode 100644 GatewayEpayment/Command/CommandManager.php create mode 100755 GatewayEpayment/Command/GatewayCommand.php create mode 100644 GatewayEpayment/Command/PaymentCommandManager.php create mode 100644 GatewayEpayment/Command/PaymentDetailsProvider.php create mode 100644 GatewayEpayment/Command/PaymentsPostCommand.php create mode 100644 GatewayEpayment/Command/RefundCommand.php create mode 100644 GatewayEpayment/Data/Aggregate.php create mode 100644 GatewayEpayment/Data/AggregateFactory.php create mode 100644 GatewayEpayment/Data/Amount.php create mode 100644 GatewayEpayment/Data/BillingDetails.php create mode 100644 GatewayEpayment/Data/Customer.php create mode 100644 GatewayEpayment/Data/InitSession.php create mode 100644 GatewayEpayment/Data/InitSessionBuilder.php create mode 100644 GatewayEpayment/Data/Payment.php create mode 100644 GatewayEpayment/Data/PaymentBuilder.php create mode 100644 GatewayEpayment/Data/PaymentDetails.php create mode 100644 GatewayEpayment/Data/PaymentEventLog.php create mode 100644 GatewayEpayment/Data/PaymentEventLog/Item.php create mode 100644 GatewayEpayment/Data/PaymentEventLogBuilder.php create mode 100644 GatewayEpayment/Data/PaymentMethod.php create mode 100644 GatewayEpayment/Data/Profile.php create mode 100644 GatewayEpayment/Data/Session.php create mode 100644 GatewayEpayment/Data/SessionBuilder.php create mode 100644 GatewayEpayment/Data/ShippingDetails.php create mode 100644 GatewayEpayment/Exception/AuthenticationException.php create mode 100644 GatewayEpayment/Exception/VippsException.php create mode 100644 GatewayEpayment/Exception/WrongAmountException.php create mode 100644 GatewayEpayment/Http/Client/CheckoutCurl.php create mode 100644 GatewayEpayment/Http/Client/ClientInterface.php create mode 100644 GatewayEpayment/Http/Client/PaymentCurl.php create mode 100644 GatewayEpayment/Http/Transfer.php create mode 100644 GatewayEpayment/Http/TransferBuilder.php create mode 100644 GatewayEpayment/Http/TransferFactory.php create mode 100644 GatewayEpayment/Http/TransferFactoryInterface.php create mode 100644 GatewayEpayment/Http/TransferInterface.php create mode 100644 GatewayEpayment/Model/PaymentEventLogProvider.php create mode 100644 GatewayEpayment/Model/PaymentProvider.php create mode 100644 GatewayEpayment/Model/TransactionProcessor.php create mode 100644 GatewayEpayment/Request/AdjustAuthorization/AuthorisationTypeBuilder.php create mode 100644 GatewayEpayment/Request/DefaultDataBuilder.php create mode 100644 GatewayEpayment/Request/GetSession/ReferenceDataBuilder.php create mode 100644 GatewayEpayment/Request/GetSession/SessionIdDataBuilder.php create mode 100644 GatewayEpayment/Request/InitSession/AddressFieldsDataBuilder.php create mode 100644 GatewayEpayment/Request/InitSession/ConfigurationDataBuilder.php create mode 100644 GatewayEpayment/Request/InitSession/ContactFieldsDataBuilder.php create mode 100644 GatewayEpayment/Request/InitSession/CustomerDataBuilder.php create mode 100755 GatewayEpayment/Request/InitSession/InitPreprocessor.php create mode 100644 GatewayEpayment/Request/InitSession/MerchantDataBuilder.php create mode 100644 GatewayEpayment/Request/InitSession/TransactionDataBuilder.php create mode 100644 GatewayEpayment/Request/ModificationDataBuilder.php create mode 100644 GatewayEpayment/Request/Payment/AmountBuilder.php create mode 100644 GatewayEpayment/Request/ReferenceDataBuilder.php create mode 100644 GatewayEpayment/Request/SendReceipt/BottomLineBuilder.php create mode 100644 GatewayEpayment/Request/SendReceipt/GenericDataBuilder.php create mode 100644 GatewayEpayment/Request/SendReceipt/OrderLinesBuilder.php create mode 100644 GatewayEpayment/Request/SubjectReader.php create mode 100644 GatewayEpayment/Response/GetSessionHandler.php create mode 100644 GatewayEpayment/Response/InitSessionHandler.php create mode 100644 GatewayEpayment/Response/Payment/PostHandler.php create mode 100644 GatewayEpayment/Response/Payment/TransactionHandler.php create mode 100644 GatewayEpayment/Validator/AvailabilityValidator.php create mode 100644 GatewayEpayment/Validator/CancelTransactionValidator.php create mode 100644 GatewayEpayment/Validator/CaptureTransactionValidator.php create mode 100644 GatewayEpayment/Validator/InitiateValidator.php create mode 100644 GatewayEpayment/Validator/OrderValidator.php create mode 100644 GatewayEpayment/Validator/ReferenceValidator.php create mode 100644 GatewayEpayment/Validator/RefundTransactionValidator.php create mode 100644 GraphQl/Resolver/GetPaymentLabel.php create mode 100644 Model/CommandPoolProxy.php create mode 100644 Model/Config/ConfigVersionPool.php create mode 100644 Model/Config/Source/Version.php create mode 100644 Model/Fallback/Authorise/Commerce.php create mode 100644 Model/Fallback/Authorise/Epayment.php create mode 100644 Model/Fallback/AuthoriseProxy.php create mode 100644 Model/PaymentEventLogProvider.php create mode 100644 Model/PaymentProvider.php create mode 100644 Model/Transaction/PaymentDetailsProxy.php create mode 100644 Model/Transaction/StatusVisitor.php create mode 100644 view/base/web/images/Vipps_MobilePay_Logo_Primary_RGB_Black.png create mode 100644 view/base/web/images/Vipps_MobilePay_Logo_Primary_RGB_White.png create mode 100644 view/base/web/images/mobilepay_logo.png mode change 100755 => 100644 view/base/web/images/vipps_logo_rgb.png diff --git a/Api/Fallback/AuthoriseInterface.php b/Api/Fallback/AuthoriseInterface.php new file mode 100644 index 00000000..89171b4e --- /dev/null +++ b/Api/Fallback/AuthoriseInterface.php @@ -0,0 +1,27 @@ +config = $config; $this->assetRepo = $context->getAssetRepository(); @@ -87,7 +89,9 @@ public function __construct( protected function _toHtml() //@codingStandardsIgnoreLine { if (!$this->config->getValue('active') - || !$this->config->getValue('express_checkout')) { + || !$this->config->getValue('express_checkout') + || $this->config->getValue('version') !== Version::CONFIG_VIPPS + ) { return ''; } if (!$this->getIsInCatalogProduct() && diff --git a/Controller/Payment/Fallback.php b/Controller/Payment/Fallback.php index 4e1a3964..78db598a 100644 --- a/Controller/Payment/Fallback.php +++ b/Controller/Payment/Fallback.php @@ -38,9 +38,13 @@ use Psr\Log\LoggerInterface; use Vipps\Payment\Api\Data\QuoteInterface; use Vipps\Payment\Api\QuoteRepositoryInterface; +use Vipps\Payment\Gateway\Config\Config; use Vipps\Payment\Gateway\Transaction\Transaction; +use Vipps\Payment\GatewayEpayment\Data\Payment; +use Vipps\Payment\Model\Fallback\AuthoriseProxy; use Vipps\Payment\Model\Gdpr\Compliance; use Vipps\Payment\Model\OrderLocator; +use Vipps\Payment\Model\Transaction\StatusVisitor; use Vipps\Payment\Model\TransactionProcessor; /** @@ -106,7 +110,7 @@ class Fallback implements ActionInterface, CsrfAwareActionInterface private $resultFactory; /** - * @var ConfigInterface + * @var Config */ private $config; @@ -119,6 +123,8 @@ class Fallback implements ActionInterface, CsrfAwareActionInterface * @var OrderInterface|null */ private $order; + private StatusVisitor $statusVisitor; + private AuthoriseProxy $authoriseProxy; /** * Fallback constructor. @@ -134,23 +140,25 @@ class Fallback implements ActionInterface, CsrfAwareActionInterface * @param OrderLocator $orderLocator * @param Compliance $compliance * @param LoggerInterface $logger - * @param ConfigInterface $config + * @param Config $config * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - ResultFactory $resultFactory, - RequestInterface $request, - SessionManagerInterface $checkoutSession, - TransactionProcessor $transactionProcessor, - CartRepositoryInterface $cartRepository, - ManagerInterface $messageManager, + ResultFactory $resultFactory, + RequestInterface $request, + SessionManagerInterface $checkoutSession, + TransactionProcessor $transactionProcessor, + CartRepositoryInterface $cartRepository, + ManagerInterface $messageManager, QuoteRepositoryInterface $vippsQuoteRepository, OrderManagementInterface $orderManagement, - OrderLocator $orderLocator, - Compliance $compliance, - LoggerInterface $logger, - ConfigInterface $config + OrderLocator $orderLocator, + Compliance $compliance, + LoggerInterface $logger, + Config $config, + StatusVisitor $statusVisitor, + AuthoriseProxy $authoriseProxy ) { $this->resultFactory = $resultFactory; $this->request = $request; @@ -164,6 +172,8 @@ public function __construct( $this->orderManagement = $orderManagement; $this->logger = $logger; $this->config = $config; + $this->statusVisitor = $statusVisitor; + $this->authoriseProxy = $authoriseProxy; } /** @@ -176,9 +186,10 @@ public function execute() /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); try { - $this->authorize(); - $vippsQuote = $this->getVippsQuote(); + + $this->authoriseProxy->do($this->request, $vippsQuote); + $transaction = $this->transactionProcessor->process($vippsQuote); $resultRedirect = $this->prepareResponse($resultRedirect, $transaction); @@ -196,7 +207,7 @@ public function execute() $cartPersistence = $this->config->getValue('cancellation_cart_persistence'); $quoteCouldBeRestored = $transaction - && ($transaction->transactionWasCancelled() || $transaction->isTransactionExpired()); + && ($this->statusVisitor->isCanceled($transaction) || $this->statusVisitor->isExpired($transaction)); $order = $this->getOrder(); if ($quoteCouldBeRestored && $cartPersistence) { @@ -285,7 +296,7 @@ private function getVippsQuote($forceReload = false): QuoteInterface { if (null === $this->vippsQuote || $forceReload) { $this->vippsQuote = $this->vippsQuoteRepository - ->loadByOrderId($this->request->getParam('order_id')); + ->loadByOrderId($this->authoriseProxy->getOrderId($this->request)); } return $this->vippsQuote; @@ -293,12 +304,12 @@ private function getVippsQuote($forceReload = false): QuoteInterface /** * @param Redirect $resultRedirect - * @param Transaction $transaction + * @param Transaction|Payment $transaction * * @return Redirect * @throws \Exception */ - private function prepareResponse(Redirect $resultRedirect, Transaction $transaction) + private function prepareResponse(Redirect $resultRedirect, $transaction) { $this->defineMessage($transaction); $this->defineRedirectPath($resultRedirect, $transaction); @@ -344,13 +355,19 @@ private function isCartPersistent() return $this->config->getValue('cancellation_cart_persistence'); } - private function defineMessage(Transaction $transaction): void + /** + * @param Transaction|Payment $transaction + * @return void + */ + private function defineMessage($transaction): void { - if ($transaction->transactionWasCancelled()) { - $this->messageManager->addWarningMessage(__('Your order was cancelled in Vipps.')); - } elseif ($transaction->isTransactionReserved() || $transaction->isTransactionCaptured()) { - //$this->messageManager->addWarningMessage(__('Your order was successfully placed.')); - } elseif ($transaction->isTransactionExpired()) { + if ($this->statusVisitor->isCanceled($transaction)) { + $this->messageManager->addWarningMessage(__('Your order was cancelled in %1.', $this->config->getTitle())); + } elseif ( + $this->statusVisitor->isReserved($transaction) + || $this->statusVisitor->isCaptured($transaction)) { + $this->messageManager->addWarningMessage(__('Your order was successfully placed.')); + } elseif ($this->statusVisitor->isExpired($transaction)) { $this->messageManager->addErrorMessage( __('Transaction was expired. Please, place your order again') ); @@ -363,13 +380,15 @@ private function defineMessage(Transaction $transaction): void /** * @param Redirect $resultRedirect - * @param Transaction $transaction + * @param Transaction|Payment $transaction * * @throws NoSuchEntityException */ - private function defineRedirectPath(Redirect $resultRedirect, Transaction $transaction): void + private function defineRedirectPath(Redirect $resultRedirect, $transaction): void { - if ($transaction->isTransactionReserved() || $transaction->isTransactionCaptured()) { + if ($this->statusVisitor->isReserved($transaction) + || $this->statusVisitor->isCaptured($transaction) + ) { $resultRedirect->setPath('checkout/onepage/success', ['_secure' => true]); } else { $orderId = $this->getOrder() ? $this->getOrder()->getEntityId() : null; diff --git a/Controller/Payment/InitExpress.php b/Controller/Payment/InitExpress.php index 2eabb72d..69977dd9 100644 --- a/Controller/Payment/InitExpress.php +++ b/Controller/Payment/InitExpress.php @@ -28,11 +28,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\Session\SessionManagerInterface; -use Magento\Payment\Gateway\ConfigInterface; use Magento\Payment\Gateway\Command\ResultInterface; use Magento\Quote\Api\CartRepositoryInterface; use Psr\Log\LoggerInterface; use Vipps\Payment\Api\CommandManagerInterface; +use Vipps\Payment\Gateway\Config\Config; use Vipps\Payment\Gateway\Request\Initiate\InitiateBuilderInterface; use Vipps\Payment\Model\Method\Vipps; @@ -74,7 +74,7 @@ class InitExpress implements ActionInterface private $frameworkExceptionFactory; /** - * @var ConfigInterface + * @var Config */ private $config; @@ -102,7 +102,7 @@ class InitExpress implements ActionInterface * @param CommandManagerInterface $commandManager * @param CartRepositoryInterface $cartRepository * @param LocalizedExceptionFactory $frameworkExceptionFactory - * @param ConfigInterface $config + * @param Config $config * @param ManagerInterface $messageManager * @param CheckoutHelper $checkoutHelper * @param LoggerInterface $logger @@ -116,7 +116,7 @@ public function __construct( CommandManagerInterface $commandManager, CartRepositoryInterface $cartRepository, LocalizedExceptionFactory $frameworkExceptionFactory, - ConfigInterface $config, + Config $config, ManagerInterface $messageManager, CheckoutHelper $checkoutHelper, LoggerInterface $logger @@ -155,7 +155,7 @@ public function execute() } catch (\Exception $e) { $this->logger->critical($this->enlargeMessage($e)); $this->messageManager->addErrorMessage( - __('An error occurred during request to Vipps. Please try again later.') + __('An error occurred during request to %1. Please try again later.', $this->config->getTitle()) ); $resultRedirect->setPath('checkout/cart', ['_secure' => true]); } diff --git a/Controller/Payment/InitRegular.php b/Controller/Payment/InitRegular.php index 31307dc0..2903fbf7 100644 --- a/Controller/Payment/InitRegular.php +++ b/Controller/Payment/InitRegular.php @@ -32,6 +32,7 @@ use Magento\Quote\Model\Quote; use Psr\Log\LoggerInterface; use Vipps\Payment\Api\CommandManagerInterface; +use Vipps\Payment\Gateway\Config\Config; use Vipps\Payment\Gateway\Request\Initiate\InitiateBuilderInterface; use Vipps\Payment\Model\Method\Vipps; @@ -71,6 +72,7 @@ class InitRegular implements ActionInterface * @var LoggerInterface */ private $logger; + private Config $config; /** * InitRegular constructor. @@ -83,12 +85,13 @@ class InitRegular implements ActionInterface * @param LoggerInterface $logger */ public function __construct( - ResultFactory $resultFactory, + ResultFactory $resultFactory, SessionManagerInterface $checkoutSession, SessionManagerInterface $customerSession, CommandManagerInterface $commandManager, CartRepositoryInterface $cartRepository, - LoggerInterface $logger + Config $config, + LoggerInterface $logger ) { $this->resultFactory = $resultFactory; $this->checkoutSession = $checkoutSession; @@ -96,6 +99,7 @@ public function __construct( $this->commandManager = $commandManager; $this->cartRepository = $cartRepository; $this->logger = $logger; + $this->config = $config; } /** @@ -119,7 +123,7 @@ public function execute() } catch (\Exception $e) { $this->logger->critical($this->enlargeMessage($e)); $response->setData([ - 'message' => __('An error occurred during request to Vipps. Please try again later.') + 'message' => __('An error occurred during request to %1. Please try again later.', $this->config->getTitle()) ]); } @@ -135,14 +139,15 @@ public function execute() */ private function initiatePayment(CartInterface $quote) { - return $this->commandManager->initiatePayment( - $quote->getPayment(), - [ - 'amount' => $quote->getGrandTotal(), - InitiateBuilderInterface::PAYMENT_TYPE_KEY => InitiateBuilderInterface::PAYMENT_TYPE_REGULAR_PAYMENT, - Vipps::METHOD_TYPE_KEY => Vipps::METHOD_TYPE_REGULAR_CHECKOUT - ] - ); + return $this->commandManager + ->initiatePayment( + $quote->getPayment(), + [ + 'amount' => $quote->getGrandTotal(), + InitiateBuilderInterface::PAYMENT_TYPE_KEY => InitiateBuilderInterface::PAYMENT_TYPE_REGULAR_PAYMENT, + Vipps::METHOD_TYPE_KEY => Vipps::METHOD_TYPE_REGULAR_CHECKOUT + ] + ); } /** @@ -150,7 +155,7 @@ private function initiatePayment(CartInterface $quote) * * @return string */ - private function enlargeMessage($e): string + private function enlargeMessage(\Exception $e): string { $quoteId = $this->checkoutSession->getQuoteId(); $trace = $e->getTraceAsString(); diff --git a/Controller/Payment/Klarna/InitRegular.php b/Controller/Payment/Klarna/InitRegular.php index fa4012b7..6560d469 100644 --- a/Controller/Payment/Klarna/InitRegular.php +++ b/Controller/Payment/Klarna/InitRegular.php @@ -39,7 +39,8 @@ use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Psr\Log\LoggerInterface; -use Vipps\Payment\Api\CommandManagerInterface; +use Vipps\Payment\Api\Payment\CommandManagerInterface; +use Vipps\Payment\Gateway\Config\Config; use Vipps\Payment\Gateway\Request\Initiate\InitiateBuilderInterface; use Vipps\Payment\Model\Method\Vipps; use function __; @@ -109,6 +110,7 @@ class InitRegular implements ActionInterface * @var ManagerInterface */ private $messageManager; + private Config $config; /** * InitRegular constructor. @@ -129,18 +131,19 @@ class InitRegular implements ActionInterface * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - ResultFactory $resultFactory, - CommandManagerInterface $commandManager, - SessionManagerInterface $checkoutSession, - SessionManagerInterface $customerSession, - CheckoutHelper $checkoutHelper, - CartRepositoryInterface $cartRepository, - LoggerInterface $logger, - CartManagementInterface $cartManagement, + ResultFactory $resultFactory, + CommandManagerInterface $commandManager, + SessionManagerInterface $checkoutSession, + SessionManagerInterface $customerSession, + CheckoutHelper $checkoutHelper, + CartRepositoryInterface $cartRepository, + LoggerInterface $logger, + CartManagementInterface $cartManagement, GuestPaymentInformationManagementInterface $guestPaymentInformationManagement, - PaymentInformationManagementInterface $paymentInformationManagement, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId, - ManagerInterface $messageManager + PaymentInformationManagementInterface $paymentInformationManagement, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId, + ManagerInterface $messageManager, + Config $config ) { $this->resultFactory = $resultFactory; $this->commandManager = $commandManager; @@ -154,6 +157,7 @@ public function __construct( $this->paymentInformationManagement = $paymentInformationManagement; $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; $this->messageManager = $messageManager; + $this->config = $config; } /** @@ -180,7 +184,7 @@ public function execute() } catch (\Exception $e) { $this->logger->critical($this->enlargeMessage($e)); $this->messageManager->addErrorMessage( - __('An error occurred during request to Vipps. Please try again later.') + __('An error occurred during request to %1. Please try again later.', $this->config->getTitle()) ); $response->setPath('checkout/cart'); } @@ -200,9 +204,9 @@ private function initiatePayment(CartInterface $quote) return $this->commandManager->initiatePayment( $quote->getPayment(), [ - 'amount' => $quote->getGrandTotal(), + 'amount' => $quote->getGrandTotal(), InitiateBuilderInterface::PAYMENT_TYPE_KEY => InitiateBuilderInterface::PAYMENT_TYPE_REGULAR_PAYMENT, - Vipps::METHOD_TYPE_KEY => Vipps::METHOD_TYPE_REGULAR_CHECKOUT + Vipps::METHOD_TYPE_KEY => Vipps::METHOD_TYPE_REGULAR_CHECKOUT ] ); } diff --git a/Cron/CancelQuoteByAttempts.php b/Cron/CancelQuoteByAttempts.php index 8e7c18ac..d2a9563f 100644 --- a/Cron/CancelQuoteByAttempts.php +++ b/Cron/CancelQuoteByAttempts.php @@ -29,62 +29,20 @@ use Vipps\Payment\Model\ResourceModel\Quote\CollectionFactory as VippsQuoteCollectionFactory; /** - * Class FetchOrderStatus - * @package Vipps\Payment\Cron * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CancelQuoteByAttempts { - /** - * Order collection page size - */ - const COLLECTION_PAGE_SIZE = 250; - - /** - * @var string - */ - const DEFAULT_ATTEMPTS_MAX_COUNT = 3; + private const COLLECTION_PAGE_SIZE = 250; + private const DEFAULT_ATTEMPTS_MAX_COUNT = 3; - /** - * @var LoggerInterface - */ - private $logger; + private LoggerInterface $logger; + private StoreManagerInterface $storeManager; + private ScopeCodeResolver $scopeCodeResolver; + private Config $cancellationConfig; + private VippsQuoteCollectionFactory $vippsQuoteCollectionFactory; + private CancelFacade $cancelFacade; - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @var ScopeCodeResolver - */ - private $scopeCodeResolver; - - /** - * @var Config - */ - private $cancellationConfig; - - /** - * @var VippsQuoteCollectionFactory - */ - private $vippsQuoteCollectionFactory; - - /** - * @var CancelFacade - */ - private $cancelFacade; - - /** - * CancelQuoteByAttempts constructor. - * - * @param LoggerInterface $logger - * @param StoreManagerInterface $storeManager - * @param ScopeCodeResolver $scopeCodeResolver - * @param Config $cancellationConfig - * @param VippsQuoteCollectionFactory $vippsQuoteCollectionFactory - * @param CancelFacade $cancelFacade - */ public function __construct( LoggerInterface $logger, StoreManagerInterface $storeManager, @@ -102,11 +60,9 @@ public function __construct( } /** - * Create orders from Vipps that are not created in Magento yet - * * @throws NoSuchEntityException */ - public function execute() + public function execute(): void { try { $currentStore = $this->storeManager->getStore()->getId(); diff --git a/Cron/FetchOrderFromVipps.php b/Cron/FetchOrderFromVipps.php index 1f96b1f8..7cb22c86 100644 --- a/Cron/FetchOrderFromVipps.php +++ b/Cron/FetchOrderFromVipps.php @@ -27,7 +27,7 @@ use Vipps\Payment\Gateway\Exception\VippsException; use Vipps\Payment\Gateway\Transaction\Transaction; use Vipps\Payment\Model\Order\Cancellation\Config; -use Vipps\Payment\Model\TransactionProcessor; +use Vipps\Payment\GatewayEpayment\Model\TransactionProcessor; use Vipps\Payment\Model\Quote as VippsQuote; use Vipps\Payment\Model\Quote\AttemptManagement; use Vipps\Payment\Model\QuoteRepository as VippsQuoteRepository; @@ -35,69 +35,21 @@ use Vipps\Payment\Model\ResourceModel\Quote\CollectionFactory as VippsQuoteCollectionFactory; /** - * Class FetchOrderStatus - * @package Vipps\Payment\Cron * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FetchOrderFromVipps { - /** - * Order collection page size - */ - const COLLECTION_PAGE_SIZE = 250; - - /** - * @var TransactionProcessor - */ - private $transactionProcessor; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @var ScopeCodeResolver - */ - private $scopeCodeResolver; - - /** - * @var Config - */ - private $cancellationConfig; - - /** - * @var AttemptManagement - */ - private $attemptManagement; + private const COLLECTION_PAGE_SIZE = 250; - /** - * @var VippsQuoteCollectionFactory - */ - private $vippsQuoteCollectionFactory; - - /** - * @var VippsQuoteRepository - */ - private $vippsQuoteRepository; + private TransactionProcessor $transactionProcessor; + private LoggerInterface $logger; + private StoreManagerInterface $storeManager; + private ScopeCodeResolver $scopeCodeResolver; + private Config $cancellationConfig; + private AttemptManagement $attemptManagement; + private VippsQuoteCollectionFactory $vippsQuoteCollectionFactory; + private VippsQuoteRepository $vippsQuoteRepository; - /** - * FetchOrderFromVipps constructor. - * - * @param VippsQuoteCollectionFactory $vippsQuoteCollectionFactory - * @param VippsQuoteRepository $vippsQuoteRepository - * @param TransactionProcessor $orderProcessor - * @param LoggerInterface $logger - * @param StoreManagerInterface $storeManager - * @param ScopeCodeResolver $scopeCodeResolver - * @param Config $cancellationConfig - * @param AttemptManagement $attemptManagement - */ public function __construct( VippsQuoteCollectionFactory $vippsQuoteCollectionFactory, VippsQuoteRepository $vippsQuoteRepository, @@ -169,8 +121,6 @@ private function processQuote(VippsQuote $vippsQuote) /** * Prepare environment. - * - * @param VippsQuote $quote */ private function prepareEnv(VippsQuote $quote) { @@ -180,12 +130,7 @@ private function prepareEnv(VippsQuote $quote) $this->storeManager->setCurrentStore($quote->getStoreId()); } - /** - * @param $currentPage - * - * @return VippsQuoteCollection - */ - private function createCollection($currentPage) + private function createCollection(int $currentPage): VippsQuoteCollection { /** @var VippsQuoteCollection $collection */ $collection = $this->vippsQuoteCollectionFactory->create(); diff --git a/Gateway/Command/PaymentDetailsProvider.php b/Gateway/Command/PaymentDetailsProvider.php index 4e07555c..25731388 100644 --- a/Gateway/Command/PaymentDetailsProvider.php +++ b/Gateway/Command/PaymentDetailsProvider.php @@ -16,6 +16,7 @@ namespace Vipps\Payment\Gateway\Command; use Vipps\Payment\Api\CommandManagerInterface; +use Vipps\Payment\Api\Transaction\PaymentDetailsInterface; use Vipps\Payment\Gateway\Exception\VippsException; use Vipps\Payment\Gateway\Transaction\Transaction; use Vipps\Payment\Gateway\Transaction\TransactionBuilder; @@ -25,7 +26,7 @@ * @package Vipps\Payment\Gateway\Command * @spi */ -class PaymentDetailsProvider +class PaymentDetailsProvider implements PaymentDetailsInterface { /** * @var CommandManagerInterface @@ -62,7 +63,7 @@ public function __construct( * @return Transaction * @throws VippsException */ - public function get($orderId): ?Transaction + public function get(string $orderId): ?Transaction { if (!isset($this->cache[$orderId])) { $response = $this->commandManager->getPaymentDetails(['orderId' => $orderId]); diff --git a/Gateway/Config/Config.php b/Gateway/Config/Config.php index d898b214..e9a3e66e 100644 --- a/Gateway/Config/Config.php +++ b/Gateway/Config/Config.php @@ -13,46 +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\Gateway\Config; use Magento\Payment\Gateway\Config\Config as OriginConfig; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\App\Config\ScopeConfigInterface; +use Vipps\Payment\Model\Adminhtml\Source\PaymentAction; +use Vipps\Payment\Model\Config\Source\Version; +use Vipps\Payment\Model\Method\Vipps; -/** - * Class Config - * @package Vipps\Payment\Gateway\Config - */ class Config extends OriginConfig { - /** - * @var StoreManagerInterface - */ - private $storeManager; + private StoreManagerInterface $storeManager; - /** - * Config constructor. - * - * @param ScopeConfigInterface $scopeConfig - * @param StoreManagerInterface $storeManager - * @param string $methodCode - * @param string $pathPattern - */ public function __construct( - ScopeConfigInterface $scopeConfig, + ScopeConfigInterface $scopeConfig, StoreManagerInterface $storeManager, - $methodCode = 'vipps', - $pathPattern = OriginConfig::DEFAULT_PATH_PATTERN ) { - parent::__construct($scopeConfig, $methodCode, $pathPattern); + parent::__construct($scopeConfig, Vipps::METHOD_CODE); + $this->storeManager = $storeManager; } /** - * @param string $field - * @param null $storeId - * * @return mixed * @throws NoSuchEntityException */ @@ -64,4 +49,21 @@ public function getValue($field, $storeId = null) return OriginConfig::getValue($field, $storeId); } + + public function getVersion(): string + { + return $this->getValue('version'); + } + + public function getPaymentAction(): string + { + return $this->getValue('vipps_payment_action'); + } + + public function getTitle(): string + { + $version = $this->getValue('version'); + + return $this->getValue('title_' . $version); + } } diff --git a/Gateway/Response/TransactionHandler.php b/Gateway/Response/TransactionHandler.php index 557137cd..f3f67ea1 100644 --- a/Gateway/Response/TransactionHandler.php +++ b/Gateway/Response/TransactionHandler.php @@ -70,7 +70,7 @@ public function handle(array $handlingSubject, array $response) //@codingStandar if ($payment instanceof Payment) { $status = $transaction->getTransactionInfo()->getStatus(); - $transactionId = $transaction->getTransactionId(); + $transactionId = $transaction->getOrderId(); switch ($status) { case Transaction::TRANSACTION_STATUS_CANCELLED: diff --git a/Gateway/Transaction/TransactionBuilder.php b/Gateway/Transaction/TransactionBuilder.php index 5be9fb85..917a2518 100644 --- a/Gateway/Transaction/TransactionBuilder.php +++ b/Gateway/Transaction/TransactionBuilder.php @@ -113,7 +113,7 @@ public function setData($response) */ public function build() { - $orderId = $this->response['orderId']; + $orderId = $this->response['orderId'] ?? $this->response['reference']; $infoData = $this->response['transactionInfo'] ?? $this->response['transaction'] ?? []; $info = $this->infoFactory->create(['data' => $infoData]); diff --git a/Gateway/Validator/AvailabilityValidatorProxy.php b/Gateway/Validator/AvailabilityValidatorProxy.php new file mode 100644 index 00000000..cb2e3d30 --- /dev/null +++ b/Gateway/Validator/AvailabilityValidatorProxy.php @@ -0,0 +1,39 @@ +configVersionPool = $configVersionPool; + } + + private function get(): ValidatorInterface + { + return $this->configVersionPool->get(); + } + + public function validate(array $validationSubject) + { + return $this->get()->validate($validationSubject); + } +} diff --git a/GatewayEpayment/Command/CancelCommand.php b/GatewayEpayment/Command/CancelCommand.php new file mode 100644 index 00000000..fab11349 --- /dev/null +++ b/GatewayEpayment/Command/CancelCommand.php @@ -0,0 +1,150 @@ +paymentProvider = $paymentProvider; + $this->paymentEventLogProvider = $paymentEventLogProvider; + $this->config = $config; + $this->subjectReader = $subjectReader; + $this->orderRepository = $orderRepository; + } + + /** + * {@inheritdoc} + * + * @param array $commandSubject + * + * @return ResultInterface|array|bool|null + * @throws ClientException + * @throws ConverterException + * @throws LocalizedException + * @throws VippsException + */ + public function execute(array $commandSubject) + { + $orderId = $this->subjectReader->readPayment($commandSubject)->getOrder()->getOrderIncrementId(); + $payment = $this->paymentProvider->get($orderId); + + // try to cancel based on payment info + if ($payment->isTerminated()) { + return true; + } + + $offlineVoidEnabled = $this->config->getValue('partial_void_enabled'); + if ($payment->getAggregate()->getCapturedAmount()->getValue() > 0) { + if ($offlineVoidEnabled) { + return true; + } + throw new LocalizedException(__('Can\'t cancel captured transaction.')); + } + + return parent::execute($commandSubject); + } +} diff --git a/GatewayEpayment/Command/CaptureCommand.php b/GatewayEpayment/Command/CaptureCommand.php new file mode 100644 index 00000000..7e50fbed --- /dev/null +++ b/GatewayEpayment/Command/CaptureCommand.php @@ -0,0 +1,232 @@ +paymentProvider = $paymentProvider; + $this->paymentEventLogProvider = $paymentEventLogProvider; + $this->subjectReader = $subjectReader; + $this->orderRepository = $orderRepository; + } + + public function execute(array $commandSubject) + { + $amount = $this->subjectReader->readAmount($commandSubject); + $amount = (int)round($this->formatPrice($amount) * 100); + + if ($amount === 0) { + return true; + } + + $orderId = $this->subjectReader->readPayment($commandSubject)->getOrder()->getOrderIncrementId(); + $payment = $this->paymentProvider->get($orderId); + + // try to capture based on payment info + if ($this->captureBasedOnPayment($commandSubject, $payment)) { + return true; + } + + // try to capture based on capture service itself + if ($this->getRemainingAmountToCapture($payment) < $amount) { + throw new LocalizedException(__('Captured amount is higher then remaining amount to capture')); + } + +// $paymentEventLog = $this->paymentEventLogProvider->get($orderId); +// $requestId = $this->getFailedEventItem($paymentEventLog, $amount); +// if ($requestId) { +// $commandSubject[\Vipps\Checkout\Gateway\Http\Client\ClientInterface::HEADER_PARAM_IDEMPOTENCY_KEY] +// = $requestId; +// } + + return parent::execute($commandSubject); + } + + /** + * Try to capture based on GetPaymentDetails service. + * + * @param $commandSubject + * @param \Vipps\Payment\GatewayEpayment\Data\Payment $payment + * + * @return bool + * @throws LocalizedException + */ + private function captureBasedOnPayment($commandSubject, Payment $payment) + { + $paymentDO = $this->subjectReader->readPayment($commandSubject); + if (!$paymentDO) { + return false; + } + + $amount = $this->subjectReader->readAmount($commandSubject); + $amount = (int)round($this->formatPrice($amount) * 100); + + $orderAdapter = $paymentDO->getOrder(); + $order = $this->orderRepository->get($orderAdapter->getId()); + + $magentoTotalDue = (int)round($this->formatPrice($order->getTotalDue()) * 100); + $vippsTotalDue = $this->getRemainingAmountToCapture($payment); + + $deltaTotalDue = $magentoTotalDue - $vippsTotalDue; + if ($deltaTotalDue > 0) { + // In means that in Vipps the remainingAmountToCapture is less then in Magento + // It can happened if previous operation was successful in vipps + // but for some reason Magento didn't get response + + // Check that we are trying to capture the same amount as has been already captured in Vipps + // otherwise - show an error about desync + if ($amount === $deltaTotalDue) { + return true; + } + $suggestedAmountToCapture = $this->formatPrice($deltaTotalDue / 100); + $message = __( + 'Captured amount is not the same as you are trying to capture.' + . PHP_EOL . ' Payment information was not synced correctly between Magento and Vipps.' + . PHP_EOL . ' It might be that the previous operation was successfully completed in Vipps' + . PHP_EOL . ' but Magento did not receive a response.' + . PHP_EOL . ' To be in sync you have to capture the same amount that has been already captured' + . PHP_EOL . ' in Vipps: %1 %2', + $suggestedAmountToCapture, + $order->getStoreCurrencyCode() + ); + throw new LocalizedException($message); + } + + return false; + } + + /** + * @param Payment $payment + * + * @return int|null + */ + private function getRemainingAmountToCapture(Payment $payment): ?int + { + return $payment->getAggregate()->getAuthorizedAmount()->getValue() + - $payment->getAggregate()->getCancelledAmount()->getValue() + - $payment->getAggregate()->getCapturedAmount()->getValue() + - $payment->getAggregate()->getRefundedAmount()->getValue(); + } + + /** + * Retrieve request id of last failed operation from transaction log history. + * + * @param \Vipps\Payment\GatewayEpayment\Data\PaymentEventLog $paymentEventLog + * @param int $amount + * + * @return PaymentEventLog\Item|null + */ + private function getFailedEventItem(PaymentEventLog $paymentEventLog, $amount) + { + foreach ($paymentEventLog->getItems() as $item) { + if ($item->getPaymentAction() !== PaymentEventLog\Item::PAYMENT_ACTION_CAPTURE) { + continue; + } + if (!$item->getSuccess() && $item->getAmount() === $amount) { + return $item; + } + } + return null; + } +} diff --git a/GatewayEpayment/Command/CommandManager.php b/GatewayEpayment/Command/CommandManager.php new file mode 100644 index 00000000..0c7cc568 --- /dev/null +++ b/GatewayEpayment/Command/CommandManager.php @@ -0,0 +1,89 @@ +commandManager = $commandManager; + } + + /** + * {@inheritdoc} + * + * @param CommandInterface $command + * @param InfoInterface|null $payment + * @param array $arguments + * + * @return ResultInterface|null + * @throws CommandException + */ + public function execute(CommandInterface $command, InfoInterface $payment = null, array $arguments = []) + { + return $this->commandManager->execute($command, $payment, $arguments); + } + + /** + * @param string $commandCode + * @param InfoInterface|null $payment + * @param array $arguments + * + * @return ResultInterface|null + * @throws CommandException + * @throws NotFoundException + */ + public function executeByCode($commandCode, InfoInterface $payment = null, array $arguments = []) + { + return $this->commandManager->executeByCode($commandCode, $payment, $arguments); + } + + /** + * {@inheritdoc} + * + * @param string $commandCode + * + * @return CommandInterface + * @throws NotFoundException + */ + public function get($commandCode) + { + return $this->commandManager->get($commandCode); + } +} diff --git a/GatewayEpayment/Command/GatewayCommand.php b/GatewayEpayment/Command/GatewayCommand.php new file mode 100755 index 00000000..642951dc --- /dev/null +++ b/GatewayEpayment/Command/GatewayCommand.php @@ -0,0 +1,206 @@ +requestBuilder = $requestBuilder; + $this->transferFactory = $transferFactory; + $this->client = $client; + $this->logger = $logger; + $this->jsonDecoder = $jsonDecoder; + $this->profiler = $profiler; + $this->handler = $handler; + $this->validator = $validator; + } + + /** + * {@inheritdoc} + * + * @param array $commandSubject + * + * @return ResultInterface|array|null + * @throws ClientException + * @throws ConverterException + * @throws LocalizedException + */ + public function execute(array $commandSubject) + { + $transfer = $this->transferFactory->create( + $this->requestBuilder->build($commandSubject) + ); + + $commandSubject['transferObject'] = $transfer; + + $result = $this->client->placeRequest($transfer); + + /** @var Response $response */ + $response = $result['response']; + try { + $responseBody = $this->jsonDecoder->decode($response->getContent()); + } catch (\Exception $e) { + $responseBody = []; + } + + if ($this->profiler !== null) { + $this->profiler->save($transfer, $response); + } + + if (!$response->isSuccess()) { + $error = $this->extractError($responseBody); + $errorCode = $error['code'] ?? $response->getStatusCode(); + $errorMessage = $error['message'] ?? $response->getReasonPhrase(); + $message = sprintf( + 'Request error. Code: "%s", message: "%s"', + $errorCode, + $errorMessage + ); + $this->logger->critical($message); + throw new \Exception($errorMessage, $errorCode); + } + + /** Validating Success response body by specific command validators */ + if ($this->validator !== null) { + $validationResult = $this->validator->validate( + array_merge($commandSubject, ['jsonData' => $responseBody]) + ); + if (!$validationResult->isValid()) { + $this->logValidationFails($validationResult->getFailsDescription()); + throw new CommandException( + __('Transaction validation failed.') + ); + } + } + + /** Handling response after validation is success */ + if ($this->handler) { + $this->handler->handle($commandSubject, $responseBody); + } + + return $responseBody; + } + + /** + * @param Phrase[] $fails + * + * @return void + */ + private function logValidationFails(array $fails) + { + foreach ($fails as $failPhrase) { + $this->logger->critical((string) $failPhrase); + } + } + + /** + * Method to extract error code and message from response. + * + * @param $responseBody + * + * @return array + */ + private function extractError($responseBody) + { + return [ + 'code' => isset($responseBody[0]['errorCode']) ? $responseBody[0]['errorCode'] : null, + 'message' => isset($responseBody[0]['errorMessage']) ? $responseBody[0]['errorMessage'] : null, + ]; + } +} diff --git a/GatewayEpayment/Command/PaymentCommandManager.php b/GatewayEpayment/Command/PaymentCommandManager.php new file mode 100644 index 00000000..b14f4b4b --- /dev/null +++ b/GatewayEpayment/Command/PaymentCommandManager.php @@ -0,0 +1,89 @@ +executeByCode('get-payment', null, $arguments); + } + + public function getPaymentEventLog($orderId, $arguments = []) + { + $arguments['order_id'] = $orderId; + + return $this->executeByCode('get-payment-event-log', null, $arguments); + } + + /** + * @param OrderInterface $order + * @param array $arguments + * + * @return mixed|void + */ + public function sendReceipt(OrderInterface $order, $arguments = []) + { + $arguments['order'] = $order; + + return $this->executeByCode('send-receipt', null, $arguments); + } + + /** + * {@inheritdoc} + * + * @param InfoInterface $payment + * @param array $arguments + * + * @return ResultInterface|mixed|null + * @throws CommandException + * @throws NotFoundException + */ + public function cancel(InfoInterface $payment, $arguments = []) + { + return $this->executeByCode('cancel', $payment, $arguments); + } + + public function initiatePayment(InfoInterface $payment, $arguments) + { + $quote = $payment->getQuote(); + $quote->setReservedOrderId(null); + + $arguments[Vipps::METHOD_TYPE_KEY] = Vipps::METHOD_TYPE_EPAYMENT_CHECKOUT; + + return $this->executeByCode('initiate', $payment, $arguments); + } + + public function getPaymentDetails($arguments = []) + { + // TODO: Implement getPaymentDetails() method. + } +} diff --git a/GatewayEpayment/Command/PaymentDetailsProvider.php b/GatewayEpayment/Command/PaymentDetailsProvider.php new file mode 100644 index 00000000..7504cbe2 --- /dev/null +++ b/GatewayEpayment/Command/PaymentDetailsProvider.php @@ -0,0 +1,93 @@ +commandManager = $commandManager; + $this->paymentBuilder = $paymentBuilder; + } + + /** + * @param string $orderId + * + * @return Payment + * @throws VippsException + */ + public function get(string $orderId): ?Payment + { + if (!isset($this->cache[$orderId])) { + /** + * { + * "aggregate":{"authorizedAmount":{"currency":"NOK","value":40000},"cancelledAmount":{"currency":"NOK","value":0},"capturedAmount":{"currency":"NOK","value":0},"refundedAmount":{"currency":"NOK","value":0}}, + * "amount":{"currency":"NOK","value":40000}, + * "state":"AUTHORIZED", + * "paymentMethod":{"type":"WALLET","cardBin":"494656"}, + * "profile":[],"pspReference": + * "fd750d5c-be6a-49ec-963a-fc076e1166ae", + * "redirectUrl":"https:\/\/apitest.vipps.no\/dwo-api-application\/v1\/deeplink\/vippsgateway?v=2&token=eyJraWQiOiJqd3RrZXkiLCJhbGciOiJSUzI1NiJ9.eyJhcHAiOiJlUGF5bWVudCIsInN1YiI6ImYwMTQyYjFjLTZiMjAtNDUzYy1hOWYxLWUxZTBkYWI2OTM5NyIsIm5vIjp7IkhlYWRlciI6IkJldGFsIiwiQ29udGVudCI6IjQwMCBrciB0aWwgVmFpbW8ifSwibWVyY2hhbnRTZXJpYWxOdW1iZXIiOiIyMTE3NTIiLCJpc3MiOiJodHRwczpcL1wvVklQUFMtTVQtQ09OLUFHRU5ULWlsYi50ZWNoLTAyLm5ldFwvbXQxXC9kZWVwbGluay1vcGVuaWQtcHJvdmlkZXItYXBwbGljYXRpb25cLyIsImVuIjp7IkhlYWRlciI6IlBheSIsIkNvbnRlbnQiOiI0MDAga3IgdG8gVmFpbW8ifSwiMWZhIjoidHJ1ZSIsInR5cGUiOiJQQVlNRU5UIiwidGl0bGUiOiI0MDAga3IiLCJ1dWlkIjoiZmQ3NTBkNWMtYmU2YS00OWVjLTk2M2EtZmMwNzZlMTE2NmFlIiwicmVmZXJlbmNlSWQiOiJudmQyNDUtMDAwMDAwMTgzIiwidXJsIjoiaHR0cHM6XC9cL2FwaXRlc3QudmlwcHMubm9cL3ZpcHBzLWVwYXltZW50LWxlZ2FjeS1tb2JpbGUtYXBpXC9sYW5kaW5nLXBhZ2UiLCJhdWQiOiJmMDE0MmIxYy02YjIwLTQ1M2MtYTlmMS1lMWUwZGFiNjkzOTciLCJhenAiOiJmMDE0MmIxYy02YjIwLTQ1M2MtYTlmMS1lMWUwZGFiNjkzOTciLCJhcHBUeXBlIjoiTEFORElOR1BBR0UiLCJub0VkaXQiOmZhbHNlLCJzY29wZXMiOltdLCJleHAiOjE2OTc3MjgxNDEsInRva2VuVHlwZSI6IkRFRVBMSU5LIiwiaWF0IjoxNjk3NzI3NTQxLCJmYWxsYmFjayI6Imh0dHBzOlwvXC83NjQxLTc5LTExMC0xMzQtMTQ1Lm5ncm9rLWZyZWUuYXBwXC92aXBwc1wvcGF5bWVudFwvZmFsbGJhY2tcL3JlZmVyZW5jZVwvbnZkMjQ1LTAwMDAwMDE4M1wvIiwianRpIjoiZjA4NTg1NzQtNzIxOC00ZjY3LWExNGYtNWQxMjE3M2Q1ODY4In0.eBZFcEB_j9huNKlQnxTgSlfRCTHl1Pf3tC6x-pIqVG6CA-7ZpPyy960UvfZnC2OjhxDJ_UMdsDDjmnjBjxAbwiOf-G5q8H_E7ZIMU3h1zLJPEDzFFHZe920SDxc41eA7CiERhnIeEkUcHhsmokQdmNBjd2ODHKyC7z0MLRoE8pkfYcjig0OSYrRgpgIIzvjTmZcxy-PjSZOPv4UMCf1aPjGtz1C0TRFm5AgfuW-3fGqrhBskzfZVC6rKaA4Veg0Cquljh-PkHrj_QLriHNnhfAd0_t2xvDuebuwoCaUPzKYKCqVGkyQHpvL6rjwKxcK7BQf0QSaL-vuD7ZDuLhiFWw", + * "reference":"nvd245-000000183" + * } + */ + $response = $this->commandManager->getPayment($orderId); + $transaction = $this->paymentBuilder->setData($response)->build(); + + $this->cache[$orderId] = $transaction; + } + + return $this->cache[$orderId]; + } +} diff --git a/GatewayEpayment/Command/PaymentsPostCommand.php b/GatewayEpayment/Command/PaymentsPostCommand.php new file mode 100644 index 00000000..7a2834d6 --- /dev/null +++ b/GatewayEpayment/Command/PaymentsPostCommand.php @@ -0,0 +1,243 @@ +paymentProvider = $paymentProvider; + $this->paymentEventLogProvider = $paymentEventLogProvider; + $this->subjectReader = $subjectReader; + $this->orderRepository = $orderRepository; + } + + /** + * {@inheritdoc} + * + * @param array $commandSubject + * + * @return ResultInterface|array|bool|null + * @throws ClientException + * @throws ConverterException + * @throws LocalizedException + * @throws VippsException + */ + public function execute(array $commandSubject) + { + $amount = $this->subjectReader->readAmount($commandSubject); + $amount = (int)round($this->formatPrice($amount) * 100); + + if ($amount === 0) { + return true; + } + + $orderId = $this->subjectReader->readPayment($commandSubject)->getOrder()->getOrderIncrementId(); + $payment = $this->paymentProvider->get($orderId); + + // try to refund based on payment info + if ($this->refundBasedOnPayment($commandSubject, $payment)) { + return true; + } + + // try to refund based on refund service itself + if ($this->getRemainingAmountToRefund($payment) < $amount) { + throw new LocalizedException(__('Refunded amount is higher then remaining amount to refund')); + } + +// $paymentEventLog = $this->paymentEventLogProvider->get($orderId); +// $requestId = $this->getFailedEventItem($paymentEventLog, $amount); +// if ($requestId) { +// $commandSubject[\Vipps\Payment\GatewayEpayment\Http\Client\ClientInterface::HEADER_PARAM_IDEMPOTENCY_KEY] +// = $requestId; +// } + + return parent::execute($commandSubject); + } + + /** + * Try to refund based on GetPaymentDetails service. + * + * @param $commandSubject + * @param Payment $payment + * + * @return bool + * @throws LocalizedException + */ + private function refundBasedOnPayment($commandSubject, Payment $payment) + { + $paymentDO = $this->subjectReader->readPayment($commandSubject); + if (!$paymentDO) { + return false; + } + + $amount = $this->subjectReader->readAmount($commandSubject); + $amount = (int)round($this->formatPrice($amount) * 100); + + $orderAdapter = $paymentDO->getOrder(); + + $order = $this->orderRepository->get($orderAdapter->getId()); + + $magentoTotalRefunded = (int)round($this->formatPrice($order->getTotalRefunded()) * 100); + $vippsTotalRefunded = $payment->getAggregate()->getRefundedAmount()->getValue(); + + $deltaTotalRefunded = $vippsTotalRefunded - $magentoTotalRefunded; + if ($deltaTotalRefunded > 0) { + // In means that in Vipps the refunded amount is higher then in Magento + // It can happened if previous operation was successful in vipps + // but for some reason Magento didn't get response + + // Check that we are trying to refund the same amount as has been already refunded in Vipps + // otherwise - show an error about desync + if ($amount !== $deltaTotalRefunded) { + $suggestedAmountToRefund = $this->formatPrice($deltaTotalRefunded / 100); + $message = __( + 'Refunded amount is not the same as you are trying to refund.' + . PHP_EOL . ' Payment information was not synced correctly between Magento and Vipps.' + . PHP_EOL . ' It might be that the previous operation was successfully completed in Vipps' + . PHP_EOL . ' but Magento did not receive a response.' + . PHP_EOL . ' To be in sync you have to refund the same amount that has been already refunded' + . PHP_EOL . ' in Vipps: %1 %2', + $suggestedAmountToRefund, + $order->getStoreCurrencyCode() + ); + + throw new LocalizedException($message); + } + + return true; + } + + return false; + } + + /** + * @param Payment $payment + * + * @return int|null + */ + private function getRemainingAmountToRefund(Payment $payment): ?int + { + return $payment->getAggregate()->getCapturedAmount()->getValue(); + } + + /** + * Retrieve request id of last failed operation from transaction log history. + * + * @param PaymentEventLog $paymentEventLog + * @param int $amount + * + * @return PaymentEventLog\Item|null + */ + private function getFailedEventItem(PaymentEventLog $paymentEventLog, $amount) + { + foreach ($paymentEventLog->getItems() as $item) { + if ($item->getPaymentAction() !== PaymentEventLog\Item::PAYMENT_ACTION_REFUND) { + continue; + } + if (!$item->getSuccess() && $item->getAmount() === $amount) { + return $item; + } + } + return null; + } +} diff --git a/GatewayEpayment/Command/RefundCommand.php b/GatewayEpayment/Command/RefundCommand.php new file mode 100644 index 00000000..13a3c492 --- /dev/null +++ b/GatewayEpayment/Command/RefundCommand.php @@ -0,0 +1,243 @@ +paymentProvider = $paymentProvider; + $this->paymentEventLogProvider = $paymentEventLogProvider; + $this->subjectReader = $subjectReader; + $this->orderRepository = $orderRepository; + } + + /** + * {@inheritdoc} + * + * @param array $commandSubject + * + * @return ResultInterface|array|bool|null + * @throws ClientException + * @throws ConverterException + * @throws LocalizedException + * @throws VippsException + */ + public function execute(array $commandSubject) + { + $amount = $this->subjectReader->readAmount($commandSubject); + $amount = (int)round($this->formatPrice($amount) * 100); + + if ($amount === 0) { + return true; + } + + $orderId = $this->subjectReader->readPayment($commandSubject)->getOrder()->getOrderIncrementId(); + $payment = $this->paymentProvider->get($orderId); + + // try to refund based on payment info + if ($this->refundBasedOnPayment($commandSubject, $payment)) { + return true; + } + + // try to refund based on refund service itself + if ($this->getRemainingAmountToRefund($payment) < $amount) { + throw new LocalizedException(__('Refunded amount is higher then remaining amount to refund')); + } + +// $paymentEventLog = $this->paymentEventLogProvider->get($orderId); +// $requestId = $this->getFailedEventItem($paymentEventLog, $amount); +// if ($requestId) { +// $commandSubject[\Vipps\Payment\GatewayEpayment\Http\Client\ClientInterface::HEADER_PARAM_IDEMPOTENCY_KEY] +// = $requestId; +// } + + return parent::execute($commandSubject); + } + + /** + * Try to refund based on GetPaymentDetails service. + * + * @param $commandSubject + * @param Payment $payment + * + * @return bool + * @throws LocalizedException + */ + private function refundBasedOnPayment($commandSubject, Payment $payment) + { + $paymentDO = $this->subjectReader->readPayment($commandSubject); + if (!$paymentDO) { + return false; + } + + $amount = $this->subjectReader->readAmount($commandSubject); + $amount = (int)round($this->formatPrice($amount) * 100); + + $orderAdapter = $paymentDO->getOrder(); + + $order = $this->orderRepository->get($orderAdapter->getId()); + + $magentoTotalRefunded = (int)round($this->formatPrice($order->getTotalRefunded()) * 100); + $vippsTotalRefunded = $payment->getAggregate()->getRefundedAmount()->getValue(); + + $deltaTotalRefunded = $vippsTotalRefunded - $magentoTotalRefunded; + if ($deltaTotalRefunded > 0) { + // In means that in Vipps the refunded amount is higher then in Magento + // It can happened if previous operation was successful in vipps + // but for some reason Magento didn't get response + + // Check that we are trying to refund the same amount as has been already refunded in Vipps + // otherwise - show an error about desync + if ($amount !== $deltaTotalRefunded) { + $suggestedAmountToRefund = $this->formatPrice($deltaTotalRefunded / 100); + $message = __( + 'Refunded amount is not the same as you are trying to refund.' + . PHP_EOL . ' Payment information was not synced correctly between Magento and Vipps.' + . PHP_EOL . ' It might be that the previous operation was successfully completed in Vipps' + . PHP_EOL . ' but Magento did not receive a response.' + . PHP_EOL . ' To be in sync you have to refund the same amount that has been already refunded' + . PHP_EOL . ' in Vipps: %1 %2', + $suggestedAmountToRefund, + $order->getStoreCurrencyCode() + ); + + throw new LocalizedException($message); + } + + return true; + } + + return false; + } + + /** + * @param Payment $payment + * + * @return int|null + */ + private function getRemainingAmountToRefund(Payment $payment): ?int + { + return $payment->getAggregate()->getCapturedAmount()->getValue(); + } + + /** + * Retrieve request id of last failed operation from transaction log history. + * + * @param PaymentEventLog $paymentEventLog + * @param int $amount + * + * @return PaymentEventLog\Item|null + */ + private function getFailedEventItem(PaymentEventLog $paymentEventLog, $amount) + { + foreach ($paymentEventLog->getItems() as $item) { + if ($item->getPaymentAction() !== PaymentEventLog\Item::PAYMENT_ACTION_REFUND) { + continue; + } + if (!$item->getSuccess() && $item->getAmount() === $amount) { + return $item; + } + } + return null; + } +} diff --git a/GatewayEpayment/Data/Aggregate.php b/GatewayEpayment/Data/Aggregate.php new file mode 100644 index 00000000..d08c4e08 --- /dev/null +++ b/GatewayEpayment/Data/Aggregate.php @@ -0,0 +1,75 @@ +getData(self::CANCELLED_AMOUNT); + } + + /** + * @return string + */ + public function getCapturedAmount(): Amount + { + return $this->getData(self::CAPTURED_AMOUNT); + } + + /** + * @return string + */ + public function getRefundedAmount(): Amount + { + return $this->getData(self::REFUNDED_AMOUNT); + } + + /** + * @return string + */ + public function getAuthorizedAmount(): Amount + { + return $this->getData(self::AUTHORIZED_AMOUNT); + } +} diff --git a/GatewayEpayment/Data/AggregateFactory.php b/GatewayEpayment/Data/AggregateFactory.php new file mode 100644 index 00000000..d2685cd2 --- /dev/null +++ b/GatewayEpayment/Data/AggregateFactory.php @@ -0,0 +1,72 @@ +objectManager = $objectManager; + $this->amountFactory = $amountFactory; + $this->instanceName = $instanceName; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Vipps\Payment\GatewayEpayment\Data\Aggregate + */ + public function create(array $data = []) + { + $objData = [ + 'cancelledAmount' => + $this->amountFactory->create([ + 'data' => (array)($data['cancelledAmount'] ?? null) + ]), + 'capturedAmount' => + $this->amountFactory->create([ + 'data' => (array)($data['capturedAmount'] ?? null) + ]), + 'refundedAmount' => + $this->amountFactory->create([ + 'data' => (array)($data['refundedAmount'] ?? null) + ]), + 'authorizedAmount' => + $this->amountFactory->create([ + 'data' => (array)($data['authorizedAmount'] ?? null) + ]), + ]; + + return $this->objectManager->create($this->instanceName, ['data' => $objData]); + } +} diff --git a/GatewayEpayment/Data/Amount.php b/GatewayEpayment/Data/Amount.php new file mode 100644 index 00000000..4e2e1d3f --- /dev/null +++ b/GatewayEpayment/Data/Amount.php @@ -0,0 +1,52 @@ +getData(self::CURRENCY); + } + + /** + * @return int|null + */ + public function getValue() + { + return $this->getData(self::VALUE); + } +} diff --git a/GatewayEpayment/Data/BillingDetails.php b/GatewayEpayment/Data/BillingDetails.php new file mode 100644 index 00000000..229465d7 --- /dev/null +++ b/GatewayEpayment/Data/BillingDetails.php @@ -0,0 +1,122 @@ +getData(self::FIRST_NAME); + } + + /** + * @return string + */ + public function getLastName() + { + return $this->getData(self::LAST_NAME); + } + + /** + * @return string + */ + public function getEmail() + { + return $this->getData(self::EMAIL); + } + + /** + * @return string + */ + public function getPhoneNumber() + { + return $this->getData(self::PHONE_NUMBER); + } + + /** + * @return string + */ + public function getStreetAddress() + { + return $this->getData(self::STREET_ADDRESS); + } + + /** + * @return string|null + */ + public function getPostalCode() + { + return $this->getData(self::POSTAL_CODE); + } + + /** + * @return string + */ + public function getCity() + { + return $this->getData(self::CITY); + } + + /** + * @return string + */ + public function getCountry() + { + return $this->getData(self::COUNTRY); + } +} diff --git a/GatewayEpayment/Data/Customer.php b/GatewayEpayment/Data/Customer.php new file mode 100644 index 00000000..c8a59fde --- /dev/null +++ b/GatewayEpayment/Data/Customer.php @@ -0,0 +1,52 @@ +getData(self::PHONE_NUMBER); + } + + /** + * @return string + */ + public function getSub(): string + { + return (string)$this->getData(self::SUB); + } +} diff --git a/GatewayEpayment/Data/InitSession.php b/GatewayEpayment/Data/InitSession.php new file mode 100644 index 00000000..61ba513b --- /dev/null +++ b/GatewayEpayment/Data/InitSession.php @@ -0,0 +1,63 @@ +getData(self::TOKEN); + } + + /** + * @return string + */ + public function getCheckoutFrontendUrl() + { + return $this->getData(self::CHECKOUT_FRONTEND_URL); + } + + /** + * @return string + */ + public function getPollingUrl() + { + return $this->getData(self::POLLING_URL); + } +} diff --git a/GatewayEpayment/Data/InitSessionBuilder.php b/GatewayEpayment/Data/InitSessionBuilder.php new file mode 100644 index 00000000..72cfe9cc --- /dev/null +++ b/GatewayEpayment/Data/InitSessionBuilder.php @@ -0,0 +1,72 @@ +initSessionFactory = $initSessionFactory; + } + + /** + * Set request to builder + * + * @param $response + * + * @return $this + */ + public function setData($response) + { + $this->response = $response; + return $this; + } + + /** + * build session object + * + * @return InitSession + */ + public function build() + { + return $this->initSessionFactory->create(['data' => [ + 'token' => $this->response['token'] ?? null, + 'checkoutFrontendUrl' => $this->response['checkoutFrontendUrl'] ?? null, + 'pollingUrl' => $this->response['pollingUrl'] ?? null, + ]]); + } +} diff --git a/GatewayEpayment/Data/Payment.php b/GatewayEpayment/Data/Payment.php new file mode 100644 index 00000000..be4a3e92 --- /dev/null +++ b/GatewayEpayment/Data/Payment.php @@ -0,0 +1,225 @@ +getData(self::AGGREGATE); + } + + /** + * @return Amount + */ + public function getAmount(): Amount + { + return $this->getData(self::AMOUNT); + } + + /** + * @return string + */ + public function getAuthorisationType(): string + { + return (string)$this->getData(self::AUTHORISATION_TYPE); + } + + /** + * @return string + */ + public function getState(): string + { + return \strtolower($this->getData(self::STATE)); + } + + /** + * @return bool + */ + public function getDirectCapture(): bool + { + return (bool)$this->getData(self::DIRECT_CAPTURE); + } + + /** + * @return Customer|null + */ + public function getCustomer(): ?Customer + { + return $this->getData(self::CUSTOMER); + } + + /** + * @return string + */ + public function getCustomerInteraction(): string + { + return (string)$this->getData(self::CUSTOMER_INTERACTION); + } + + /** + * @return PaymentMethod + */ + public function getPaymentMethod(): PaymentMethod + { + return $this->getData(self::PAYMENT_METHOD); + } + + /** + * @return string + */ + public function getPaymentDescription(): string + { + return (string)$this->getData(self::PAYMENT_DESCRIPTION); + } + + /** + * @return string + */ + public function getPspReference(): string + { + return (string)$this->getData(self::PSP_REFERENCE); + } + + /** + * @return string + */ + public function getReference(): string + { + return (string)$this->getData(self::REFERENCE); + } + + /** + * @return string + */ + public function getSubReference(): string + { + return (string)$this->getData(self::SUB_REFERENCE); + } + + /** + * @return string + */ + public function getUserFlow(): string + { + return (string)$this->getData(self::USER_FLOW); + } + + /** + * @return Profile + */ + public function getProfile(): Profile + { + return $this->getData(self::PROFILE); + } + + public function isCreated() + { + return $this->getState() === self::STATE_CREATED; + } + + public function isAborter() + { + return $this->getState() === self::STATE_ABORTED; + } + + public function isExpired() + { + return $this->getState() === self::STATE_EXPIRED; + } + + public function isAuthorised() + { + return $this->getState() === self::STATE_AUTHORIZED; + } + + public function isTerminated() + { + return $this->getState() === self::STATE_TERMINATED; + } + + public function getRawData(): string + { + return (string)$this->getData('raw_data'); + } +} diff --git a/GatewayEpayment/Data/PaymentBuilder.php b/GatewayEpayment/Data/PaymentBuilder.php new file mode 100644 index 00000000..cb954005 --- /dev/null +++ b/GatewayEpayment/Data/PaymentBuilder.php @@ -0,0 +1,88 @@ +paymentFactory = $paymentFactory; + $this->aggregateFactory = $aggregateFactory; + $this->amountFactory = $amountFactory; + $this->customerFactory = $customerFactory; + $this->paymentMethodFactory = $paymentMethodFactory; + $this->profileFactory = $profileFactory; + } + + /** + * Set request to builder + * + * @param array $response + * + * @return $this + */ + public function setData(array $response) + { + $this->response = $response; + return $this; + } + + /** + * build session object + * + * @return Payment + */ + public function build() + { + return $this->paymentFactory->create([ + 'data' => array_merge( + $this->response, + [ + 'aggregate' => + $this->aggregateFactory->create((array)($this->response['aggregate'] ?? null)), + 'amount' => $this->amountFactory->create((array)($this->response['amount'] ?? null)), + 'customer' => $this->customerFactory->create((array)($this->response['customer'] ?? null)), + 'paymentMethod' => + $this->paymentMethodFactory->create((array)($this->response['paymentMethod'] ?? null)), + 'profile' => $this->profileFactory->create((array)($this->response['profile'] ?? null)), + 'raw_data' => \json_encode($this->response) + ] + ) + ]); + } +} diff --git a/GatewayEpayment/Data/PaymentDetails.php b/GatewayEpayment/Data/PaymentDetails.php new file mode 100644 index 00000000..a99acdab --- /dev/null +++ b/GatewayEpayment/Data/PaymentDetails.php @@ -0,0 +1,89 @@ +getData(self::AMOUNT); + } + + /** + * @return string + */ + public function getState() + { + return strtolower((string)$this->getData(self::STATE)); + } + + /** + * @return + */ + public function getAggregate(): Aggregate + { + return $this->getData(self::AGGREGATE); + } + + public function isCreated() + { + return $this->getState() === self::STATE_CREATED; + } + + public function isAuthorised() + { + return $this->getState() === self::STATE_AUTHORIZED; + } + + public function isTerminated() + { + return $this->getState() === self::STATE_TERMINATED; + } +} diff --git a/GatewayEpayment/Data/PaymentEventLog.php b/GatewayEpayment/Data/PaymentEventLog.php new file mode 100644 index 00000000..c4b752e9 --- /dev/null +++ b/GatewayEpayment/Data/PaymentEventLog.php @@ -0,0 +1,39 @@ +getData(self::ITEMS); + } +} diff --git a/GatewayEpayment/Data/PaymentEventLog/Item.php b/GatewayEpayment/Data/PaymentEventLog/Item.php new file mode 100644 index 00000000..8936142b --- /dev/null +++ b/GatewayEpayment/Data/PaymentEventLog/Item.php @@ -0,0 +1,137 @@ +getData(self::REFERENCE); + } + + public function getPspReference(): ?string + { + return $this->getData(self::PSP_REFERENCE); + } + + public function getPaymentAction(): ?string + { + return $this->getData(self::PAYMENT_ACTION); + } + + public function getAmount(): Amount + { + return $this->getData(self::AMOUNT); + } + + public function getAuthorisationType(): ?string + { + return $this->getData(self::AUTHORISATION_TYPE); + } + + public function getProcessedAt(): ?string + { + return $this->getData(self::PROCESSED_AT); + } + + public function getIdempotencyKey(): ?string + { + return $this->getData(self::IDEMPOTENCY_KEY); + } + + public function getSuccess(): bool + { + return (bool)$this->getData(self::SUCCESS); + } +} diff --git a/GatewayEpayment/Data/PaymentEventLogBuilder.php b/GatewayEpayment/Data/PaymentEventLogBuilder.php new file mode 100644 index 00000000..bb84b23c --- /dev/null +++ b/GatewayEpayment/Data/PaymentEventLogBuilder.php @@ -0,0 +1,95 @@ +itemFactory = $itemFactory; + $this->paymentEventLogFactory = $paymentEventLogFactory; + $this->amountFactory = $amountFactory; + } + + /** + * Set request to builder + * + * @param array $response + * + * @return $this + */ + public function setData(array $response) + { + $this->response = $response; + + return $this; + } + + /** + * @return PaymentEventLog + */ + public function build() + { + $items = []; + foreach ($this->response as $itemData) { + $items[] = $this->itemFactory->create([ + 'data' => array_merge( + (array)$itemData, + [ + 'amount' => $this->amountFactory->create([ + 'data' => (array)($itemData['amount'] ?? null) + ]) + ] + ) + ]); + } + + return $this->paymentEventLogFactory->create(['data' => ['items' => $items]]); + } +} diff --git a/GatewayEpayment/Data/PaymentMethod.php b/GatewayEpayment/Data/PaymentMethod.php new file mode 100644 index 00000000..311ed1c0 --- /dev/null +++ b/GatewayEpayment/Data/PaymentMethod.php @@ -0,0 +1,39 @@ +getData(self::TYPE); + } +} diff --git a/GatewayEpayment/Data/Profile.php b/GatewayEpayment/Data/Profile.php new file mode 100644 index 00000000..63b27d91 --- /dev/null +++ b/GatewayEpayment/Data/Profile.php @@ -0,0 +1,52 @@ +getData(self::SCOPE); + } + + /** + * @return string + */ + public function getSub() + { + return $this->getData(self::SUB); + } +} diff --git a/GatewayEpayment/Data/Session.php b/GatewayEpayment/Data/Session.php new file mode 100644 index 00000000..baaf6bc8 --- /dev/null +++ b/GatewayEpayment/Data/Session.php @@ -0,0 +1,136 @@ +getData(self::SESSION_ID); + } + + /** + * @return string + */ + public function getReference() + { + return $this->getData(self::REFERENCE); + } + + /** + * @return string + */ + public function getSessionState() + { + return $this->getData(self::SESSION_STATE); + } + + /** + * @return string + */ + public function getPaymentMethod() + { + return $this->getData(self::PAYMENT_METHOD); + } + + /** + * @return string + */ + public function getPaymentDetails(): PaymentDetails + { + return $this->getData(self::PAYMENT_DETAILS); + } + + /** + * @return string + */ + public function getShippingDetails(): ShippingDetails + { + return $this->getData(self::SHIPPING_DETAILS); + } + + /** + * @return string + */ + public function getBillingDetails(): BillingDetails + { + return $this->getData(self::BILLING_DETAILS); + } + + public function isSessionExpired(): bool + { + return $this->getSessionState() === 'SessionExpired'; + } + + public function isSessionCreated(): bool + { + return $this->getSessionState() === 'SessionCreated'; + } + + public function isPaymentInitiated(): bool + { + return $this->getSessionState() === 'PaymentInitiated'; + } + + public function isPaymentSuccessful(): bool + { + return $this->getSessionState() === 'PaymentSuccessful'; + } + + public function isPaymentTerminated(): bool + { + return $this->getSessionState() === 'PaymentTerminated'; + } +} diff --git a/GatewayEpayment/Data/SessionBuilder.php b/GatewayEpayment/Data/SessionBuilder.php new file mode 100644 index 00000000..0b3e2d9c --- /dev/null +++ b/GatewayEpayment/Data/SessionBuilder.php @@ -0,0 +1,130 @@ +sessionFactory = $sessionFactory; + $this->paymentDetailsFactory = $paymentDetailsFactory; + $this->amountFactory = $amountFactory; + $this->billingDetailsFactory = $billingDetailsFactory; + $this->shippingDetailsFactory = $shippingDetailsFactory; + $this->aggregateFactory = $aggregateFactory; + } + + /** + * Set request to builder + * + * @param $response + * + * @return $this + */ + public function setData($response) + { + $this->response = $response; + return $this; + } + + /** + * build session object + * + * @return Session + */ + public function build() + { + $paymentDetails = $this->paymentDetailsFactory->create(['data' => [ + 'amount' => $this->amountFactory->create([ + 'data' => (array)($this->response['paymentDetails']['amount'] ?? null) + ]), + 'state' => $this->response['paymentDetails']['state'] ?? null, + 'aggregate' => $this->aggregateFactory->create([ + 'data' => [(array)($this->response['paymentDetails']['aggregate'] ?? null)] + ]) + ]]); + + return $this->sessionFactory->create(['data' => [ + 'sessionId' => $this->response['sessionId'] ?? null, + 'reference' => $this->response['reference'] ?? null, + 'sessionState' => $this->response['sessionState'] ?? null, + 'paymentMethod' => $this->response['paymentMethod'] ?? null, + 'paymentDetails' => $paymentDetails, + 'shippingDetails' => $this->shippingDetailsFactory->create([ + 'data' => (array)($this->response['shippingDetails'] ?? null) + ]), + 'billingDetails' => $this->billingDetailsFactory->create([ + 'data' => (array)($this->response['billingDetails'] ?? null) + ]), + ]]); + } +} diff --git a/GatewayEpayment/Data/ShippingDetails.php b/GatewayEpayment/Data/ShippingDetails.php new file mode 100644 index 00000000..f8fb4d72 --- /dev/null +++ b/GatewayEpayment/Data/ShippingDetails.php @@ -0,0 +1,134 @@ +getData(self::FIRST_NAME); + } + + /** + * @return string + */ + public function getLastName() + { + return $this->getData(self::LAST_NAME); + } + + /** + * @return string + */ + public function getEmail() + { + return $this->getData(self::EMAIL); + } + + /** + * @return string + */ + public function getPhoneNumber() + { + return $this->getData(self::PHONE_NUMBER); + } + + /** + * @return string + */ + public function getStreetAddress() + { + return $this->getData(self::STREET_ADDRESS); + } + + /** + * @return string|null + */ + public function getPostalCode() + { + return $this->getData(self::POSTAL_CODE); + } + + /** + * @return string + */ + public function getCity() + { + return $this->getData(self::CITY); + } + + /** + * @return string + */ + public function getCountry() + { + return $this->getData(self::COUNTRY); + } + + /** + * @return string + */ + public function getShippingMethodId() + { + return $this->getData(self::SHIPPING_METHOD_ID); + } +} diff --git a/GatewayEpayment/Exception/AuthenticationException.php b/GatewayEpayment/Exception/AuthenticationException.php new file mode 100644 index 00000000..415c78c4 --- /dev/null +++ b/GatewayEpayment/Exception/AuthenticationException.php @@ -0,0 +1,24 @@ +config = $config; + $this->adapterFactory = $adapterFactory; + $this->jsonEncoder = $jsonEncoder; + $this->logger = $logger; + $this->moduleMetadata = $moduleMetadata; + } + + /** + * @param TransferInterface $transfer + * + * @return array|string + * @throws \Exception + */ + public function placeRequest(TransferInterface $transfer) + { + try { + $response = $this->place($transfer); + + return ['response' => $response]; + } catch (\Throwable $t) { + $this->logger->critical($t->__toString()); + throw new \Exception($t->getMessage(), $t->getCode(), $t); //@codingStandardsIgnoreLine + } + } + + /** + * @param TransferInterface $transfer + * + * @return Response + */ + private function place(TransferInterface $transfer) + { + try { + $adapter = null; + /** @var MagentoCurl $adapter */ + $adapter = $this->adapterFactory->create(); + $options = $this->getBasicOptions(); + $requestBody = $transfer->getBody(); + unset($requestBody['payment']); + if ($transfer->getMethod() === Request::METHOD_PUT) { + $options += [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => Request::METHOD_PUT, + CURLOPT_POSTFIELDS => $this->jsonEncoder->encode($requestBody) + ]; + } + $adapter->setOptions($options); + $headers = $this->getHeaders($transfer->getHeaders()); + // send request + $adapter->write( + $transfer->getMethod(), + $transfer->getUri(), + '1.1', + $headers, + $this->jsonEncoder->encode($transfer->getBody()) + ); + + $response = $adapter->read(); + + return Response::fromString($response); + } finally { + $adapter ? $adapter->close() : null; + } + } + + /** + * @param $headers + * + * @return array + */ + private function getHeaders($headers) + { + $headers = array_merge( + [ + self::HEADER_PARAM_CONTENT_TYPE => 'application/json', + self::HEADER_PARAM_IDEMPOTENCY_KEY => '', + self::HEADER_PARAM_X_SOURCE_ADDRESS => '', + self::HEADER_PARAM_X_TIMESTAMP => '', + self::HEADER_PARAM_MERCHANT_SERIAL_NUMBER => $this->config->getValue('merchant_serial_number'), + self::HEADER_PARAM_CLIENT_ID => $this->config->getValue('client_id'), + self::HEADER_PARAM_CLIENT_SECRET => $this->config->getValue('client_secret'), + self::HEADER_PARAM_SUBSCRIPTION_KEY => $this->config->getValue('subscription_key1'), + ], + $headers + ); + + $headers = $this->moduleMetadata->addOptionalHeaders($headers); + + $result = []; + foreach ($headers as $key => $value) { + $result[] = sprintf('%s: %s', $key, $value); + } + + return $result; + } + + /** + * @return array + */ + private function getBasicOptions() + { + return [ + CURLOPT_TIMEOUT => 30, + ]; + } +} diff --git a/GatewayEpayment/Http/Client/ClientInterface.php b/GatewayEpayment/Http/Client/ClientInterface.php new file mode 100644 index 00000000..960bce09 --- /dev/null +++ b/GatewayEpayment/Http/Client/ClientInterface.php @@ -0,0 +1,70 @@ +config = $config; + $this->adapterFactory = $adapterFactory; + $this->tokenProvider = $tokenProvider; + $this->jsonEncoder = $jsonEncoder; + $this->logger = $logger; + $this->productMetadata = $moduleMetadata; + } + + /** + * @param TransferInterface $transfer + * + * @return array|string + * @throws \Exception + */ + public function placeRequest(TransferInterface $transfer) + { + try { + $response = $this->place($transfer); + if ($response->getStatusCode() == Response::STATUS_CODE_401) { + $this->tokenProvider->regenerate(); + $response = $this->place($transfer); + } + + return ['response' => $response]; + } catch (\Throwable $t) { + $this->logger->critical($t->__toString()); + throw new \Exception($t->getMessage(), $t->getCode(), $t); //@codingStandardsIgnoreLine + } + } + + /** + * @param TransferInterface $transfer + * + * @return Response + * @throws AuthenticationException + */ + private function place(TransferInterface $transfer) + { + try { + $adapter = null; + /** @var MagentoCurl $adapter */ + $adapter = $this->adapterFactory->create(); + $options = $this->getBasicOptions(); + $requestBody = $transfer->getBody(); + if ($transfer->getMethod() === Request::METHOD_PUT) { + $options = $options + + [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => Request::METHOD_PUT, + CURLOPT_POSTFIELDS => $this->jsonEncoder->encode($requestBody) + ]; + } + $adapter->setOptions($options); + $headers = $this->getHeaders($transfer->getHeaders()); + // send request + $adapter->write( + $transfer->getMethod(), + $transfer->getUri(), + '1.1', + $headers, + $this->jsonEncoder->encode($transfer->getBody()) + ); + + return Response::fromString($adapter->read()); + } finally { + $adapter ? $adapter->close() : null; + } + } + + /** + * @param $headers + * + * @return array + * @throws AuthenticationException + */ + private function getHeaders($headers) + { + $headers = array_merge( + [ + self::HEADER_PARAM_CONTENT_TYPE => 'application/json', + self::HEADER_PARAM_AUTHORIZATION => 'Bearer ' . $this->tokenProvider->get(), + self::HEADER_PARAM_IDEMPOTENCY_KEY => '', + self::HEADER_PARAM_X_SOURCE_ADDRESS => '', + self::HEADER_PARAM_X_TIMESTAMP => '', + self::HEADER_PARAM_MERCHANT_SERIAL_NUMBER => $this->config->getValue('merchant_serial_number'), + self::HEADER_PARAM_SUBSCRIPTION_KEY => $this->config->getValue('subscription_key1'), + ], + $headers + ); + + $headers = $this->productMetadata->addOptionalHeaders($headers); + + $result = []; + foreach ($headers as $key => $value) { + $result[] = sprintf('%s: %s', $key, $value); + } + + return $result; + } + + /** + * @return array + */ + private function getBasicOptions() + { + return [ + CURLOPT_TIMEOUT => 30, + ]; + } +} diff --git a/GatewayEpayment/Http/Transfer.php b/GatewayEpayment/Http/Transfer.php new file mode 100644 index 00000000..f5e99a60 --- /dev/null +++ b/GatewayEpayment/Http/Transfer.php @@ -0,0 +1,190 @@ +clientConfig = $clientConfig; + $this->headers = $headers; + $this->body = $body; + $this->auth = $auth; + $this->method = $method; + $this->uri = $uri; + $this->encode = $encode; + $this->urlParameters = $urlParameters; + } + + /** + * Returns gateway client configuration + * + * @return array + */ + public function getClientConfig() + { + return $this->clientConfig; + } + + /** + * Returns method used to place request + * + * @return string|int + */ + public function getMethod() + { + return (string)$this->method; + } + + /** + * Returns headers + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Returns request body + * + * @return array|string + */ + public function getBody() + { + return $this->body; + } + + /** + * Returns URI + * + * @return string + */ + public function getUri() + { + return (string)$this->uri; + } + + /** + * @return boolean + */ + public function shouldEncode() + { + return $this->encode; + } + + /** + * Returns Auth username + * + * @return string + */ + public function getAuthUsername() + { + return $this->auth[self::AUTH_USERNAME]; + } + + /** + * Returns Auth password + * + * @return string + */ + public function getAuthPassword() + { + return $this->auth[self::AUTH_PASSWORD]; + } + + public function getUrlParameters($name = null) + { + if ($name !== null) { + return $this->urlParameters[$name] ?? null; + } + + return $this->urlParameters; + } +} diff --git a/GatewayEpayment/Http/TransferBuilder.php b/GatewayEpayment/Http/TransferBuilder.php new file mode 100644 index 00000000..339dc15c --- /dev/null +++ b/GatewayEpayment/Http/TransferBuilder.php @@ -0,0 +1,170 @@ + null, Transfer::AUTH_PASSWORD => null]; + /** + * @var array + */ + private $urlParameters; + + /** + * @param array $clientConfig + * @return $this + */ + public function setClientConfig(array $clientConfig) + { + $this->clientConfig = $clientConfig; + + return $this; + } + + /** + * @param array $headers + * @return $this + */ + public function setHeaders(array $headers) + { + $this->headers = $headers; + + return $this; + } + + /** + * @param array|string $body + * @return $this + */ + public function setBody($body) + { + $this->body = $body; + + return $this; + } + + /** + * @param string $username + * @return $this + */ + public function setAuthUsername($username) + { + $this->auth[Transfer::AUTH_USERNAME] = $username; + + return $this; + } + + /** + * @param string $password + * @return $this + */ + public function setAuthPassword($password) + { + $this->auth[Transfer::AUTH_PASSWORD] = $password; + + return $this; + } + + /** + * @param string $method + * @return $this + */ + public function setMethod($method) + { + $this->method = $method; + + return $this; + } + + /** + * @param string $uri + * @return $this + */ + public function setUri($uri) + { + $this->uri = $uri; + + return $this; + } + + /** + * @param bool $encode + * @return $this + */ + public function shouldEncode($encode) + { + $this->encode = $encode; + + return $this; + } + + /** + * @param $parameters + */ + public function setUrlParameters($parameters) + { + $this->urlParameters = $parameters; + } + + /** + * @return TransferInterface + */ + public function build() + { + return new Transfer( + $this->clientConfig, + $this->headers, + $this->body, + $this->auth, + $this->method, + $this->uri, + $this->encode, + $this->urlParameters + ); + } +} diff --git a/GatewayEpayment/Http/TransferFactory.php b/GatewayEpayment/Http/TransferFactory.php new file mode 100644 index 00000000..5d8fca3e --- /dev/null +++ b/GatewayEpayment/Http/TransferFactory.php @@ -0,0 +1,160 @@ +transferBuilder = $transferBuilder; + $this->urlResolver = $urlResolver; + $this->method = $method; + $this->endpointUrl = $endpointUrl; + $this->urlParams = $urlParams; + $this->allowedBodyKeys = $allowedBodyKeys; + } + + /** + * Builds gateway transfer object + * + * @param array $request + * + * @return TransferInterface + * @throws \Exception + */ + public function create(array $request) + { + $this->transferBuilder->setHeaders([ + ClientInterface::HEADER_PARAM_IDEMPOTENCY_KEY => + $request[ClientInterface::HEADER_PARAM_IDEMPOTENCY_KEY] ?? $this->generateRequestId() + ]); + + $this->transferBuilder + ->setBody($this->getBody($request)) + ->setMethod($this->method) + ->setUri($this->getUrl($request)) + ->setUrlParameters($this->getUrlParameters()); + + return $this->transferBuilder->build(); + } + + /** + * Generating Url. + * + * @param $request + * + * @return string + */ + private function getUrl(array $request = []) + { + $endpointUrl = $this->endpointUrl; + /** Binding url parameters if they were specified */ + foreach ($this->urlParams as $paramKey => $paramValue) { + if (isset($request[$paramKey])) { + $endpointUrl = str_replace(':' . $paramKey, $request[$paramKey], $this->endpointUrl); + $this->urlParams[$paramKey] = $request[$paramKey]; + } + } + return $this->urlResolver->getUrl($endpointUrl); + } + + /** + * Method to get needed content body from request. + * + * @param array $request + * + * @return array + */ + private function getBody(array $request = []) + { + $body = []; + foreach ($this->allowedBodyKeys as $key) { + if (isset($request[$key])) { + $body[$key] = $request[$key]; + } + } + + return $body; + } + + private function getUrlParameters() + { + return $this->urlParams; + } + + /** + * Generate value of request id for current request + * + * @return string + */ + private function generateRequestId() + { + return uniqid('req-id-', true); + } +} diff --git a/GatewayEpayment/Http/TransferFactoryInterface.php b/GatewayEpayment/Http/TransferFactoryInterface.php new file mode 100644 index 00000000..bb9781bb --- /dev/null +++ b/GatewayEpayment/Http/TransferFactoryInterface.php @@ -0,0 +1,33 @@ +commandManager = $commandManager; + $this->paymentEventLogBuilder = $paymentEventLogBuilder; + } + + /** + * @param string $orderId + * + * @throws VippsException + */ + public function get($orderId): PaymentEventLog + { + if (!isset($this->cache[$orderId])) { + $response = $this->commandManager->getPaymentEventLog($orderId); + $eventLog = $this->paymentEventLogBuilder->setData($response)->build(); + + $this->cache[$orderId] = $eventLog; + } + + return $this->cache[$orderId]; + } +} diff --git a/GatewayEpayment/Model/PaymentProvider.php b/GatewayEpayment/Model/PaymentProvider.php new file mode 100644 index 00000000..405cda5a --- /dev/null +++ b/GatewayEpayment/Model/PaymentProvider.php @@ -0,0 +1,78 @@ +commandManager = $commandManager; + $this->paymentBuilder = $paymentBuilder; + } + + /** + * @param string $orderId + * + * @return Payment + * @throws VippsException + */ + public function get(string $orderId): Payment + { + if (!isset($this->cache[$orderId])) { + $response = $this->commandManager->getPayment($orderId); + /** @var Payment $payment */ + $payment = $this->paymentBuilder->setData($response)->build(); + + $this->cache[$orderId] = $payment; + } + + return $this->cache[$orderId]; + } +} diff --git a/GatewayEpayment/Model/TransactionProcessor.php b/GatewayEpayment/Model/TransactionProcessor.php new file mode 100644 index 00000000..f727bcaf --- /dev/null +++ b/GatewayEpayment/Model/TransactionProcessor.php @@ -0,0 +1,461 @@ +orderRepository = $orderRepository; + $this->cartRepository = $cartRepository; + $this->cartManagement = $cartManagement; + $this->quoteLocator = $quoteLocator; + $this->orderLocator = $orderLocator; + $this->processor = $processor; + $this->quoteUpdater = $quoteUpdater; + $this->lockManager = $lockManager; + $this->config = $config; + $this->quoteManagement = $quoteManagement; + $this->orderManagement = $orderManagement; + $this->receiptSender = $receiptSender; + $this->resourceConnection = $resourceConnection; + $this->paymentProvider = $paymentProvider; + } + + /** + * @return DataPayment|void + * @throws InputException + */ + public function process(QuoteInterface $vippsQuote) + { + try { + $lockName = $this->acquireLock($vippsQuote->getReservedOrderId()); + + $payment = $this->paymentProvider->get($vippsQuote->getReservedOrderId()); + + if ($payment->isAborter()) { + $this->processCancelledTransaction($vippsQuote); + } elseif ($payment->isAuthorised()) { + $this->processReservedTransaction($vippsQuote, $payment); + } elseif ($payment->isExpired()) { + $this->processExpiredTransaction($vippsQuote); + } + + return $payment; + } catch (\Throwable $exception) { + echo $exception->getMessage(); + } finally { + if (isset($lockName)) { + $this->releaseLock($lockName); + } + } + } + + /** + * @throws CouldNotSaveException + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function processCancelledTransaction(QuoteInterface $vippsQuote) + { + $order = $this->orderLocator->get($vippsQuote->getReservedOrderId()); + if ($order) { + $this->cancelOrder($order); + } + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_CANCELED); + $this->quoteManagement->save($vippsQuote); + } + + /** + * @return OrderInterface|null + * @throws CouldNotSaveException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws VippsException + * @throws WrongAmountException + */ + private function processReservedTransaction(QuoteInterface $vippsQuote, DataPayment $payment) + { + $order = $this->orderLocator->get($vippsQuote->getReservedOrderId()); + if (!$order) { + $order = $this->placeOrder($vippsQuote, $payment); + } + + $this->sendReceipt($order, $payment); + + $paymentAction = $this->config->getPaymentAction(); + $this->processAction($paymentAction, $order, $payment); + + $this->notify($order); + + $vippsQuote->setStatus(QuoteInterface::STATUS_RESERVED); + $this->quoteManagement->save($vippsQuote); + + return $order; + } + + /** + * @throws CouldNotSaveException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function processExpiredTransaction(QuoteInterface $vippsQuote): void + { + $order = $this->orderLocator->get($vippsQuote->getReservedOrderId()); + if ($order) { + $this->cancelOrder($order); + } + + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_EXPIRED); + $this->quoteManagement->save($vippsQuote); + } + + /** + * @throws LocalizedException + */ + private function processAction(?string $action, OrderInterface $order, DataPayment $transaction): void + { + // Only capture is supported by epayment gateway + $this->capture($order); + } + + /** + * @throws VippsException + */ + private function sendReceipt(OrderInterface $order, DataPayment $payment) + { + if (!in_array($order->getState(), [Order::STATE_NEW, Order::STATE_PAYMENT_REVIEW])) { + return; + } + + $this->receiptSender->send($order); + } + + /** + * @param $reservedOrderId + * + * @return string + * @throws AlreadyExistsException + * @throws InputException + * @throws AcquireLockException + * @throws \Exception + */ + private function acquireLock($reservedOrderId) + { + $lockName = 'vipps_place_order_' . $reservedOrderId; + $retries = 0; + $canLock = $this->lockManager->lock($lockName, 10); + + while (!$canLock && ($retries < 10)) { + usleep(200000); + //wait for 0.2 seconds + $retries++; + $canLock = $this->lockManager->lock($lockName, 10); + } + + if (!$canLock) { + throw new AcquireLockException( + (string)__('Can not acquire lock for order "%1"', $reservedOrderId) + ); + } + + return $lockName; + } + + /** + * @param CartInterface|Quote $quote + * @param Transaction $transaction + * + * @return OrderInterface|null + * @throws CouldNotSaveException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws VippsException + * @throws WrongAmountException + * @throws \Exception + */ + private function placeOrder(QuoteInterface $vippsQuote, DataPayment $transaction) + { + $quote = $this->cartRepository->get($vippsQuote->getQuoteId()); + if (!$quote) { + throw new \Exception( //@codingStandardsIgnoreLine + (string)__('Could not place order. Could not find quote.') + ); + } + + if ($vippsQuote->getReservedOrderId() + && $quote->getReservedOrderId() !== $vippsQuote->getReservedOrderId() + ) { + $quote->setReservedOrderId($vippsQuote->getReservedOrderId()); + $this->cartRepository->save($quote); + } + + if (!$quote->getReservedOrderId() || $quote->getReservedOrderId() !== $transaction->getOrderId()) { + throw new \Exception( //@codingStandardsIgnoreLine + (string)__('Quote reserved order id does not match Vipps transaction order id.') + ); + } + + if ($transaction->isExpressCheckout()) { + $this->quoteUpdater->execute($quote, $transaction); + } + + $quote = $this->cartRepository->get($quote->getId()); + + // fix quote to be able to work from different areas (frontend/adminhtml/etc...) + $this->fixQuote($quote); + + $quote->getShippingAddress()->setCollectShippingRates(true); + $quote->collectTotals(); + + $this->validateAmount($quote, $transaction); + + // set quote active, collect totals and place order + $quote->setIsActive(true); + $orderId = $this->cartManagement->placeOrder($quote->getId()); + + $order = $this->orderRepository->get($orderId); + + $quote->setReservedOrderId(null); + $quote->setIsActive(false); + $this->cartRepository->save($quote); + + return $order; + } + + /** + * @param CartInterface|Quote $quote + */ + private function fixQuote($quote) + { + $websiteId = $quote->getStore()->getWebsiteId(); + foreach ($quote->getAllItems() as $item) { + /** @var Quote\Item $item */ + $item->getProduct()->setWebsiteId($websiteId); + } + } + + /** + * Check if reserved Order amount in vipps is the same as in Magento. + * + * @param CartInterface $quote + * @param Transaction $transaction + * + * @return void + * @throws WrongAmountException + */ + private function validateAmount(CartInterface $quote, Transaction $transaction) + { + $quoteAmount = (int)round($this->formatPrice($quote->getGrandTotal()) * 100); + $vippsAmount = (int)$transaction->getTransactionSummary()->getRemainingAmountToCapture(); + + if ($quoteAmount != $vippsAmount) { + throw new WrongAmountException( + __("Quote Grand Total {$quoteAmount} does not match Transaction Amount {$vippsAmount}") + ); + } + } + + /** + * Capture + * + * @param OrderInterface $order + * + * @throws LocalizedException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function capture(OrderInterface $order) + { + if (!in_array($order->getState(), [Order::STATE_NEW, Order::STATE_PAYMENT_REVIEW])) { + return; + } + + // preconditions + $totalDue = $order->getTotalDue(); + $baseTotalDue = $order->getBaseTotalDue(); + + /** @var Payment $payment */ + $payment = $order->getPayment(); + $payment->setAmountAuthorized($totalDue); + $payment->setBaseAmountAuthorized($baseTotalDue); + + // do capture + $this->processor->capture($payment, null); + $this->orderRepository->save($order); + } + + /** + * Authorize action + * + * @param OrderInterface $order + * @param DataPayment $dataPayment + */ + private function authorize(OrderInterface $order, DataPayment $dataPayment) + { + if (!in_array($order->getState(), [Order::STATE_NEW, Order::STATE_PAYMENT_REVIEW])) { + return; + } + + // preconditions + $totalDue = $order->getTotalDue(); + $baseTotalDue = $order->getBaseTotalDue(); + + $payment = $order->getPayment(); + if ($payment instanceof Payment) { + $transactionId = $dataPayment->getPspReference(); + $payment->setIsTransactionClosed(false); + $payment->setTransactionId($transactionId); + $payment->setTransactionAdditionalInfo( + PaymentTransaction::RAW_DETAILS, + $dataPayment->getRawData(), + ); + } + + // do authorize + $this->processor->authorize($payment, false, $baseTotalDue); + // base amount will be set inside + $payment->setAmountAuthorized($totalDue); + $this->orderRepository->save($order); + } + + /** + * @param $lockName + * + * @return bool + * @throws InputException + */ + private function releaseLock($lockName) + { + return $this->lockManager->unlock($lockName); + } + + /** + * @param OrderInterface $order + */ + private function notify(OrderInterface $order) + { + if (!$order->getEmailSent()) { + $this->orderManagement->notify($order->getEntityId()); + } + } + + /** + * @param $order + * + * @throws \Exception + */ + private function cancelOrder($order): void + { + if ($order->getState() === Order::STATE_NEW) { + $this->orderManagement->cancel($order->getEntityId()); + } elseif ($order->getState() === Order::STATE_PAYMENT_REVIEW) { + $connection = $this->resourceConnection->getConnection(); + try { + $connection->beginTransaction(); + + $order->setState(Order::STATE_NEW); + $this->orderRepository->save($order); + $this->orderManagement->cancel($order->getEntityId()); + + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw $e; + } + } + } +} diff --git a/GatewayEpayment/Request/AdjustAuthorization/AuthorisationTypeBuilder.php b/GatewayEpayment/Request/AdjustAuthorization/AuthorisationTypeBuilder.php new file mode 100644 index 00000000..7dbbeeb9 --- /dev/null +++ b/GatewayEpayment/Request/AdjustAuthorization/AuthorisationTypeBuilder.php @@ -0,0 +1,36 @@ + 'FINAL_AUTH']; + } +} diff --git a/GatewayEpayment/Request/DefaultDataBuilder.php b/GatewayEpayment/Request/DefaultDataBuilder.php new file mode 100644 index 00000000..a0595070 --- /dev/null +++ b/GatewayEpayment/Request/DefaultDataBuilder.php @@ -0,0 +1,26 @@ + $buildSubject['reference'] + ]; + } +} diff --git a/GatewayEpayment/Request/GetSession/SessionIdDataBuilder.php b/GatewayEpayment/Request/GetSession/SessionIdDataBuilder.php new file mode 100644 index 00000000..6adeb7ae --- /dev/null +++ b/GatewayEpayment/Request/GetSession/SessionIdDataBuilder.php @@ -0,0 +1,40 @@ + $buildSubject['session_id'] + ]; + } +} diff --git a/GatewayEpayment/Request/InitSession/AddressFieldsDataBuilder.php b/GatewayEpayment/Request/InitSession/AddressFieldsDataBuilder.php new file mode 100644 index 00000000..c22fb998 --- /dev/null +++ b/GatewayEpayment/Request/InitSession/AddressFieldsDataBuilder.php @@ -0,0 +1,40 @@ + true + ]; + } +} diff --git a/GatewayEpayment/Request/InitSession/ConfigurationDataBuilder.php b/GatewayEpayment/Request/InitSession/ConfigurationDataBuilder.php new file mode 100644 index 00000000..640ab3f7 --- /dev/null +++ b/GatewayEpayment/Request/InitSession/ConfigurationDataBuilder.php @@ -0,0 +1,63 @@ +allowedCountries = $allowedCountries; + } + + /** + * Get related data for transaction section. + * + * @param array $buildSubject + * + * @return array + * @throws \Exception + */ + public function build(array $buildSubject) + { + $data = []; + + + $data['customerInteraction'] = 'CUSTOMER_NOT_PRESENT'; + $data['elements'] = 'Full'; + //$data['userFlow'] = 'WEB_REDIRECT'; //WEB_REDIRECT|NATIVE_REDIRECT + $data['countries']['supported'] = $this->allowedCountries->getAllowedCountries(); + + return $data; + } +} diff --git a/GatewayEpayment/Request/InitSession/ContactFieldsDataBuilder.php b/GatewayEpayment/Request/InitSession/ContactFieldsDataBuilder.php new file mode 100644 index 00000000..202c6413 --- /dev/null +++ b/GatewayEpayment/Request/InitSession/ContactFieldsDataBuilder.php @@ -0,0 +1,40 @@ + true + ]; + } +} diff --git a/GatewayEpayment/Request/InitSession/CustomerDataBuilder.php b/GatewayEpayment/Request/InitSession/CustomerDataBuilder.php new file mode 100644 index 00000000..691cff6a --- /dev/null +++ b/GatewayEpayment/Request/InitSession/CustomerDataBuilder.php @@ -0,0 +1,69 @@ +customerSession = $customerSession; + } + + /** + * Get related data for transaction section. + * + * @param array $buildSubject + * + * @return array + * @throws \Exception + */ + public function build(array $buildSubject) + { + $data = []; + + /** @var Customer $customer */ + $customer = $this->customerSession->getCustomer(); + if ($customer && $customer->getDefaultBillingAddress()) { + $data['prefillCustomer'] = [ + 'firstName' => $customer->getFirstname(), + 'lastName' => $customer->getLastname(), + 'email' => $customer->getEmail(), + 'phoneNumber' => $customer->getDefaultBillingAddress()->getTelephone(), + 'streetAddress' => $customer->getDefaultBillingAddress()->getStreetFull(), + 'city' => $customer->getDefaultBillingAddress()->getCity(), + 'postalCode' => $customer->getDefaultBillingAddress()->getPostcode(), + 'country' => $customer->getDefaultBillingAddress()->getCountry() + ]; + } + + return $data; + } +} diff --git a/GatewayEpayment/Request/InitSession/InitPreprocessor.php b/GatewayEpayment/Request/InitSession/InitPreprocessor.php new file mode 100755 index 00000000..1d6d9076 --- /dev/null +++ b/GatewayEpayment/Request/InitSession/InitPreprocessor.php @@ -0,0 +1,58 @@ +subjectReader = $subjectReader; + } + + /** + * Get merchant related data for Initiate payment request. + * + * @throws \Exception + */ + public function build(array $buildSubject): array + { + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof QuotePayment) { + $payment->setMethod(Vipps::METHOD_CODE); + + $quote = $payment->getQuote(); + $quote->setReservedOrderId(null); + $quote->reserveOrderId(); + + $quote->getPayment() + ->setAdditionalInformation(Vipps::METHOD_TYPE_KEY, Vipps::METHOD_TYPE_EPAYMENT_CHECKOUT); + } + + return []; + } +} diff --git a/GatewayEpayment/Request/InitSession/MerchantDataBuilder.php b/GatewayEpayment/Request/InitSession/MerchantDataBuilder.php new file mode 100644 index 00000000..2d656711 --- /dev/null +++ b/GatewayEpayment/Request/InitSession/MerchantDataBuilder.php @@ -0,0 +1,64 @@ +urlBuilder = $urlBuilder; + $this->subjectReader = $subjectReader; + } + + /** + * Get merchant related data for session request. + * + * @throws \Exception + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $callbackAuthorizationToken = $this->generateAuthToken(); + + $reference = $paymentDO->getOrder()->getOrderIncrementId(); + + return [ + 'merchantInfo' => [ + 'callbackUrl' => rtrim($this->urlBuilder->getUrl('checkout/vipps/callback'), '/'), + 'returnUrl' => $this->urlBuilder->getUrl('checkout/vipps/fallback', ['reference' => $reference]), + 'callbackAuthorizationToken' => $callbackAuthorizationToken, + 'termsAndConditionsUrl' => $this->urlBuilder->getUrl(), + ] + ]; + } + + /** + * Method to generate access token. + */ + private function generateAuthToken(): string + { + return uniqid('', true); + } +} diff --git a/GatewayEpayment/Request/InitSession/TransactionDataBuilder.php b/GatewayEpayment/Request/InitSession/TransactionDataBuilder.php new file mode 100644 index 00000000..75f71149 --- /dev/null +++ b/GatewayEpayment/Request/InitSession/TransactionDataBuilder.php @@ -0,0 +1,60 @@ +subjectReader = $subjectReader; + } + + /** + * Get related data for transaction section. + * + * @param array $buildSubject + * + * @return array[] + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + + /** @var Payment $payment */ + $payment = $paymentDO->getPayment(); + /** @var Quote $quote */ + $quote = $payment->getQuote(); + + return [ + 'transaction' => [ + 'amount' => [ + 'currency' => $quote->getStoreCurrencyCode(), + 'value' => $quote->getGrandTotal() * 100 + ], + 'reference' => $quote->getReservedOrderId(), + 'paymentDescription' => 'Order Id' + ] + ]; + } +} diff --git a/GatewayEpayment/Request/ModificationDataBuilder.php b/GatewayEpayment/Request/ModificationDataBuilder.php new file mode 100644 index 00000000..84e64948 --- /dev/null +++ b/GatewayEpayment/Request/ModificationDataBuilder.php @@ -0,0 +1,72 @@ +subjectReader = $subjectReader; + } + + /** + * Default data builder + * + * @param array $buildSubject + * + * @return array + * @throws \Exception + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $amount = $this->subjectReader->readAmount($buildSubject); + + /** @var InfoInterface $payment */ + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + + $result = []; + if ($amount > 0) { + $result['modificationAmount'] = [ + 'currency' => $order->getCurrencyCode(), + 'value' => $amount * 100 + ]; + } + + $result['modificationReference'] = $order->getOrderIncrementId(); + + return $result; + } +} diff --git a/GatewayEpayment/Request/Payment/AmountBuilder.php b/GatewayEpayment/Request/Payment/AmountBuilder.php new file mode 100644 index 00000000..e48209d5 --- /dev/null +++ b/GatewayEpayment/Request/Payment/AmountBuilder.php @@ -0,0 +1,72 @@ +subjectReader = $subjectReader; + $this->urlBuilder = $urlBuilder; + $this->storeManager = $storeManager; + } + + /** + * Get related data for transaction section. + * + * @throws NoSuchEntityException + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + + /** @var \Magento\Quote\Model\Quote\Payment $payment */ + $payment = $paymentDO->getPayment(); + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $payment->getQuote(); + + if (!$quote->getReservedOrderId()) { + $quote->reserveOrderId(); + } + $reference = $quote->getReservedOrderId(); + + return [ + 'amount' => [ + 'currency' => $quote->getStoreCurrencyCode(), + 'value' => $quote->getGrandTotal() * 100 + ], + 'reference' => $reference, + 'paymentDescription' => $this->storeManager->getStore()->getName(), + 'paymentMethod' => ["type" => "WALLET"], + "userFlow" => "WEB_REDIRECT", + 'returnUrl' => $this->urlBuilder->getUrl('vipps/payment/fallback', ['reference' => $reference]) + ]; + } +} diff --git a/GatewayEpayment/Request/ReferenceDataBuilder.php b/GatewayEpayment/Request/ReferenceDataBuilder.php new file mode 100644 index 00000000..ef7d246c --- /dev/null +++ b/GatewayEpayment/Request/ReferenceDataBuilder.php @@ -0,0 +1,48 @@ +subjectReader = $subjectReader; + } + + /** + * This builders for passing parameters into TransferFactory object. + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + if ($paymentDO) { + $orderAdapter = $paymentDO->getOrder(); + if ($orderAdapter) { + $buildSubject = array_merge( + $buildSubject, + ['reference' => $orderAdapter->getOrderIncrementId()] + ); + } + } + + return $buildSubject; + } +} diff --git a/GatewayEpayment/Request/SendReceipt/BottomLineBuilder.php b/GatewayEpayment/Request/SendReceipt/BottomLineBuilder.php new file mode 100644 index 00000000..516b7313 --- /dev/null +++ b/GatewayEpayment/Request/SendReceipt/BottomLineBuilder.php @@ -0,0 +1,44 @@ + [ + 'currency' => $order->getStoreCurrencyCode(), + 'tipAmount' => 0, + 'giftCardAmount' => 0, + 'terminalId' => null + ] + ]; + } +} diff --git a/GatewayEpayment/Request/SendReceipt/GenericDataBuilder.php b/GatewayEpayment/Request/SendReceipt/GenericDataBuilder.php new file mode 100644 index 00000000..2860a8e3 --- /dev/null +++ b/GatewayEpayment/Request/SendReceipt/GenericDataBuilder.php @@ -0,0 +1,37 @@ + $order->getIncrementId()]; + } + + return []; + } +} diff --git a/GatewayEpayment/Request/SendReceipt/OrderLinesBuilder.php b/GatewayEpayment/Request/SendReceipt/OrderLinesBuilder.php new file mode 100644 index 00000000..3bd88391 --- /dev/null +++ b/GatewayEpayment/Request/SendReceipt/OrderLinesBuilder.php @@ -0,0 +1,110 @@ +getItemsCollection() as $item) { + /** @var Order\Item $item */ + if (($item->getChildrenItems() && $item->getProductType() !== Configurable::TYPE_CODE) + || ($item->getParentItem() && $item->getParentItem()->getProductType() === Configurable::TYPE_CODE) + ) { + // it means we take into account only simple products that is not a part of configurable + // for configurable product we take into account main configurable product bu not its simples + continue; + } + + $totalAmount = $item->getRowTotal() + $item->getTaxAmount() - $item->getDiscountAmount(); + $totalAmountExcludingTax = $totalAmount - $item->getTaxAmount(); + + $monitaryTotalAmount = (int)($totalAmount * 100); + $monitaryTotalAmountExcludingTax = (int)($totalAmountExcludingTax * 100); + $monitaryTaxAmount = $monitaryTotalAmount - $monitaryTotalAmountExcludingTax; + + $orderLines[] = [ + 'name' => $item->getName(), + 'id' => $item->getSku(), + 'totalAmount' => $monitaryTotalAmount, + 'totalAmountExcludingTax' => $monitaryTotalAmountExcludingTax, + 'totalTaxAmount' => $monitaryTaxAmount, + 'taxPercentage' => $monitaryTotalAmountExcludingTax > 0 + ? (int)round($monitaryTaxAmount * 100 / $monitaryTotalAmountExcludingTax) + : $item->getTaxPercent(), + 'unitInfo' => [ + 'unitPrice' => (int)($item->getPrice() * 100), + 'quantity' => (string)$item->getQtyOrdered() + ], + 'discount' => (int)($item->getDiscountAmount() * 100), + 'productUrl' => $this->getProductUrl($item), + 'isReturn' => false, + 'isShipping' => false + ]; + } + + $monitaryShippingTotalAmount = (int)($order->getShippingInclTax() * 100); + $monitaryShippingTotalAmountExcludingTax = (int)($order->getShippingAmount() * 100); + $monitaryShippingTaxAmount = $monitaryShippingTotalAmount - $monitaryShippingTotalAmountExcludingTax; + + $orderLines[] = [ + 'name' => $order->getShippingDescription(), + 'id' => 'shipping', + 'totalAmount' => (int)($order->getShippingInclTax() * 100), + 'totalAmountExcludingTax' => (int)($order->getShippingAmount() * 100), + 'totalTaxAmount' => (int)($order->getShippingTaxAmount() * 100), + 'taxPercentage' => $monitaryShippingTotalAmountExcludingTax > 0 + ? (int)round($monitaryShippingTaxAmount * 100 / $monitaryShippingTotalAmountExcludingTax) + : 0, + 'discount' => (int)($order->getShippingDiscountAmount() * 100), + 'isReturn' => false, + 'isShipping' => true + ]; + + return ['orderLines'=> $orderLines]; + } + + public function getProductUrl($item) + { + if ($item->getRedirectUrl()) { + return $item->getRedirectUrl(); + } + + /** @var Order\Item $item */ + $product = $item->getProduct(); + $option = $item->getOptionByCode('product_type'); + if ($option) { + $product = $option->getProduct(); + } + + return $product->getUrlModel()->getUrl($product); + } +} diff --git a/GatewayEpayment/Request/SubjectReader.php b/GatewayEpayment/Request/SubjectReader.php new file mode 100644 index 00000000..828eb888 --- /dev/null +++ b/GatewayEpayment/Request/SubjectReader.php @@ -0,0 +1,39 @@ +subjectReader = $subjectReader; + $this->quoteFactory = $quoteFactory; + $this->quoteRepository = $quoteRepository; + $this->cartRepository = $cartRepository; + } + + /** + * Save quote payment method. + * + * @param array $handlingSubject + * @param array $responseBody + * + * @throws \Exception + */ + public function handle(array $handlingSubject, array $responseBody) //@codingStandardsIgnoreLine + { + /** @var Transfer $transfer */ + $transfer = $handlingSubject['transferObject']; + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + + $payment = $paymentDO->getPayment(); + /** @var Quote $quote */ + $quote = $payment->getQuote(); + $this->cartRepository->save($quote); + + try { + $vippsQuote = $this->quoteRepository->loadNewByQuote($quote->getId()); + } catch (NoSuchEntityException $e) { + /** @var QuoteInterface $vippsQuote */ + $vippsQuote = $this->quoteFactory->create(); + $vippsQuote->setStoreId($quote->getStoreId()); + $vippsQuote->setQuoteId($quote->getId()); + $vippsQuote->setStatus(QuoteStatusInterface::STATUS_NEW); + } + + $vippsQuote->setReservedOrderId($quote->getReservedOrderId()); + $vippsQuote->setAuthToken($transfer->getBody()['merchantInfo']['callbackAuthorizationToken'] ?? null); + $vippsQuote->setCheckoutToken($responseBody['token']); + $vippsQuote->setCheckoutSessionId($this->extractSessionId($responseBody)); + + $this->quoteRepository->save($vippsQuote); + } + + /** + * @param array $responseBody + * + * @return string|null + */ + private function extractSessionId($responseBody): ?string + { + return (preg_match('/session\/(.+)$/iu', $responseBody['pollingUrl'], $matches)) ? $matches[1] : null; + } +} diff --git a/GatewayEpayment/Response/Payment/PostHandler.php b/GatewayEpayment/Response/Payment/PostHandler.php new file mode 100644 index 00000000..fdb9dd24 --- /dev/null +++ b/GatewayEpayment/Response/Payment/PostHandler.php @@ -0,0 +1,123 @@ +subjectReader = $subjectReader; + $this->quoteFactory = $quoteFactory; + $this->quoteRepository = $quoteRepository; + $this->orderRepository = $orderRepository; + $this->cartRepository = $cartRepository; + } + + /** + * Save quote payment method. + * + * @param array $handlingSubject + * @param array $responseBody + * + * @throws \Exception + */ + public function handle(array $handlingSubject, array $responseBody) //@codingStandardsIgnoreLine + { + /** @var Transfer $transfer */ + $transfer = $handlingSubject['transferObject']; + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + + $payment = $paymentDO->getPayment(); + $orderAdapter = $paymentDO->getOrder(); + + if ($payment instanceof OrderPayment) { + $order = $this->orderRepository->get($orderAdapter->getId()); + $quoteId = $order->getQuoteId(); + } elseif ($payment instanceof QuotePayment) { + $cart = $payment->getQuote(); + $this->cartRepository->save($cart); + $quoteId = $cart->getId(); + } + + /** @var QuoteInterface $vippsQuote */ + $vippsQuote = $this->quoteFactory->create(); + $vippsQuote->setStoreId($orderAdapter->getStoreId()); + $vippsQuote->setQuoteId((int)$quoteId); + $vippsQuote->setStatus(QuoteInterface::STATUS_NEW); + $vippsQuote->setReservedOrderId($orderAdapter->getOrderIncrementId()); + $vippsQuote->setAuthToken($transfer->getBody()['merchantInfo']['callbackAuthorizationToken'] ?? ''); + $this->quoteRepository->save($vippsQuote); + } +} diff --git a/GatewayEpayment/Response/Payment/TransactionHandler.php b/GatewayEpayment/Response/Payment/TransactionHandler.php new file mode 100644 index 00000000..34342b4e --- /dev/null +++ b/GatewayEpayment/Response/Payment/TransactionHandler.php @@ -0,0 +1,92 @@ +subjectReader = $subjectReader; + $this->transactionBuilder = $transactionBuilder; + } + + /** + * {@inheritdoc} + * + * @param array $handlingSubject + * @param array $response + */ + public function handle($handlingSubject, $response) //@codingStandardsIgnoreLine + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + $transaction = $this->transactionBuilder + ->setData($response) + ->build(); + + if ($payment instanceof \Vipps\Payment\GatewayEpayment\Data\Payment) { + $status = $transaction->getTransactionInfo()->getStatus(); + $transactionId = $transaction->getPspReference(); + + switch ($status) { + case Transaction::TRANSACTION_STATUS_CANCELLED: + $transactionId .= '-void'; + break; + case Transaction::TRANSACTION_STATUS_RESERVE: + case Transaction::TRANSACTION_STATUS_RESERVED: + $payment->setIsTransactionClosed(false); + break; + } + + $payment->setTransactionId($transactionId); + $payment->setTransactionAdditionalInfo( + PaymentTransaction::RAW_DETAILS, + $transaction->getTransactionSummary()->getData() + ); + } + } +} diff --git a/GatewayEpayment/Validator/AvailabilityValidator.php b/GatewayEpayment/Validator/AvailabilityValidator.php new file mode 100644 index 00000000..ddf95ca5 --- /dev/null +++ b/GatewayEpayment/Validator/AvailabilityValidator.php @@ -0,0 +1,72 @@ +storeManager = $storeManager; + } + + /** + * @param array $validationSubject + * + * @return ResultInterface + * @throws NoSuchEntityException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function validate(array $validationSubject) + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + $isValid = \in_array( + $store->getBaseCurrencyCode(), + [self::NORWEGIAN_CURRENCY, self::FINNISH_CURRENCY, self::DANISH_CURRENCY], + true + ); + $errorMessages = $isValid ? [] : [__('Not allowed currency. Please, contact store administrator.')]; + + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/GatewayEpayment/Validator/CancelTransactionValidator.php b/GatewayEpayment/Validator/CancelTransactionValidator.php new file mode 100644 index 00000000..508d0c25 --- /dev/null +++ b/GatewayEpayment/Validator/CancelTransactionValidator.php @@ -0,0 +1,69 @@ +transactionBuilder = $transactionBuilder; + } + + /** + * @inheritdoc + * + * @param array $validationSubject + * + * @return ResultInterface + */ + public function validate(array $validationSubject) + { + $response = $validationSubject['jsonData'] ?? []; + $transaction = $this->transactionBuilder->setData($response)->build(); + + $info = $transaction->getTransactionInfo(); + + // if required fields configured - validate them + $isValid = $info->getStatus() == Transaction::TRANSACTION_STATUS_CANCELLED; + + $errorMessages = $isValid ? [] : [__('Gateway response error. Incorrect transaction data.')]; + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/GatewayEpayment/Validator/CaptureTransactionValidator.php b/GatewayEpayment/Validator/CaptureTransactionValidator.php new file mode 100644 index 00000000..fe135a6f --- /dev/null +++ b/GatewayEpayment/Validator/CaptureTransactionValidator.php @@ -0,0 +1,69 @@ +transactionBuilder = $transactionBuilder; + } + + /** + * @inheritdoc + * + * @param array $validationSubject + * + * @return ResultInterface + */ + public function validate(array $validationSubject) + { + $response = $validationSubject['jsonData'] ?? []; + $transaction = $this->transactionBuilder->setData($response)->build(); + + $info = $transaction->getTransactionInfo(); + + // if required fields configured - validate them + $isValid = $info->getStatus() == Transaction::TRANSACTION_STATUS_CAPTURED; + + $errorMessages = $isValid ? [] : [__('Gateway response error. Incorrect transaction data.')]; + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/GatewayEpayment/Validator/InitiateValidator.php b/GatewayEpayment/Validator/InitiateValidator.php new file mode 100644 index 00000000..952bf652 --- /dev/null +++ b/GatewayEpayment/Validator/InitiateValidator.php @@ -0,0 +1,41 @@ +createResult($isValid, $errorMessages); + } +} diff --git a/GatewayEpayment/Validator/OrderValidator.php b/GatewayEpayment/Validator/OrderValidator.php new file mode 100644 index 00000000..b42c72c9 --- /dev/null +++ b/GatewayEpayment/Validator/OrderValidator.php @@ -0,0 +1,71 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + * + * @param array $validationSubject + * + * @return ResultInterface + */ + public function validate(array $validationSubject) + { + $orderId = $validationSubject['jsonData']['orderId'] ?? null; + + $isValid = (bool)$orderId; + + $payment = $this->subjectReader->readPayment($validationSubject); + if ($payment) { + $orderAdapter = $payment->getOrder(); + $isValid = $orderId == $orderAdapter->getOrderIncrementId(); + } + + $errorMessages = $isValid ? [] : [__('Gateway response error. Order Id is incorrect')]; + + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/GatewayEpayment/Validator/ReferenceValidator.php b/GatewayEpayment/Validator/ReferenceValidator.php new file mode 100644 index 00000000..863d53b3 --- /dev/null +++ b/GatewayEpayment/Validator/ReferenceValidator.php @@ -0,0 +1,81 @@ +subjectReader = $subjectReader; + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + } + + /** + * @inheritdoc + * + * @param array $validationSubject + * + * @return ResultInterface + */ + public function validate(array $validationSubject) + { + $isValid = false; + $reference = $this->subjectReader->readReference($validationSubject); + $paymentDataObject = $this->subjectReader->readPayment($validationSubject); + if ($reference && $paymentDataObject && $paymentDataObject->getPayment() && $paymentDataObject->getPayment()->getQuote()) { + $isValid = $paymentDataObject->getPayment()->getQuote()->getReservedOrderId() === $reference; + } + + $errorMessages = $isValid ? [] : [__('Gateway response error. Reference is incorrect')]; + + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/GatewayEpayment/Validator/RefundTransactionValidator.php b/GatewayEpayment/Validator/RefundTransactionValidator.php new file mode 100644 index 00000000..705e3798 --- /dev/null +++ b/GatewayEpayment/Validator/RefundTransactionValidator.php @@ -0,0 +1,69 @@ +transactionBuilder = $transactionBuilder; + } + + /** + * @inheritdoc + * + * @param array $validationSubject + * + * @return ResultInterface + */ + public function validate(array $validationSubject) + { + $response = $validationSubject['jsonData'] ?? []; + $transaction = $this->transactionBuilder->setData($response)->build(); + + $info = $transaction->getTransactionInfo(); + + // if required fields configured - validate them + $isValid = $info->getStatus() == Transaction::TRANSACTION_STATUS_REFUND; + + $errorMessages = $isValid ? [] : [__('Gateway response error. Incorrect transaction data.')]; + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/GraphQl/Resolver/GetPaymentLabel.php b/GraphQl/Resolver/GetPaymentLabel.php new file mode 100644 index 00000000..10275e19 --- /dev/null +++ b/GraphQl/Resolver/GetPaymentLabel.php @@ -0,0 +1,46 @@ +vipps = $vipps; + } + + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): string + { + return (string)$this->vipps->getTitle(); + } +} diff --git a/Model/Adminhtml/Source/PaymentAction.php b/Model/Adminhtml/Source/PaymentAction.php index 2dcfcc5a..f0f036b8 100644 --- a/Model/Adminhtml/Source/PaymentAction.php +++ b/Model/Adminhtml/Source/PaymentAction.php @@ -25,12 +25,12 @@ class PaymentAction implements ArrayInterface /** * @var string */ - const ACTION_AUTHORIZE = 'authorize'; + public const ACTION_AUTHORIZE = 'authorize'; /** * @var string */ - const ACTION_AUTHORIZE_CAPTURE = 'authorize_capture'; + public const ACTION_AUTHORIZE_CAPTURE = 'authorize_capture'; /** * Possible actions on order place diff --git a/Model/Checkout/ConfigProvider.php b/Model/Checkout/ConfigProvider.php index ad15dedc..45afdd95 100644 --- a/Model/Checkout/ConfigProvider.php +++ b/Model/Checkout/ConfigProvider.php @@ -18,6 +18,7 @@ use Magento\Checkout\Model\ConfigProviderInterface; use Magento\Framework\View\Asset\Repository as AssetRepository; use Magento\Framework\UrlInterface; +use Vipps\Payment\Model\Config\ConfigVersionPool; /** * Class ConfigProvider @@ -35,6 +36,9 @@ class ConfigProvider implements ConfigProviderInterface */ private $assertRepository; + private ConfigVersionPool $logoVersion; + private ConfigVersionPool $logoWidth; + /** * ConfigProvider constructor. * @@ -43,10 +47,14 @@ class ConfigProvider implements ConfigProviderInterface */ public function __construct( UrlInterface $urlBuilder, - AssetRepository $assertRepository + AssetRepository $assertRepository, + ConfigVersionPool $logoVersion, + ConfigVersionPool $logoWidth ) { $this->urlBuilder = $urlBuilder; $this->assertRepository = $assertRepository; + $this->logoVersion = $logoVersion; + $this->logoWidth = $logoWidth; } public function getConfig() @@ -55,7 +63,8 @@ public function getConfig() 'payment' => [ 'vipps' => [ 'initiateUrl' => $this->urlBuilder->getUrl('vipps/payment/initRegular', ['_secure' => true]), - 'logoSrc' => $this->assertRepository->getUrl('Vipps_Payment::images/vipps_logo_rgb.png'), + 'logoSrc' => $this->assertRepository->getUrl($this->logoVersion->get()), + 'logoWidth' => $this->logoWidth->get(), 'continueImgSrc' => $this->assertRepository->getUrl('Vipps_Payment::images/vipps_knapp_fortsett.png'), ] diff --git a/Model/CommandPoolProxy.php b/Model/CommandPoolProxy.php new file mode 100644 index 00000000..5c283c98 --- /dev/null +++ b/Model/CommandPoolProxy.php @@ -0,0 +1,28 @@ +configVersionPool = $configVersionPool; + } + + private function getPool(): CommandPoolInterface + { + return $this->configVersionPool->get(); + } + + public function get($commandCode): CommandInterface + { + return $this->getPool()->get($commandCode); + } +} diff --git a/Model/Config/ConfigVersionPool.php b/Model/Config/ConfigVersionPool.php new file mode 100644 index 00000000..baf97401 --- /dev/null +++ b/Model/Config/ConfigVersionPool.php @@ -0,0 +1,29 @@ +pool = $pool; + $this->config = $config; + } + + public function get() + { + $versionCode = $this->config->getVersion(); + + return $this->pool[$versionCode] ?? $this->pool[Version::CONFIG_VIPPS]; + } +} diff --git a/Model/Config/Source/Version.php b/Model/Config/Source/Version.php new file mode 100644 index 00000000..b79e8969 --- /dev/null +++ b/Model/Config/Source/Version.php @@ -0,0 +1,39 @@ + self::CONFIG_VIPPS, 'label' => __(self::LABEL_VIPPS)], + ['value' => self::CONFIG_MOBILE_EPAYMENT, 'label' => __(self::LABEL_MOBILE_PAY)] + ]; + } + + /** + * Get options in "key-value" format + * + * @return array + */ + public function toArray() + { + return [self::CONFIG_VIPPS => __(self::LABEL_VIPPS), self::CONFIG_MOBILE_EPAYMENT => __(self::LABEL_MOBILE_PAY)]; + } +} diff --git a/Model/CurrencyValidator.php b/Model/CurrencyValidator.php index a8390be5..2f1f354a 100644 --- a/Model/CurrencyValidator.php +++ b/Model/CurrencyValidator.php @@ -27,7 +27,7 @@ */ class CurrencyValidator { - const NORWEGIAN_CURRENCY = 'NOK'; + private const NORWEGIAN_CURRENCY = 'NOK'; /** * @var StoreManagerInterface diff --git a/Model/Fallback/Authorise/Commerce.php b/Model/Fallback/Authorise/Commerce.php new file mode 100644 index 00000000..e887929b --- /dev/null +++ b/Model/Fallback/Authorise/Commerce.php @@ -0,0 +1,31 @@ +getParam('order_id') || + !$request->getParam('auth_token') + ) { + throw new LocalizedException(__('Invalid request parameters')); + } + + if ($vippsQuote->getAuthToken() !== $request->getParam('auth_token', '')) { + throw new LocalizedException(__('Invalid request')); + } + } + + public function getOrderId(RequestInterface $request): string + { + return $request->getParam('order_id'); + } +} diff --git a/Model/Fallback/Authorise/Epayment.php b/Model/Fallback/Authorise/Epayment.php new file mode 100644 index 00000000..b4bffc85 --- /dev/null +++ b/Model/Fallback/Authorise/Epayment.php @@ -0,0 +1,29 @@ +getParam('reference')) { + throw new LocalizedException(__('Invalid request parameters')); + } + + if ($vippsQuote->getReservedOrderId() !== $request->getParam('reference', '')) { + throw new LocalizedException(__('Invalid request')); + } + } + + public function getOrderId(RequestInterface $request): string + { + return $request->getParam('reference'); + } +} diff --git a/Model/Fallback/AuthoriseProxy.php b/Model/Fallback/AuthoriseProxy.php new file mode 100644 index 00000000..e5e92a14 --- /dev/null +++ b/Model/Fallback/AuthoriseProxy.php @@ -0,0 +1,35 @@ +configVersionPool = $configVersionPool; + } + + private function get(): AuthoriseInterface + { + return $this->configVersionPool->get(); + } + + public function do(RequestInterface $request, QuoteInterface $vippsQuote): void + { + $this->get()->do($request, $vippsQuote); + } + + public function getOrderId(RequestInterface $request): string + { + return $this->get()->getOrderId($request); + } +} diff --git a/Model/Method/Vipps.php b/Model/Method/Vipps.php index 7009e280..cd562b92 100644 --- a/Model/Method/Vipps.php +++ b/Model/Method/Vipps.php @@ -13,28 +13,27 @@ * 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\Method; use Magento\Payment\Model\Method\Adapter; -/** - * Class Vipps - * @package Vipps\Payment\Model\Method - */ class Vipps extends Adapter { - /** - * @var string - */ - const METHOD_TYPE_KEY = 'method_type'; - - /** - * @var string - */ - const METHOD_TYPE_EXPRESS_CHECKOUT = 'express_checkout'; - - /** - * @var string - */ - const METHOD_TYPE_REGULAR_CHECKOUT = 'regular_checkout'; + public const METHOD_CODE = 'vipps'; + + public const METHOD_TYPE_KEY = 'method_type'; + + public const METHOD_TYPE_EXPRESS_CHECKOUT = 'express_checkout'; + + public const METHOD_TYPE_REGULAR_CHECKOUT = 'regular_checkout'; + + public const METHOD_TYPE_EPAYMENT_CHECKOUT = 'checkout'; + + public function getTitle() + { + $version = $this->getConfigData('version'); + + return $this->getConfigData('title_' . $version); + } } diff --git a/Model/PaymentEventLogProvider.php b/Model/PaymentEventLogProvider.php new file mode 100644 index 00000000..5820c26e --- /dev/null +++ b/Model/PaymentEventLogProvider.php @@ -0,0 +1,77 @@ +commandManager = $commandManager; + $this->paymentEventLogBuilder = $paymentEventLogBuilder; + } + + /** + * @param string $orderId + * + * @return PaymentEventLog + * @throws VippsException + */ + public function get($orderId): PaymentEventLog + { + if (!isset($this->cache[$orderId])) { + $response = $this->commandManager->getPaymentEventLog($orderId); + $eventLog = $this->paymentEventLogBuilder->setData($response)->build(); + + $this->cache[$orderId] = $eventLog; + } + + return $this->cache[$orderId]; + } +} diff --git a/Model/PaymentProvider.php b/Model/PaymentProvider.php new file mode 100644 index 00000000..bcb27bf7 --- /dev/null +++ b/Model/PaymentProvider.php @@ -0,0 +1,78 @@ +commandManager = $commandManager; + $this->paymentBuilder = $paymentBuilder; + } + + /** + * @param string $orderId + * + * @return Payment + * @throws VippsException + */ + public function get($orderId): Payment + { + if (!isset($this->cache[$orderId])) { + $response = $this->commandManager->getPayment($orderId); + /** @var Payment $payment */ + $payment = $this->paymentBuilder->setData($response)->build(); + + $this->cache[$orderId] = $payment; + } + + return $this->cache[$orderId]; + } +} diff --git a/Model/Profiling/Profiler.php b/Model/Profiling/Profiler.php index 97d88815..a7ba5403 100644 --- a/Model/Profiling/Profiler.php +++ b/Model/Profiling/Profiler.php @@ -119,7 +119,7 @@ public function save(TransferInterface $transfer, Response $response) private function parseOrderId(Response $response) { $content = $this->jsonDecoder->decode($response->getContent()); - return $content['orderId'] ?? null; + return ($content['orderId'] ?? $content['reference'] ?? null); } /** diff --git a/Model/Quote/CancelFacade.php b/Model/Quote/CancelFacade.php index 1e472d5e..e9146045 100644 --- a/Model/Quote/CancelFacade.php +++ b/Model/Quote/CancelFacade.php @@ -18,16 +18,13 @@ use Magento\Framework\Exception\CouldNotSaveException; use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Api\OrderManagementInterface; -use Magento\Sales\Model\Order; use Psr\Log\LoggerInterface; use Vipps\Payment\Api\CommandManagerInterface; use Vipps\Payment\Api\Data\QuoteInterface; use Vipps\Payment\Api\Data\QuoteStatusInterface; use Vipps\Payment\Api\Quote\CancelFacadeInterface; use Vipps\Payment\Model\OrderLocator; -use Vipps\Payment\Model\Quote; use Vipps\Payment\Model\QuoteRepository; /** @@ -36,65 +33,20 @@ */ class CancelFacade implements CancelFacadeInterface { - /** - * @var CommandManagerInterface - */ - private $commandManager; + private CommandManagerInterface $commandManager; + private OrderManagementInterface $orderManagement; + private QuoteRepository $quoteRepository; + private AttemptManagement $attemptManagement; + private CartRepositoryInterface $cartRepository; + private OrderLocator $orderLocator; + private LoggerInterface $logger; - /** - * @var OrderManagementInterface - */ - private $orderManagement; - - /** - * @var QuoteRepository - */ - private $quoteRepository; - - /** - * @var AttemptManagement - */ - private $attemptManagement; - - /** - * @var CartRepositoryInterface - */ - private $cartRepository; - - /** - * @var OrderRepositoryInterface - */ - private $orderRepository; - - /** - * @var OrderLocator - */ - private $orderLocator; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * CancelFacade constructor. - * - * @param CommandManagerInterface $commandManager - * @param OrderManagementInterface $orderManagement - * @param QuoteRepository $quoteRepository - * @param AttemptManagement $attemptManagement - * @param CartRepositoryInterface $cartRepository - * @param OrderRepositoryInterface $orderRepository - * @param OrderLocator $orderLocator - * @param LoggerInterface $logger - */ public function __construct( CommandManagerInterface $commandManager, OrderManagementInterface $orderManagement, QuoteRepository $quoteRepository, AttemptManagement $attemptManagement, CartRepositoryInterface $cartRepository, - OrderRepositoryInterface $orderRepository, OrderLocator $orderLocator, LoggerInterface $logger ) { @@ -103,17 +55,14 @@ public function __construct( $this->quoteRepository = $quoteRepository; $this->attemptManagement = $attemptManagement; $this->cartRepository = $cartRepository; - $this->orderRepository = $orderRepository; $this->orderLocator = $orderLocator; $this->logger = $logger; } /** - * @param QuoteInterface|Quote $vippsQuote - * * @throws CouldNotSaveException */ - public function cancel(QuoteInterface $vippsQuote) + public function cancel(QuoteInterface $vippsQuote): void { try { $order = $this->orderLocator->get($vippsQuote->getReservedOrderId()); diff --git a/Model/TokenProvider.php b/Model/TokenProvider.php index e2a830a0..ea7dce96 100644 --- a/Model/TokenProvider.php +++ b/Model/TokenProvider.php @@ -22,9 +22,9 @@ use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\HTTP\Adapter\CurlFactory; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Payment\Gateway\ConfigInterface; use Magento\Store\Model\ScopeInterface; use Psr\Log\LoggerInterface; +use Vipps\Payment\Gateway\Config\Config; use Vipps\Payment\Gateway\Exception\AuthenticationException; use Vipps\Payment\Gateway\Http\Client\Curl; @@ -52,7 +52,7 @@ class TokenProvider implements TokenProviderInterface private $resourceConnection; /** - * @var ConfigInterface + * @var Config */ private $config; @@ -89,7 +89,7 @@ class TokenProvider implements TokenProviderInterface /** * @param ResourceConnection $resourceConnection * @param CurlFactory $adapterFactory - * @param ConfigInterface $config + * @param Config $config * @param Json $serializer * @param LoggerInterface $logger * @param UrlResolver $urlResolver @@ -98,7 +98,7 @@ class TokenProvider implements TokenProviderInterface public function __construct( ResourceConnection $resourceConnection, CurlFactory $adapterFactory, - ConfigInterface $config, + Config $config, Json $serializer, LoggerInterface $logger, UrlResolver $urlResolver, @@ -192,11 +192,11 @@ private function readJwt($scopeId) throw new \Exception($response->getBody()); //@codingStandardsIgnoreLine } if (!$this->isJwtValid($jwt)) { - throw new \Exception('Not valid JWT data returned from Vipps. Response: '. $response->toString()); //@codingStandardsIgnoreLine + throw new \Exception((string)__('Not valid JWT data returned from %1. Response: '. $response->toString(), $this->config->getTitle())); //@codingStandardsIgnoreLine } } catch (\Exception $e) { //@codingStandardsIgnoreLine $this->logger->critical($e->getMessage()); - throw new AuthenticationException(__('Can\'t retrieve access token from Vipps.'), $e); + throw new AuthenticationException(__('Can\'t retrieve access token from %1.', $this->config->getTitle()), $e); } finally { $adapter ? $adapter->close() : null; } diff --git a/Model/Transaction/PaymentDetailsProxy.php b/Model/Transaction/PaymentDetailsProxy.php new file mode 100644 index 00000000..171c3219 --- /dev/null +++ b/Model/Transaction/PaymentDetailsProxy.php @@ -0,0 +1,28 @@ +configVersionPool = $configVersionPool; + } + + private function getPaymentDetailsCommand(): \Vipps\Payment\Api\Transaction\PaymentDetailsInterface + { + return $this->configVersionPool->get(); + } + + public function get($incrementId) + { + return $this->getPaymentDetailsCommand()->get($incrementId); + } +} diff --git a/Model/Transaction/StatusVisitor.php b/Model/Transaction/StatusVisitor.php new file mode 100644 index 00000000..34eb2915 --- /dev/null +++ b/Model/Transaction/StatusVisitor.php @@ -0,0 +1,79 @@ +isTransactionExpired(); + } + + if ($transaction instanceof Payment) { + return $transaction->isExpired(); + } + + return false; + } + + public function isCanceled($transaction): bool + { + if ($transaction instanceof Transaction) { + return $transaction->transactionWasCancelled(); + } + + if ($transaction instanceof Payment) { + return $transaction->isTerminated(); + } + + return false; + } + + public function isVoided($transaction): bool + { + if ($transaction instanceof Transaction) { + return $transaction->transactionWasVoided(); + } + + if ($transaction instanceof Payment) { + return $transaction->isAborter(); + } + } + + public function isAuthorised($transaction): bool + { + if ($transaction instanceof Payment) { + return $transaction->isAuthorised(); + } + + return false; + } + + public function isReserved($transaction): bool + { + if ($transaction instanceof Transaction) { + return $transaction->isTransactionReserved(); + } + + if ($transaction instanceof Payment) { + return $transaction->isAuthorised(); + } + + return false; + } + + public function isCaptured($transaction): bool + { + if ($transaction instanceof Transaction) { + return $transaction->isTransactionCaptured(); + } + + return false; + } +} diff --git a/Model/TransactionProcessor.php b/Model/TransactionProcessor.php index 78c4179a..73533914 100644 --- a/Model/TransactionProcessor.php +++ b/Model/TransactionProcessor.php @@ -45,6 +45,7 @@ use Vipps\Payment\Gateway\Exception\WrongAmountException; use Vipps\Payment\Model\Adminhtml\Source\PaymentAction; use Vipps\Payment\Model\Exception\AcquireLockException; +use Vipps\Payment\Model\Transaction\StatusVisitor; /** * Class TransactionProcessor @@ -128,6 +129,8 @@ class TransactionProcessor * @var ResourceConnection */ private $resourceConnection; + private \Vipps\Payment\Model\Transaction\PaymentDetailsProxy $paymentDetailsProxy; + private StatusVisitor $statusVisitor; /** * TransactionProcessor constructor. @@ -151,21 +154,23 @@ class TransactionProcessor * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - OrderRepositoryInterface $orderRepository, - CartRepositoryInterface $cartRepository, - CartManagementInterface $cartManagement, - QuoteLocator $quoteLocator, - OrderLocator $orderLocator, - Processor $processor, - QuoteUpdater $quoteUpdater, - LockManager $lockManager, - ConfigInterface $config, - QuoteManagement $quoteManagement, - OrderManagementInterface $orderManagement, - PaymentDetailsProvider $paymentDetailsProvider, - ReceiptSender $receiptSender, - LoggerInterface $logger, - ResourceConnection $resourceConnection + OrderRepositoryInterface $orderRepository, + CartRepositoryInterface $cartRepository, + CartManagementInterface $cartManagement, + QuoteLocator $quoteLocator, + OrderLocator $orderLocator, + Processor $processor, + QuoteUpdater $quoteUpdater, + LockManager $lockManager, + ConfigInterface $config, + QuoteManagement $quoteManagement, + OrderManagementInterface $orderManagement, + PaymentDetailsProvider $paymentDetailsProvider, + ReceiptSender $receiptSender, + LoggerInterface $logger, + ResourceConnection $resourceConnection, + \Vipps\Payment\Model\Transaction\PaymentDetailsProxy $paymentDetailsProxy, + StatusVisitor $statusVisitor ) { $this->orderRepository = $orderRepository; $this->cartRepository = $cartRepository; @@ -182,6 +187,8 @@ public function __construct( $this->receiptSender = $receiptSender; $this->logger = $logger; $this->resourceConnection = $resourceConnection; + $this->paymentDetailsProxy = $paymentDetailsProxy; + $this->statusVisitor = $statusVisitor; } /** @@ -202,15 +209,13 @@ public function process(QuoteInterface $vippsQuote) try { $lockName = $this->acquireLock($vippsQuote->getReservedOrderId()); - $transaction = $this->paymentDetailsProvider->get( - $vippsQuote->getReservedOrderId() - ); + $transaction = $this->paymentDetailsProxy->get($vippsQuote->getReservedOrderId()); - if ($transaction->transactionWasCancelled() || $transaction->transactionWasVoided()) { + if ($this->statusVisitor->isCanceled($transaction) || $this->statusVisitor->isVoided($transaction)) { $this->processCancelledTransaction($vippsQuote); - } elseif ($transaction->isTransactionReserved()) { + } elseif ($this->statusVisitor->isReserved($transaction)) { $this->processReservedTransaction($vippsQuote, $transaction); - } elseif ($transaction->isTransactionExpired()) { + } elseif ($this->statusVisitor->isExpired($transaction)) { $this->processExpiredTransaction($vippsQuote); } @@ -241,7 +246,7 @@ private function processCancelledTransaction(QuoteInterface $vippsQuote) /** * @param QuoteInterface $vippsQuote - * @param Transaction $transaction + * @param $transaction * * @return OrderInterface|null * @throws CouldNotSaveException @@ -250,7 +255,7 @@ private function processCancelledTransaction(QuoteInterface $vippsQuote) * @throws VippsException * @throws WrongAmountException */ - private function processReservedTransaction(QuoteInterface $vippsQuote, Transaction $transaction) + private function processReservedTransaction(QuoteInterface $vippsQuote, $transaction) { $order = $this->orderLocator->get($vippsQuote->getReservedOrderId()); if (!$order) { @@ -259,7 +264,7 @@ private function processReservedTransaction(QuoteInterface $vippsQuote, Transact $this->sendReceipt($order, $transaction); - $paymentAction = $this->config->getValue('vipps_payment_action'); + $paymentAction = $this->config->getPaymentAction(); $this->processAction($paymentAction, $order, $transaction); $this->notify($order); @@ -291,11 +296,11 @@ private function processExpiredTransaction(QuoteInterface $vippsQuote) /** * @param string|null $action * @param OrderInterface $order - * @param Transaction $transaction + * @param $transaction * * @throws LocalizedException */ - private function processAction($action, OrderInterface $order, Transaction $transaction) + private function processAction($action, OrderInterface $order, $transaction) { switch ($action) { case PaymentAction::ACTION_AUTHORIZE_CAPTURE: @@ -308,9 +313,8 @@ private function processAction($action, OrderInterface $order, Transaction $tran /** * @param OrderInterface $order - * @param Transaction $transaction */ - private function sendReceipt(OrderInterface $order, Transaction $transaction) + private function sendReceipt(OrderInterface $order) { if (!in_array($order->getState(), [Order::STATE_NEW, Order::STATE_PAYMENT_REVIEW])) { return; @@ -362,7 +366,7 @@ private function acquireLock($reservedOrderId) * @throws WrongAmountException * @throws \Exception */ - private function placeOrder(QuoteInterface $vippsQuote, Transaction $transaction) + private function placeOrder(QuoteInterface $vippsQuote, $transaction) { $quote = $this->cartRepository->get($vippsQuote->getQuoteId()); if (!$quote) { @@ -432,10 +436,15 @@ private function fixQuote($quote) * @return void * @throws WrongAmountException */ - private function validateAmount(CartInterface $quote, Transaction $transaction) + private function validateAmount(CartInterface $quote, $transaction) { $quoteAmount = (int)round($this->formatPrice($quote->getGrandTotal()) * 100); - $vippsAmount = (int)$transaction->getTransactionSummary()->getRemainingAmountToCapture(); + + if($transaction instanceof Transaction) { + $vippsAmount = (int)$transaction->getTransactionSummary()->getRemainingAmountToCapture(); + } elseif ($transaction instanceof \Vipps\Payment\GatewayEpayment\Data\Payment) { + $vippsAmount = (int)$transaction->getAggregate()->getData(); + } if ($quoteAmount != $vippsAmount) { throw new WrongAmountException( @@ -477,9 +486,9 @@ private function capture(OrderInterface $order) * Authorize action * * @param OrderInterface $order - * @param Transaction $transaction + * @param Transaction|\Vipps\Payment\GatewayEpayment\Data\Payment $transaction */ - private function authorize(OrderInterface $order, Transaction $transaction) + private function authorize(OrderInterface $order, $transaction) { if (!in_array($order->getState(), [Order::STATE_NEW, Order::STATE_PAYMENT_REVIEW])) { return; @@ -491,12 +500,12 @@ private function authorize(OrderInterface $order, Transaction $transaction) $payment = $order->getPayment(); if ($payment instanceof Payment) { - $transactionId = $transaction->getTransactionId(); + $transactionId = ($transaction instanceof Transaction) ? $transaction->getTransactionId() : $transaction->getPspReference(); $payment->setIsTransactionClosed(false); $payment->setTransactionId($transactionId); $payment->setTransactionAdditionalInfo( PaymentTransaction::RAW_DETAILS, - $transaction->getTransactionSummary()->getData() + ($transaction instanceof Transaction) ? $transaction->getTransactionSummary()->getData() : $transaction->getRawData() ); } diff --git a/Observer/CheckoutSubmitAllAfter.php b/Observer/CheckoutSubmitAllAfter.php index 7f520f29..e3503c5c 100644 --- a/Observer/CheckoutSubmitAllAfter.php +++ b/Observer/CheckoutSubmitAllAfter.php @@ -83,7 +83,7 @@ public function execute(Observer $observer) return; } - if ('vipps' == $payment->getMethod()) { + if ('vipps' === $payment->getMethod()) { try { // updated vipps quote $vippsQuote = $this->quoteRepository->loadByOrderId($order->getIncrementId()); diff --git a/Test/Unit/Gateway/Command/GatewayCommandTest.php b/Test/Unit/Gateway/Command/GatewayCommandTest.php index 6866f134..f409493b 100755 --- a/Test/Unit/Gateway/Command/GatewayCommandTest.php +++ b/Test/Unit/Gateway/Command/GatewayCommandTest.php @@ -132,8 +132,13 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['handle']) ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManager($this); - $this->jsonDecoder = $this->objectManagerHelper->getObject(\Magento\Framework\Json\Decoder::class, []); + $this->jsonDecoder = $this->objectManagerHelper->getObject( + \Magento\Framework\Json\Decoder::class, [ + 'jsonSerializer' => $this->objectManagerHelper->getObject(\Magento\Framework\Serialize\Serializer\Json::class) + ] + ); $localizedExceptionFactory = $this ->getMockBuilder(\Magento\Framework\Exception\LocalizedExceptionFactory::class) ->disableOriginalConstructor() diff --git a/Test/Unit/Model/TokenProviderTest.php b/Test/Unit/Model/TokenProviderTest.php index 54a586a0..d44f8850 100644 --- a/Test/Unit/Model/TokenProviderTest.php +++ b/Test/Unit/Model/TokenProviderTest.php @@ -31,7 +31,7 @@ use Vipps\Payment\Gateway\Exception\AuthenticationException; use Vipps\Payment\Model\TokenProvider; use Vipps\Payment\Model\UrlResolver; -use Zend_Http_Response; +use Laminas\Http\Response; /** * Class TokenProvider @@ -51,17 +51,17 @@ class TokenProviderTest extends TestCase private $connection; /** - * @var ZendClient|MockObject + * @var \Magento\Framework\HTTP\Adapter\Curl|MockObject */ private $httpClient; /** - * @var ZendClientFactory|MockObject + * @var \Magento\Framework\HTTP\Adapter\CurlFactory|MockObject */ - private $httpClientFactory; + private $adapterFactory; /** - * @var Zend_Http_Response|MockObject + * @var Laminas\Http\Response|MockObject */ private $httpClientResponse; @@ -118,24 +118,25 @@ protected function setUp(): void $this->select = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() ->getMock(); + $this->select->expects($this->any())->method('from')->will($this->returnSelf()); $this->select->expects($this->any())->method('where')->will($this->returnSelf()); $this->select->expects($this->any())->method('limit')->will($this->returnSelf()); $this->select->expects($this->any())->method('order')->will($this->returnSelf()); - $this->httpClientFactory = $this->getMockBuilder(ZendClientFactory::class) + $this->adapterFactory = $this->getMockBuilder(\Magento\Framework\HTTP\Adapter\CurlFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->httpClient = $this->getMockBuilder(ZendClient::class) + + $this->httpClient = $this->getMockBuilder(\Magento\Framework\HTTP\Adapter\Curl::class) ->disableOriginalConstructor() ->getMock(); - $this->httpClient->expects($this->any())->method('setConfig')->will($this->returnSelf()); - $this->httpClient->expects($this->any())->method('setUri')->will($this->returnSelf()); - $this->httpClient->expects($this->any())->method('setMethod')->will($this->returnSelf()); - $this->httpClient->expects($this->any())->method('setHeaders')->will($this->returnSelf()); - $this->httpClientResponse = $this->getMockBuilder(Zend_Http_Response::class) + $this->httpClient->expects($this->any())->method('write')->willReturnSelf(); + $this->httpClient->expects($this->any())->method('read')->willReturnSelf(); + + $this->httpClientResponse = $this->getMockBuilder(Response::class) ->disableOriginalConstructor() ->getMock(); @@ -170,7 +171,7 @@ protected function setUp(): void $managerHelper = new ObjectManager($this); $this->action = $managerHelper->getObject(TokenProvider::class, [ 'resourceConnection' => $this->resourceConnection, - 'httpClientFactory' => $this->httpClientFactory, + 'adapterFactory' => $this->adapterFactory, 'config' => $this->config, 'serializer' => $this->serializer, 'logger' => $this->logger, @@ -230,7 +231,7 @@ public function testGetWithoutValidTokenRecord() "access_token" => "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNx" ]; - $this->httpClientFactory->expects(self::once()) + $this->adapterFactory->expects(self::once()) ->method('create') ->willReturn($this->httpClient); @@ -259,15 +260,15 @@ public function testGetWithoutValidTokenRecord() ->willReturnSelf(); $this->httpClient->expects($this->once()) - ->method('request') - ->willReturn($this->httpClientResponse); + ->method('read') + ->willReturn($this->httpClientResponse->toString()); $this->httpClientResponse->expects($this->once()) ->method('getBody') ->willReturn(''); $this->httpClientResponse->expects($this->once()) - ->method('isSuccessful') + ->method('isSuccess') ->willReturn(true); $this->serializer->expects($this->once()) @@ -280,10 +281,10 @@ public function testGetWithoutValidTokenRecord() public function testGetCouldNotSaveException() { $exception = new \Exception(); - $this->httpClient->method('request') + $this->httpClient->method('read') ->willThrowException($exception); - $this->httpClientFactory->expects(self::once()) + $this->adapterFactory->expects(self::once()) ->method('create') ->willReturn($this->httpClient); diff --git a/composer.json b/composer.json index 0e109461..7c1a5959 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "vipps/module-payment", "type": "magento2-module", - "description": "Vipps Payment Method", + "description": "Vipps MobilePay Payment Module for Magento 2", "license": "proprietary", "require": { "magento/framework": "101.0.*|103.0.*", diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 7051268e..9f5cf339 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -17,10 +17,10 @@
- + - + complex vipps-section Vipps\Payment\Block\Adminhtml\System\Config\Fieldset\Payment payment/vipps/active @@ -33,13 +33,13 @@ - https://www.vipps.no/ - + https://vippsmobilepay.com/ + Vipps\Payment\Block\Adminhtml\System\Config\Fieldset\Hint - https://github.com/vippsas/vipps-magento - + https://developer.vippsmobilepay.com/docs/plugins/magento/ + Vipps\Payment\Block\Adminhtml\System\Config\Fieldset\Hint @@ -51,6 +51,11 @@ Vipps\Payment\Model\Adminhtml\Source\Environment payment/vipps/environment + + + Vipps\Payment\Model\Config\Source\Version + payment/vipps/version + Magento\Config\Model\Config\Source\Yesno @@ -61,7 +66,7 @@ Magento\Config\Model\Config\Source\Yesno payment/vipps/profiling - + payment/vipps/vipps_payment_action Vipps\Payment\Model\Adminhtml\Source\PaymentAction diff --git a/etc/adminhtml/system/express_checkout.xml b/etc/adminhtml/system/express_checkout.xml index b7293e0c..894097c4 100644 --- a/etc/adminhtml/system/express_checkout.xml +++ b/etc/adminhtml/system/express_checkout.xml @@ -20,6 +20,9 @@ Magento\Config\Model\Config\Source\Yesno payment/vipps/express_checkout + + + diff --git a/etc/config.xml b/etc/config.xml index c867ba21..3aeab2bd 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -20,7 +20,10 @@ Vipps\Payment\Model\Method\Vipps Vipps + Vipps + MobilePay 1 + vipps_payment pending 1 1 diff --git a/etc/di.xml b/etc/di.xml index a812295a..05c5ad87 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,6 +1,6 @@ + + + + + + ProxyCommandManager + + + + + + ProxyVippsCommandManager + + + + + + versionedCommandPool + + + + + + + VippsEcommerceCommandPool + VippsEpaymentCommandPool + + + + + + + versionedPaymentDetailsPool + + + + + + + Vipps\Payment\Gateway\Command\PaymentDetailsProvider + Vipps\Payment\GatewayEpayment\Command\PaymentDetailsProvider + + + + + + + VippsEpaymentCommandManager + + + + + + VippsEpaymentCommandPool + + + + + + VippsEpaymentInitCommand + VippsEpaymentCaptureCommand + VippsEpaymentRefundCommand + VippsEpaymentCancelCommand + VippsEpaymentGetPaymentCommand + VippsEpaymentGetPaymentEventLogCommand + VippsEpaymentSendReceiptCommand + + + + + + + + VippsPaymentSendReceiptRequest + VippsPaymentSendReceiptTransportFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + + + + + + + Vipps\Payment\GatewayEpayment\Request\SendReceipt\GenericDataBuilder + Vipps\Payment\GatewayEpayment\Request\SendReceipt\OrderLinesBuilder + Vipps\Payment\GatewayEpayment\Request\SendReceipt\BottomLineBuilder + + + + + + POST + /order-management/v2/ecom/receipts/:order_id + + order_id + + + orderLines + bottomLine + + + + + + + + Vipps\Payment\Gateway\Config\Config + Vipps\Payment\Model\Logger + + + Vipps\Payment\Gateway\Config\Config @@ -63,6 +176,12 @@ + + + Vipps\Payment\Gateway\Config\Config + + + Vipps\Payment\Gateway\Config\Config @@ -81,19 +200,31 @@ Vipps\Payment\Block\Info VippsValueHandlerPool VippsValidatorPool - BuiltInCommandManager + ProxyCommandManager - BuiltInCommandManager + EcommerceCommandManager - + - VippsCommandPool + Vipps\Payment\Model\CommandPoolProxy + + + + + + VippsEcommerceCommandPool + + + + + + VippsEpaymentCommandPool @@ -111,7 +242,7 @@ - + VippsInitiateCommand @@ -131,7 +262,7 @@ VippsInitiateTransferFty Vipps\Payment\Gateway\Http\Client\Curl Vipps\Payment\Gateway\Response\InitiateHandler - VippsInitiateValidator + VippsEcommInitiateValidator @@ -140,6 +271,7 @@ /ecomm/v2/payments + @@ -277,6 +409,7 @@ VippsCancelValidator + PUT @@ -286,6 +419,17 @@ + + + + PUT + /epayment/v1/payments/:reference/cancel + + reference + + + + @@ -303,11 +447,29 @@ Vipps\Payment\Gateway\Validator\InitiateValidator - Vipps\Payment\Gateway\Validator\AvailabilityValidator + Vipps\Payment\Gateway\Validator\AvailabilityValidatorProxy - + + + + availabilityValidatorPool + + + + + + + + Vipps\Payment\Gateway\Validator\AvailabilityValidator + Vipps\Payment\GatewayEpayment\Validator\AvailabilityValidator + + + + + + Vipps\Payment\Gateway\Validator\OrderValidator @@ -315,6 +477,14 @@ + + + + Vipps\Payment\GatewayEpayment\Validator\ReferenceValidator + Vipps\Payment\GatewayEpayment\Validator\InitiateValidator + + + @@ -466,4 +636,246 @@ Vipps\Payment\Model\Logger + + + + + VippsPaymentPostRequest + VippsPaymentsTestPostTransferFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + Vipps\Payment\GatewayEpayment\Response\Payment\PostHandler + VippsEpaymentInitiateValidator + + + + + + POST + /epayment/v1/payments + + amount + paymentMethod + customer + reference + returnUrl + userFlow + paymentDescription + + + + + + + Vipps\Payment\GatewayEpayment\Request\Payment\AmountBuilder + + + + + + + + VippsPaymentCaptureRequest + VippsPaymentCaptureTransferFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + Vipps\Payment\GatewayEpayment\Response\Payment\TransactionHandler + VippsPaymentCaptureProfiler + + + + + POST + /epayment/v1/payments/:reference/capture + + reference + + + modificationAmount + modificationReference + + + + + + + Vipps\Payment\GatewayEpayment\Request\ReferenceDataBuilder + Vipps\Payment\GatewayEpayment\Request\ModificationDataBuilder + + + + + + Vipps\Payment\Model\Profiling\TypeInterface::CAPTURE + + + + + + + + VippsPaymentRefundRequest + VippsPaymentRefundTransferFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + Vipps\Payment\GatewayEpayment\Response\Payment\TransactionHandler + VippsPaymentRefundProfiler + + + + + Vipps\Payment\Model\Profiling\TypeInterface::REFUND + + + + + POST + /epayment/v1/payments/:reference/refund + + reference + + + modificationAmount + modificationReference + + + + + + + Vipps\Payment\GatewayEpayment\Request\ReferenceDataBuilder + Vipps\Payment\GatewayEpayment\Request\ModificationDataBuilder + + + + + + + + + VippsPaymentCancelRequest + VippsPaymentCancelTransferFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + Vipps\Payment\GatewayEpayment\Response\Payment\TransactionHandler + Vipps\Payment\Gateway\Config\Config + VippsPaymentCancelProfiler + + + + + POST + /epayment/v1/payments/:reference/cancel + + reference + + + modificationReference + + + + + + + Vipps\Payment\GatewayEpayment\Request\ReferenceDataBuilder + Vipps\Payment\GatewayEpayment\Request\ModificationDataBuilder + + + + + + Vipps\Payment\Model\Profiling\TypeInterface::CANCEL + + + + + + + + VippsGetPaymentRequest + VippsGetPaymentTransferFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + + + + + + GET + /epayment/v1/payments/:reference + + reference + + + + + + + Vipps\Payment\GatewayEpayment\Request\DefaultDataBuilder + + + + + + + + VippsGetPaymentEventLogRequest + VippsGetPaymentEventLogTransferFty + Vipps\Payment\GatewayEpayment\Http\Client\PaymentCurl + + + + + GET + /epayment/v1/payments/:order_id/events + + order_id + + + + + + + Vipps\Payment\GatewayEpayment\Request\DefaultDataBuilder + + + + + + + + versionedFallbackAuthPool + + + + + + + Vipps\Payment\Model\Fallback\Authorise\Commerce + Vipps\Payment\Model\Fallback\Authorise\Epayment + + + + + + + + + Vipps_Payment::images/vipps_logo_rgb.png + Vipps_Payment::images/mobilepay_logo.png + + + + + + + + + 70 + 107 + + + + + + + versionedPaymentLogo + versionedPaymentLogoWidth + + diff --git a/etc/graphql/di.xml b/etc/graphql/di.xml index 8b4c04b8..c6140c6c 100755 --- a/etc/graphql/di.xml +++ b/etc/graphql/di.xml @@ -19,4 +19,11 @@ Vipps\Payment\Gateway\Config\Config + + + + payment/vipps/version + + + diff --git a/etc/schema.graphqls b/etc/schema.graphqls index d94830a1..a08916dd 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -8,7 +8,8 @@ type Mutation { input vippsInitPaymentInput { cart_id: String! - fallback_url: String + fallback_url: String, + deactivate_cart: Boolean } type vippsInitPaymentOutput { @@ -21,3 +22,8 @@ type VippsPaymentDetails { reserved: Boolean restore_cart: Boolean } + +type StoreConfig { + vipps_version: String @doc(description: "Vipps api version (vipps, mobilepay).") + vipps_label: String @resolver(class: "Vipps\\Payment\\GraphQl\\Resolver\\GetPaymentLabel") @doc(description: "Vipps payment label.") +} diff --git a/i18n/nb_NO.csv b/i18n/nb_NO.csv index 0ef4992d..2c71105c 100644 --- a/i18n/nb_NO.csv +++ b/i18n/nb_NO.csv @@ -1,8 +1,8 @@ "A total of %1 record(s) were deleted.","Totalt %1 rad(er) ble slettet." "Invalid request","Invalid forespørsel" -"An error occurred during request to Vipps. Please try again later.","En feil har oppstått i forespørselen mot Vipps. Vennligst prøv igjen senere." +"An error occurred during request to %1. Please try again later.","En feil har oppstått i forespørselen mot %1. Vennligst prøv igjen senere." "An error occurred during payment status update.","En feil oppstod under betalingens statusoppdatering." -"Your order was cancelled in Vipps.","Bestillingen din ble kansellert i Vipps." +"Your order was cancelled in %1.","Bestillingen din ble kansellert i %1." "An error occurred during Shipping Details processing.","En feil oppstod ved behandling av leveringsdetaljene." "Can't cancel captured transaction.","Kan ikke kansellere en trukket transaksjon." "Captured amount is higher then remaining amount to capture","Beløpet som du prøver å trekke er høyere enn resterede beløp som kan trekkes" @@ -14,7 +14,7 @@ "Gateway response error. Incorrect transaction data.","Gateway response error. Ugyldig transaksjonsdata." "Gateway response error. Incorrect initiate payment parameters.","Gateway response error. Ugyldige parametere for initiering av betaling." "Gateway response error. Order Id is incorrect","Gateway response error. Order Id er feil" -"Can't retrieve access token from Vipps.","Kan ikke hente access token fra Vipps." +"Can't retrieve access token from %1.","Kan ikke hente access token fra %1." "Refreshed Jwt data.","Oppdatert Jwt data." "Vipps payment method does not support Capture Offline","Vipps støtter ikke reservasjon uten nettilkobling." "Vipps payment method does not support Refund Offline","Vipps støtter ikke refusjon uten nettilkobling." @@ -22,8 +22,8 @@ "Some error phrase here","Legg inn feilmelding her" Show,Vis "Created At",Opprettet -"You will be redirected to the Vipps website.","Du vil bli videresendt til Vipps landingsside." -"Almost done! Remember, there are no fees using Vipps when shopping online.","Snart i mål! Husk, Vipps er gebyrfritt når du handler på nett." +"You will be redirected to the %1 website.","Du vil bli videresendt til %1 landingsside." +"Almost done! Remember, there are no fees using %1 when shopping online.","Snart i mål! Husk, %1 er gebyrfritt når du handler på nett." "Continue to Vipps","Fortsett til Vipps" "Vipps Payment","Vipps Betaling" "Enable this Solution","Aktiver denne løsningen" diff --git a/phpcs.xml b/phpcs.xml index e6a241f0..34240b0b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -15,9 +15,9 @@ --> Code Sniffer Configuration for Vipps Payment - - + + Api/ Controller/ Cron/ diff --git a/view/adminhtml/web/styles.css b/view/adminhtml/web/styles.css index 705c6875..50b96013 100644 --- a/view/adminhtml/web/styles.css +++ b/view/adminhtml/web/styles.css @@ -13,5 +13,5 @@ * IN THE SOFTWARE. */ -.vipps-section .heading {background: url("images/vipps_logo_rgb.png") no-repeat 0 50% / 18rem auto; padding-left: 20rem;} +.vipps-section .heading {background: url("images/Vipps_MobilePay_Logo_Primary_RGB_Black.png") no-repeat 0 50% / 18rem auto; padding-left: 20rem;} .vipps-section .button-container {float: right;} diff --git a/view/base/web/images/Vipps_MobilePay_Logo_Primary_RGB_Black.png b/view/base/web/images/Vipps_MobilePay_Logo_Primary_RGB_Black.png new file mode 100644 index 0000000000000000000000000000000000000000..3302d690add1d6ae242dedf946992d01f540b182 GIT binary patch literal 27033 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
@?6al6e_a{ZZ`8s8TK`FRd z1t;;p=pM|>#WUi@{ABbEN)81yAZ9Nesvq%Pcfm91HMu>K5tVN`pH%N!Z-Uoz`3+{+ zc?w1mOb)#Vf0zF3Oaabf1fJ)c!W%@HQH3vP`4L$)v80B$8&4Bc<6pI3o~&UZw`d{m z$-l!%Knu85nYFUp zM8qM|?5b{ufHB!zEaPongF_;IUBUg&m9eD_@y(fcFyMk3<9@d11#BCe;6JVm@x4YM zt)e=Pi0w284JGLaw9wju0EHY)HRfuwT-Pe2G z9AMMG-35Tr>;D`f$Y+XlWU{3S9RU~5lmN?^e~pl@tHqG0VLu!6VICWKAJ)_;a3$cc zz28|h37r`K#GL#_nW=(#s(XhpZCExc19^s|gde*n=uNft%v!fgH z%4dIVn89E~KMG(261g&lkN8F72FP1Nhl1b2S9!fFS`1ypu6#W80y;K%HWu$!>;r+j zVa*k5`PHiNW0I2Yn@Ehxw<#^Pp+X;-aFOUj9|HpqwZevs#{0c`n4u8J7Z8uwSa84> zk`qQ1u|Izi=_*Lus-lJPX2@1{>?UEm!yK!cydcil-a+o&>9E;_Y@O-S^LM`NCQ*($ zLXT8mFCF&L)KcvijT!O9!al67ewsq2xwV_hpJ-_v^dk|)@^(RC0Q9KCt+R2eT;ubC>T3{Ai8Z-A&u^{U zBn)fpBS1y1qcMZ2qUQ|xD_0R~PxzCV)B2l#rna$MKvX3S`tDzTgEU*~lYcN1FISL~ z)(7~k^nRHl@%&!bClUsCbL3P5_vb~s{;eJUQgu1Cnu>nufw4%lP-IQ=duNwVr?6il zNSNSE;=)Ner9GT2YB}P!{I{qjrn!ij`A{fxcW)L~s z5%!MC*FFUO?CS5iP;feF`y`fdd3MA+CsAM3GHwIDA zBDfzF4Y4gFD8O>2x*szNCu`6XFJqfni$PpDD=dUIAVGwoVpgmI05-4wF}YY><6D)= zgnVB5ae4o%*AL$R&T!YCJo!$UPBf@lJu2g?$5_qXcLK_R&7;`+a?NFXEJnxDQ*pG-;2_=>o=o5 zvpV2ayh4|p?URr4X>N)=+|*O{&llqmaBzEH#!_245};zQ+p;U@2@THsC7I9+n^Bk|iWsX0tWJT@2AwPwb z+^;}$s9FePN_LKxexr5x|3@e!bnGr7B_Ih+{Gp5_c;67xB2#G!=CAX_-UKbmA(!@E zq2h&lLaSH0HxYCWQ>GO=iOpSF zdEuZhwDTC17f3WyH(w+`w)CA*jfjI=c~Xkn)`fGnX(4D|)f0ifXEeC!wz!lR_fiem z^=ye-i-Oq+Ovc%iVu(-m5$|28f5MOEv&fEi>GT_^ckkMYWG&xv{=$%pG}LL#$2tuz z9eP!D1yzQ!uUwx=>`_?=y)bRlS?L4oHC?3Wg&m(ij1Tp6f$sA<^%(AAU`tVn-Va@# z2s*}dCN?(CTbqSZyLew(y_1X7G8rzDj5V1`rA zKO5Xuh@0t(_a7ynIvX)BQAOgI_*vvJn$eS@YDi@$SlfMkDyr2aDHD-kbY6L&+&XU& zMpzR#or~*x*Na|oy4!o-&K$HCppoX8nN%WGB5J#-r1`pjM<$2~U{@fLa`BY;qJ3vn z|1aFw`52FW(`HuS{NU^D*oif@R|J%n!esvSB1E)M$o6>b#5Cl=QHG!Wxy-_YK=rND zDlYB)lR>8C3p~h4Ad0k%lV!r$gu(hvvQP8mf1Kk$%>6URXyVxZbRlaT?!aVj34%tt z)9`}P%R8#RUlwfxnmgw#C7u{^pn`lH?U=wE#W9t=*I(ehwepMp>h-&Lbn53hoAB(KkYhGZdPm0w_s^TEF1t5r1CV3*`DPz!ftn)TMLhF!9BNKJ zd>l!5ZA4@0^u0pQ+xZ&Qe3yym-*5jfHRreH6~%>;@C(!&NE;am|MY44OIQRu-2JZM zRdaC`LAk7f>SWk2-ec3^APXxSMB>Qlk>o90fsOsd+mDjjr!>)5?=`yl^jCjIR#@&E zUPT1~vQ;HK7G5*(Sh4 zBFJ*ssb8F2(7ibFc0eoy}qxB$_AqfcGjX zjX&s%oM!4XA8zelUpFl^Cz?kHngS7U1qoXJN4jz5d|)OQ$fvSAqP~*PRSo8^Q2uxW~ygj3~~%{YP(Ss@CSQ zqSZdR0(l=2m2d?&oWR?vO~Nuh^p**b-b#V_a{axap!E!@UYaW`P%nKk))kwd+fSvL zgR}J6f5eTWtvjz%9;G(!khNDNjI!$$jOYv zKi~PPZ})9arvoE{_~`B5=fOkjpD^-2PEtYaBATD{HUXY=P&9nu>)lMol6c%;IkdWt z112y#0NIh=o<>6-IoM@P+Rn~%wcfP8bq zsSym~GnXv%Ikjei1l(>kL1ldcOmwnWDLregqL>-u6g^55Cae!GX(% zb=x?Y6s4ZZeHh9wW@E;)TrzAim`>|#{0mvGCJvx^WKi0CCU>L_DHr-k z`H;P^G)zM!e>hM^{pql2POk8(^}inzkSzLby8hWd_4v^saoi#w>@ij2$8l-unaEsR zwx4k1KRxZr$0zb^$!UwbWPH%LvWU~k(L+ZX+us9nP1-BbjD$0%X#M$j`(NJY3xT7V zK5c=Tar}PFOEYGlnyEeQJe@&FvD=kftPb4nqR3kxiXpdOtj6RIM~|_XrtQm9@865acVz$!VH?Y z_r-`d@u}+~ymv^<-~$@KkJD%uU*Ucp`X3dByZtYdjPZ;=1u>@azGzzE<`VJ}<0Z12 zz?tmH14(DyyZGDGllIVD8)hHD4#YmD>MMP>HmC2JdXMoz&?jb!|#a%7`|d zx9J_RgO6wppJw}=vgEV@TD!NSeRh=J*3d`k67~Rt3!t>rJ?_GHS^AwPZ1uBGfL25K zKrX$Z_E1QDD@-wS!m~>02GH!F^2(jT&t_F(Ga&_d?&iSx*x8xAm5Lnv@JFUr;U~Bq zmACHms>P;!>sek?Uj&XC!_KNc745(3<=*ZM_ie6PeNEH&!H;AD{#vBRao;n3N*{9n zGA_5lp*xU9!P~GQ?HG4z-W?x>CQ~%7g73DLU=0N>-vDN~ekpc+frxpC8F6nV`gm>= zHU`qbJcuyVDNMHNuO#1lh026`^@)SwZTVo&J)5DRbxWq54;<*~g`QFD;MAUNniDKA z{;8{U)Q%nER9Wao2--_~`D@a9vGaQ6B`=jO^#jrSyIC6%mqg#tA5lX&970nt2Iu16 zAwAtUmuL-Okkgc5t5OZ**sz6#W47>bS|;^YVNXccb2ADLQ9fJL$&3OHJzVb+Ecb{k zTcU%iQwMTnlnOLJK}j6|Y2!=xoELKjGtKa?8-iB_(Dp&M93zs^TwhJykb9e*EA1s@ z=nW_Q*3H_#z01a(e!8c;P8Tz0%O3RCL|}YDJ1q0fH`SR2UOM!*{GAjZJX$W*l&AYW z8KxL_N@QMYCZ_DPT$dacKH0vc5;y2z4->?FkZGoX6s5n0}QXeCh`OH4Qd!Q!}a%Q4J?HoDO>6!kHu|&(_zb45k z?Ut5b&8X7?;3JepVNsFkg~6;nddC?Z0c3GxIC z;!KLs@OP$J7y0S?uuOfIfLn_K=VK!H`Jztn!2{^MWy{?wyg zHs{%U){E)u=0@W<0|W2QPVb+ag!iv1PX?szmk$X>nJ5szk)e%+i~NFe4&}?*y7|~! z;i=V21*RB{k=+l3W5+@7lBdzE8|YzVH=~$+O0nxbeBH?l^}NTTO&l8zpZ2v~wcbK> zd0I)a53huH=WDU#onuA*zxJ;EpXv7hD@7u7hcd@*-H1@w93yu{5fMW_& zRE~v|(-LzSHo~0dT$IzQVa~I}n#G*Q#(byl&-ah`JU;vBdc1dC*ZY0FPJ3Uk>-Bst zdLfG^ld^8?Xu^b$bXdcFZK{vT(TWUF14i@#i33t+I5JNWS-n)U&=ehfe!1C4wjM28 zb%(@^<9R^)T+myWx#@Z8NuGu4HER`aB7p0=wgUKo?VxG9Bn_>~hjAG0mUXwoTjf00 zlT1Q6!*F0a?~W~S~xx@_ATy?dt= ze=3<+Q^?KgH=0oLb|4_NMDwpnhveQD=ReC-$AncFFWr+^fx~4V;U|xG*2qmNwe$!Y zvd4*=WT6aJBqw?QdjXyz8RyTdKjYqXf(AxzP2YIZq9+9-nS-ay?H9I!LC<;Iv(RdJ{eNjkw^cX~mvs`K+q9aNR|7Yw{?$}b17(X2zzQRH#aTf$=X+hvc@ zE<$0^?JR69##8I~pMvx?d7?KoFdNT)Y{2O1amhO|ib5W2&AuS;lNkdc^=mc6u3pi_ zf+kF()j7~2!kU9MV;6arJl%UmNg+$Nh~_Uorb zIZ~z@T4d(eBju1y+F7dE^P2;Vwl6#)COyE)6xY6|aq94otVW}J$6#?s<5o03Yo+s8 zwvPYZ*YrBS!l6<5Fg}r5(@Z%S*KPyY%9c7>sQ{Rqu8Q-`97|HUTFzCT68DqbL6cY+5jNoRPHjYuryg6*z8B2p`!7Zy)fkmAU)Q(8=PvHWqi=JH@-9;+ zb$Sb+9@QOBdCRNy0im}AVD29!%B$QH7tx7EJFQcv_1Ox7eaDv*Ty$8#?wO{dqm)Iu z0GS9^`32tDn{Y3;N#VxS4H&o{>MyaiH3zq&zq_I*0vxRP>NGXQU2$ofq5Kye>qE{R z>lPIHp(X(SVU88Ph4GXFKLne7iPG>VEVnwCaz~qdwF6q}OWwJGbdS*Aja51(I4>RvHo4 z(nps7%$i{XRwi}{Y2%bthH4FnR3C_6+BS=rRvPn5gv2x>5)ACooouA=uZ#x#PtN%V zbtF-Dv(iX86ylJY5x95zh|KoqvEh+wXn0OXkm#Fzydcq;Qv?^m6ej~KGMBThE-2%Q z<=qb-Gq5xE5RQDC*535)HLBzFdh49vu^wB#YSW-gAHWwvEQ(4?tMqD$j2uMWkWTf2 zDWC7dd{}G)NIJ+<*IW{^HjS*ERV#`rv1*gg@O42=1~z9m)<&_vN}0yNC0P9&!%Xjy z<|9wI@R46qjnbSVpGXmZvO9N`Douv?)#CS0QR~0AhD2MgNKQP{qN0@lZkQ`JN?cy6 zQ%#-NX@;L)pi5TdVaZ9)usqjHZDhsO@jUUpxy)A3ZtR%2eh2X6J# z&)(HDtvdgW(gsq|-UIbg*mWtoJc-!Rt^kX|x5@ZntG+@bp%1p-s-Nj#N$eekK0gI% zaX5*}( zB1_vIn3$};z_?T-8^SIvS(FWFh4>g4IYwN|`0zC{if*lc7se{BhkvT?!| zr&f-eoy5Ro%pt54T}I`UWo3@D+pqyej6Nsr9lm#pcE2?-b94pb#(D#YqVQf0@A6p0 z{jm4g-Lm_8YxVMJeO63@bf24W^v^P3tP9K&&H*#;&5=0xiF;2DY*f;vY;O0M()phj zY!s3{D@lU>4j)i_4LOaoJ#J=k@P63PZ6Jrwf($yJ{$Y5aHfxjL*Tt1BnOlOJc+#l@A8#MBPH)2t!jwi4(E=pBOSQ=~=w{>78kX%SRBB zheow<3Xk$+K9X#4M=RQP6-H|2bKMm6C7n$!II_DiL(Os;PyO_@gCKd;kJPaO&YcV&b_p}o<%o^3-l#9?Cvto#6Iizx`8-9Iim z5!!><-yfHw`N-oEJegw?B#kYS#IahJEwbzxIMe>f&1enIZqd?pAp89?Q%**=*#lPb z3@wLCA+2@8;)7_MlC!9d_&qk~mAqXyz#82fcC<{4sk;gD~nS#1y4nm z@`Bq<^gvvrC2^QkPs!4<)<75TpGc7rE3{mhZVei!n(Av6?aX;f*o6kvl0>URJJ!z_ zx&)T8OR?gwI!v4rQz$UsoC%jg-Xq`OhPX)3xCYg1E$t2wG-vuq+Il#_GG=6pYlr|Y zFoF)5NW;k6+V-^TvVVFZ7^;^A_eJ(x^G$x#o6#=Eh?Mm=N4LC9xZ4c`qUSg@cQMDR z;uWT<+q%UzH43Y`Kt3V|f3kEPI~%D4aY;o-On3*Ckw+=b0M-g z(8z&HR&L>vnA*{2O;!v-TZ^L_*De?xmqnb|TB=OFD41XIukdt(7;pZjp!}|7C>n}G@>N$IVkxXLJ+p+$d=c#~m=kA;= zcVOLW{94zWjI0GY6HZV1=_Z7>47AKN<#_sP8{VNs?xKJ%@7b9FT~F6dF7ctP$84NBv2cmsj2d06UIT9mlX0nJ7`n7$HB~4&bIVVbtnI1dQIF0^$F7<{V<*&N zLFcA}@!mLa_0_F=iCzhxwc4UABX)lfdHq}6GdGgyfJftyvUe#YHTJ|Q+a5Unc7Cni zNAo#gp`zftRR2~JI44uB)x?nW+&8N^%C_+jm%fLG{d(fh6?N=k4e%zn$~ST;sN9s| z#gmROyq_*x%lEo>wW|hoJfqo7spR3=B-(^mo3iLA?^iabM^AJleu>rr(a{&_Cfp=Q z>710l!B{B-yp!(TYDtE+x4TRTS>|&RiV&@{w&0$Hg`04{Wm*q>qq76*XT*L@5+bAV z$3|+!95JYor(#tAPIOVCym`jk6rlZrXVml9d@l5pX8=`Vf z5ame3a?u*dTIb?y3n@eHvXO0;0VBAxi$%~W$I$ouB9s4WRWqaBDKNhW`>?hIJ{2|x@CTFy8lXj?h3+fEQ@-rcRE1x0M7gQ9 zDA9rd{X^1-#ZA~hDnF405YC%1=9Z+=@;Fr1N$$|mDJ{go83s)8eXE)j zd~mmL?phq)(qt+j|F51yi#qO7%SxgN-_yz)iih~TZtn;o3}N?MvfXdSFGuZx9>F6? zZ$k92;$G&VZ)Ej2k~`NUi-_wA7|-I-8<$qpPeTP5dwfO$idvfIOtHv<4&tq@tPcgK z3hsC?TcyY}lIFnZ`Ns3`2{#WA0z}9Y9PREtxfne?Y3N3ef-d?tKm17kTfwn9BH^e| zb(Za9jV{}m;1>DVm!<7wN||lFx#NV9M3_m<&mRG7j^4Zg&ZxH%<3{p{ftFL65K#OK=feZSLU%!O< zm9_$MS@U-q!lW#EIu&i2Qk#_C=bxLY;Xt>np?J^&0zO!pLYSgX)|MB<%!cpB3BlZ} zb3mQ%fD^fa%S^{dr!0Du7EIctJCo^0$s81>!^Z6X`H`{Na<;b63l3t@Dy(Q8gmV&< zzW5~HDn1yT=0rJ^;A)Z(`|?1RM&<_%pS&jRYt3>5ysoy0ZYUTmu9vQ4UQCGXlln)E z1CVid>Lb1y2vYT5n{))*n4@P&J9~>$MivscrQ69_wrVE6B-#AKw1S(TzaJW?Faj_S z<(<22{%-ZT zclWXuW_XjWwvvBy&?oXs^5|lO&(m2#6J-Us*lgPLQ^zJ;lPfg2WY4@ON{lZ?SsVkq zb)q^MrRuJ(&V6{UrKzQ*?p0j2=1Kk5jh#q?iVJ@RrMVEAP9CLfza5Nd9lbUCX*MFR z`GQO33M?USF1D)sa8>!>J3}~Gi(f#m57L(KBqUEQb{IF+iJri-dUt`0%*ygX7JHHv z>%q!fdrXnGrkJsut;r#Xr3>&;f=({OFosHll{)FB(yd8t#N8=2*GozIz(FsgSr4Wc z;AWJ}Kf$3*+G2YiIRz%f^e?8|QZu)ca6I9o=B+`K`IiJ&4eN zck(?(#GP_=`?lY#r9#{Cp{l6bzG$MS=&LD>Oh&9%v($G($X@gCl<~xzcXRw=P@{+z zuNj?X3xh}0tiuMg4A!C-nck^Ri#nH^RbLYOw!acJ;_fRn(O(%lGQBTuvR&93_QcfK z1mDz~LW}&c4gFSI7pbfn=d5qQi^@<{PZT6f zen^*Kw^xp+U6rV*9O_m)KB`!C07T@@`a%h zp-r#G%GPI#N2R_2C05cu2<-C{Is~@$T)F^T6WvM63I1VCUHr6ZipZ6yI{po~|FvIR zx-|S?A)0K`O<0S5$h%$;DjUI4uq_qjx?O%Yt746EbJ&2y zR7b93@?AEfCJyM2y_Wq50D%CJ>ZV%8E;WxE7Zc1`V}J1O4}HtS&`)PVrGdL%t>>f1 z#5ZvI%!pTpf+0Jq9iUaFgi6dgl;ekWIvQE;snFRy9sJ%k+2bYMguAc9`$3JxYOSJNin86=*gMin%1ea5ND5sl0S12 z5H4`6J+-0P%3Hss%%|XnF;$+|k1$2p7`ulqi~G&AJU6865x#~a+}xi%qMLByK#pH) zLmV~W&h`RlfI8{!Uw6|(8aB7MQL_KJ!3P>XVhCv2>(tKn-d?ILmqe>UoR8$)_dmKa zwQ1E*EdSebGpq@1FSJ#iT#)d15KE0@zn( z@3zU|zRkR#V@6BHq-PJ*TuvM$6|JoAg7%6c4lPM&IpV?x*xrVzO$iIQ{m^N9KU;h%2$CdT95}DgLL;) zi<5=b6N|$lThPXU`r%LB!OmO`*Ks_)a?U-!bkhr%Cq)UZWlYl!p%V9cGBQ*vwZe^3 z7(5X&!D)f_$MA^ca22n722yK#&2Odg^+F!6RGuo9;7U1_b!G7zdJQxlt4ZOm`SkIa zA5P#K&@yNK45f%d3|$g;xfyb6Cc-aZUzJ3Fw(*McEI^u()?`_oS+i)+XOim$-n)W)9QTs zADZz;Cw}RQbM1)2FQewPge`u#8>c0k`~M{5+W(u7Bar{ea9;>qT>mv@(-@`Y!^L%Q uZ2vz3xGw$A$bV_~e}Uq^!^mw-sqn*yCgClHPW%3fu3xpdg8uvNgZ~2p*)~1^ literal 0 HcmV?d00001 diff --git a/view/base/web/images/mobilepay_logo.png b/view/base/web/images/mobilepay_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d043bdabfce02d655fe4497ee2bb7bb9cc17cc11 GIT binary patch literal 1914 zcmV-=2Zi{FP)IckB`eRjRuJCMzIWL7WxDJX9*QQq2ky9x8>t*nVk&K47l^Bendf57;Y+vjWTt zn5+OJRVb8rZolvT*movlCxJjDsl*`R=`}v7UmhRHVwbB|LsP zQV>KPf_gzK`sSs$wbEIf%$N~JbeuRXQ6Tl|57dEf8Wb!9#cn0 zvJ;hHc^U_u^K#huFL({*Y>U0X@L1daTVWdxv4OcBx$@R|H~ekI1VZ~LCV1B7^;1+d zEsc`hNB3$4%7=BazlG!SgmJrgHH+GA?&%~=rThGW>c|CBR9?Zs;gl+N@eEN5S1a-o z%`I%ON|;3T0Ehh;?=~S*&k0iU^s^0GVwM@WPzf@ODA@@jfVXX-P#hWPJ3^pg54Z<`JwDb>GgH(lxrX|;c#jBg zr$Gp`z8-!%VST-#epH0jRcQbTu4CN;2~rQBL=}6X zz{awG!a1OnZ?Rs#$l5W_;nRDn5!U^*r}s4@X>ImAoX1n-loByQA8qVk&8JDzvxyMo zvEoWaI{zI>lbYZeq7bntwI)W#?j$vSEhcx~mXxyGep9lhe>{;J^MrM>K%c}xYl02OIFLsw zGO*V#;?=BYBjVWa6Ms9XZ()wMoI;vPY{z|9G@s{59MV8>hiI#zxP!JH-nXZ7rPg&o zV7DVL7*q^Z4uexA12cJQ4lrKCAlo6dLWfK3@Zsb;m53AnWYLC z+oDA~7;fWD2s3b_oS(~`)|h=x?Kgif7ExeLfM@b#c&0m`Z(TH&f`K5e8#YpTm6?yc zIKJ>?+yErlbG+BRiwImt@&qUn1q`M z0R1CE)0Ah&izh-h+{kPHK)5gc0V8_uhrT+)SD=`rC%>za**k>jmu;OARikw=7V zuEW3~=ob4zh%Dw?X2_NimV>>6e4s&wc}K_)m3T{Mhe>u3Ay(-_@YM0W_F-}|u`bO_ z`5>$}is48(SPWS)|DCgi<(NQ$Hz&NeG_$OE+gT!}8{qwh0pDXAj}dl|Wa!v#zxfb#t_af0*XzAeg6j*3o^)6Nr^9^w|D z=>rm4K#}G`NQcHEbygroF_>7R?MXggf@3yGrqeQ7H+_JaM@)ouNWWkL4rlqtJ~^eu zMzSGRIS$=C)2p-5};BxS0Lq(0)lam~uZ&nO{HB+}$auP*?^xqikZz zsEWUL@pgc>)-c^y>B-xJC4D4C{@&HWd(pY7d4u6tI}pew2Dc%&9phk$t+2ZM?_Ep! zGE|<|0ITUBfT%$@gu4Utd)NKX^E=Iwmh{!p{~8^HD|M-G_5c6?07*qoM6N<$f;>!; Ai~s-t literal 0 HcmV?d00001 diff --git a/view/base/web/images/vipps_logo_rgb.png b/view/base/web/images/vipps_logo_rgb.png old mode 100755 new mode 100644 diff --git a/view/frontend/web/js/view/payment/method-renderer/vipps.js b/view/frontend/web/js/view/payment/method-renderer/vipps.js index c1d3e691..543ad005 100644 --- a/view/frontend/web/js/view/payment/method-renderer/vipps.js +++ b/view/frontend/web/js/view/payment/method-renderer/vipps.js @@ -19,6 +19,7 @@ define( 'jquery', 'mage/storage', 'mage/url', + 'mage/translate', 'Magento_Checkout/js/view/payment/default', 'Magento_Checkout/js/model/error-processor', 'Magento_Checkout/js/model/full-screen-loader' @@ -27,6 +28,7 @@ define( $, storage, url, + $t, Component, errorProcessor, fullScreenLoader @@ -46,6 +48,19 @@ define( return window.checkoutConfig.payment.vipps.logoSrc; }, + getLogoWidth: function () { + return window.checkoutConfig.payment.vipps.logoWidth; + }, + + getRedirectText: function () { + return $t('You will be redirected to the %1 website.').replace('%1', this.getTitle()); + }, + + getNoteText: function () { + return $t('Almost done! Remember, there are no fees using %1 when shopping online.') + .replace('%1', this.getTitle()); + }, + afterPlaceOrder: function () { $.mage.redirect(this.redirectUrl); }, @@ -61,10 +76,12 @@ define( {} ).done( function (response, msg, xhr) { - if (response.hasOwnProperty('url')) { - self.redirectUrl = response.url; + url = response.redirectUrl || response.url; + if (typeof url !== 'undefined') { + self.redirectUrl = url; self.isPlaceOrderActionAllowed(true); + return self.placeOrder(data, event); } else { errorProcessor.process(xhr, self.messageContainer); diff --git a/view/frontend/web/template/payment/vipps.html b/view/frontend/web/template/payment/vipps.html index bd011218..2c8c81a8 100644 --- a/view/frontend/web/template/payment/vipps.html +++ b/view/frontend/web/template/payment/vipps.html @@ -19,7 +19,7 @@ class="radio" data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" /> @@ -28,10 +28,8 @@
-
- -
- +
+