diff --git a/Block/PartialVoid/Order/Email/Items.php b/Block/PartialVoid/Order/Email/Items.php
new file mode 100644
index 00000000..3f24e6de
--- /dev/null
+++ b/Block/PartialVoid/Order/Email/Items.php
@@ -0,0 +1,53 @@
+order) {
+ $this->order = clone $this->getData('order');
+ $items = $this->order->getItems();
+ foreach ($items as $key => $item) {
+ if (!$item->getQtyCanceled()) {
+ unset($items[$key]);
+ }
+ }
+ $this->order->setItems($items);
+ }
+
+ return $this->order;
+ }
+}
diff --git a/Controller/Payment/ShippingDetails.php b/Controller/Payment/ShippingDetails.php
index 08895d4e..468d5006 100644
--- a/Controller/Payment/ShippingDetails.php
+++ b/Controller/Payment/ShippingDetails.php
@@ -1,6 +1,6 @@
logger = $logger;
$this->addressUpdater = $addressUpdater;
$this->gdprCompliance = $compliance;
+ $this->shippingMethodValidator = $shippingMethodValidator;
}
/**
@@ -147,12 +151,18 @@ public function execute()
'shippingDetails' => []
];
foreach ($shippingMethods as $key => $shippingMethod) {
+
+ $methodFullCode = $shippingMethod->getCarrierCode() . '_' . $shippingMethod->getMethodCode();
+ if (!$this->shippingMethodValidator->isValid($methodFullCode)) {
+ continue;
+ }
+
$responseData['shippingDetails'][] = [
'isDefault' => 'N',
'priority' => $key,
'shippingCost' => $shippingMethod->getAmount(),
- 'shippingMethod' => $shippingMethod->getMethodCode(),
- 'shippingMethodId' => $shippingMethod->getCarrierCode() . '_' . $shippingMethod->getMethodCode(),
+ 'shippingMethod' => $shippingMethod->getMethodTitle(),
+ 'shippingMethodId' => $methodFullCode,
];
}
$result->setHttpResponseCode(ZendResponse::STATUS_CODE_200);
diff --git a/Gateway/Command/CaptureCommand.php b/Gateway/Command/CaptureCommand.php
index acb5bb62..b72be8ef 100644
--- a/Gateway/Command/CaptureCommand.php
+++ b/Gateway/Command/CaptureCommand.php
@@ -255,7 +255,7 @@ private function captureBasedOnPaymentDetails($commandSubject, Transaction $tran
$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 happened that previous operation was successfully completed in 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',
diff --git a/Gateway/Command/RefundCommand.php b/Gateway/Command/RefundCommand.php
index 797fc7d2..021bfc03 100644
--- a/Gateway/Command/RefundCommand.php
+++ b/Gateway/Command/RefundCommand.php
@@ -253,7 +253,7 @@ private function refundBasedOnPaymentDetails($commandSubject, Transaction $trans
$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 happened that previous operation was successfully completed in 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',
diff --git a/Gateway/Config/CanVoidHandler.php b/Gateway/Config/CanVoidHandler.php
new file mode 100644
index 00000000..e69180f3
--- /dev/null
+++ b/Gateway/Config/CanVoidHandler.php
@@ -0,0 +1,85 @@
+gatewayConfig = $gatewayConfig;
+ $this->paymentDetailsProvider = $paymentDetailsProvider;
+ $this->transactionBuilder = $transactionBuilder;
+ }
+
+ /**
+ * Disable partial online void
+ *
+ * @param array $subject
+ * @param null $storeId
+ *
+ * @return bool
+ * @throws NoSuchEntityException
+ * @throws VippsException
+ */
+ public function handle(array $subject, $storeId = null): bool
+ {
+ $response = $this->paymentDetailsProvider->get($subject);
+ $transaction = $this->transactionBuilder->setData($response)->build();
+ if ($transaction->getTransactionSummary()->getCapturedAmount() > 0) {
+ return false;
+ }
+
+ return (bool)$this->gatewayConfig->getValue(SubjectReader::readField($subject), $storeId);
+ }
+}
diff --git a/INSTALL.md b/INSTALL.md
index e6333e87..ffbd8da2 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -7,7 +7,7 @@
1. You must have a Vipps merchant account. See [Vipps på Nett](https://www.vipps.no/bedrift/vipps-pa-nett)
1. As with _all_ Magento extensions, it is highly recommended to backup your site before installation and to install and test on a staging environment prior to production deployments.
-# Installation via Composer (recommended)
+# Installation via Composer
1. Navigate to your [Magento root directory](https://devdocs.magento.com/guides/v2.2/extension-dev-guide/build/module-file-structure.html).
1. Enter command: `composer require vipps/module-payment`
@@ -15,20 +15,6 @@
1. Enter command: `php bin/magento setup:upgrade`
1. Put your Magento in production mode if it’s required.
-# Installation via Marketplace
-
-**Please note:** _This extension is not yet available on Magento Marketplace. This notice will be removed when it is._
-
-Here are steps required to install Payments extension via Component Manager.
-
-1. Make a purchase for the Vipps extension on [Magento Marketplace](https://marketplace.magento.com).
-1. From your `Magento Admin` access `System` -> `Web Setup Wizard` page.
-1. Enter Marketplace authentication keys. Please read about authentication keys generation.
-1. Navigate to `Component Manager` page.
-1. On the `Component Manager` page click the `Sync button to update your new purchased extensions.
-1. Click `Install` in the `Action` column for `Realex Payments` component.
-1. . Follow Web Setup Wizard instructions.
-
# Configuration
The Vipps Payment module can be easily configured to meet business expectations of your web store. This section will show you how to configure the extension via `Magento Admin`.
@@ -38,6 +24,7 @@ From Magento Admin navigate to `Store` -> `Configuration` -> `Sales` -> `Payment
By clicking the `Configure` button, all configuration module settings will be shown. Once you have finished with the configuration simply click `Close` and `Save` button for your convenience.
## Add a separate connection for Vipps resources
+These settings are required to prevent profiles loss when Magento reverts invoice/refund transactions.
* Duplicate 'default' connection in app/etc/env.php and name it 'vipps'. It should look like:
```
'vipps' =>
@@ -59,7 +46,6 @@ By clicking the `Configure` button, all configuration module settings will be sh
'connection' => 'vipps',
),
```
-These settings are required to prevent profiles loss when Magento reverts invoice/refund transactions.
# Settings
@@ -68,7 +54,7 @@ Vipps Payments configuration is divided by sections. It helps to quickly find an
1. Basic Vipps Settings.
1. Express Checkout Settings.
-![Screenshot of Vipps Settings](docs/vipps_method.png)
+![Screenshot of Vipps Settings](docs/images/vipps_method.png)
Please ensure you check all configuration settings prior to using Vipps Payment. Pay attention to the Vipps Basic Settings section, namely `Saleunit Serial Number`, `Client ID`, `Client Secret`, `Subscription Key 1`, `Subscription Key 2`.
@@ -76,11 +62,20 @@ For information about how to find the above values, see the [Vipps Developer Por
# Basic Vipps Settings
-![Screenshot of Basic Vipps Settings](docs/vipps_basic.png)
+![Screenshot of Basic Vipps Settings](docs/images/vipps_basic.png)
# Express Checkout Settings
-![Screenshot of Express Vipps Settings](docs/express_vipps_settings.png)
+![Screenshot of Express Vipps Settings](docs/images/express_vipps_settings.png)
+
+# Quote Monitoring
+
+Quote it is a cart contents in Magento. Theoretically the quote is an offer and if the user accepts it (by checking out) it converts to order.
+
+When payment was initiated (customer was redirected to Vipps) Magento creates a new record on `Vipps Quote Monitoring` page and starts tracking an Vipps order.
+To do that Magento has a cron job that runs by schedule/each 10 min.
+
+You can find this page under `System -> Vipps` menu section. Under `Store -> Sales -> Payment Methods -> Vipps -> Cancellation` you can find appropriate configuration settings.
# Support
diff --git a/Model/Config/Source/Email/Template.php b/Model/Config/Source/Email/Template.php
new file mode 100644
index 00000000..bff6c827
--- /dev/null
+++ b/Model/Config/Source/Email/Template.php
@@ -0,0 +1,69 @@
+config = $config;
+ }
+
+ /**
+ * Replace config path for correct rendering default template option
+ *
+ * @return array
+ */
+ public function toOptionArray(): array
+ {
+ $element = $this->config->getElement($this->getPath());
+ $configData = $element->getData();
+ $this->setPath($configData['config_path']);
+ return parent::toOptionArray();
+ }
+}
diff --git a/Model/Order/PartialVoid/Config.php b/Model/Order/PartialVoid/Config.php
new file mode 100644
index 00000000..dafe6fd0
--- /dev/null
+++ b/Model/Order/PartialVoid/Config.php
@@ -0,0 +1,118 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * @param null $storeId
+ *
+ * @return bool
+ */
+ public function isOfflinePartialVoidEnabled($storeId = null): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_IS_ENABLED, ScopeInterface::SCOPE_STORE, $storeId);
+ }
+
+ /**
+ * @param null $storeId
+ *
+ * @return bool
+ */
+ public function isSendMailEnabled($storeId = null): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_IS_SEND_MAIL_ENABLED, ScopeInterface::SCOPE_STORE, $storeId);
+ }
+
+ /**
+ * @param null $storeId
+ *
+ * @return string
+ */
+ public function getEmailTemplate($storeId = null): string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_EMAIL_TEMPLATE, ScopeInterface::SCOPE_STORE, $storeId);
+ }
+
+ /**
+ * @param null $storeId
+ *
+ * @return string
+ */
+ public function emailSender($storeId = null): string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_EMAIL_SENDER, ScopeInterface::SCOPE_STORE, $storeId);
+ }
+
+ /**
+ * @param null $storeId
+ *
+ * @return string
+ */
+ public function getEmailMessage($storeId = null): string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_EMAIL_MESSAGE, ScopeInterface::SCOPE_STORE, $storeId);
+ }
+}
diff --git a/Model/Order/PartialVoid/SendMail.php b/Model/Order/PartialVoid/SendMail.php
new file mode 100644
index 00000000..07ef3feb
--- /dev/null
+++ b/Model/Order/PartialVoid/SendMail.php
@@ -0,0 +1,85 @@
+transportBuilder = $transportBuilder;
+ $this->storeManager = $storeManager;
+ $this->config = $config;
+ }
+
+ /**
+ * @param OrderInterface $order
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\MailException
+ */
+ public function send(OrderInterface $order)
+ {
+ $transport = $this->transportBuilder
+ ->setTemplateIdentifier($this->config->getEmailTemplate($order->getStoreId()))
+ ->setFromByScope($this->config->emailSender($order->getStoreId()))
+ ->addTo($order->getCustomerEmail(), "{$order->getCustomerFirstname()} {$order->getCustomerLastname()}")
+ ->setTemplateOptions([
+ 'area' => Area::AREA_FRONTEND,
+ 'store' => $order->getStoreId(),
+ ])
+ ->setTemplateVars([
+ 'order' => $order,
+ 'payment_html' => $order->getPayment()->getMethodInstance()->getTitle(),
+ 'email_message' => $this->config->getEmailMessage($order->getStoreId()),
+ ])
+ ->getTransport();
+
+ $transport->sendMessage();
+ }
+}
diff --git a/Model/Quote/ShippingMethodValidator.php b/Model/Quote/ShippingMethodValidator.php
new file mode 100644
index 00000000..491affea
--- /dev/null
+++ b/Model/Quote/ShippingMethodValidator.php
@@ -0,0 +1,50 @@
+config = $config;
+ }
+
+ /**
+ * @param string $methodCode
+ * @return bool
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function isValid($methodCode)
+ {
+ $disabledShippingMethods = explode(',', $this->config->getValue('disallowed_shipping_methods'));
+ return !in_array($methodCode, $disabledShippingMethods);
+ }
+}
diff --git a/Observer/AvailabilityByShippingMethod.php b/Observer/AvailabilityByShippingMethod.php
new file mode 100644
index 00000000..d925fceb
--- /dev/null
+++ b/Observer/AvailabilityByShippingMethod.php
@@ -0,0 +1,71 @@
+shippingMethodValidator = $shippingMethodValidator;
+ }
+
+ /**
+ * @param Observer $observer
+ * @return void
+ *
+ * 'result' => $checkResult,
+ * 'method_instance' => $this,
+ * 'quote' => $quote
+ *
+ * @throws NoSuchEntityException
+ */
+ public function execute(Observer $observer)
+ {
+ $method = $observer->getData('method_instance');
+ $quote = $observer->getData('quote');
+ $result = $observer->getData('result');
+
+ // Ignore not Vipps method and in case no quote here.
+ if ($method->getCode() !== 'vipps' || !$quote) {
+ return;
+ }
+
+ /** @var Quote $quote */
+ $quote = $observer->getData('quote');
+ if (!$this->shippingMethodValidator->isValid($quote->getShippingAddress()->getShippingMethod())) {
+ $result->setData('is_available', false);
+ }
+
+ return;
+ }
+}
diff --git a/Observer/SendOfflineVoidEmail.php b/Observer/SendOfflineVoidEmail.php
new file mode 100644
index 00000000..851588f9
--- /dev/null
+++ b/Observer/SendOfflineVoidEmail.php
@@ -0,0 +1,112 @@
+dataObjectFactory = $dataObjectFactory;
+ $this->paymentDetailsProvider = $paymentDetailsProvider;
+ $this->transactionBuilder = $transactionBuilder;
+ $this->sendMail = $sendMail;
+ $this->config = $config;
+ }
+
+ /**
+ * Send email to customer about offline void
+ *
+ * @param Observer $observer
+ *
+ * @throws LocalizedException
+ * @throws MailException
+ * @throws VippsException
+ */
+ public function execute(Observer $observer)
+ {
+ /** @var Order $order */
+ $order = $observer->getData('order');
+ $payment = $order->getPayment();
+ $offlineVoidEnabled = $this->config->isOfflinePartialVoidEnabled($order->getStoreId());
+ $sendMailEnabled = $this->config->isSendMailEnabled($order->getStoreId());
+
+ if ($payment->getMethod() === 'vipps' && $offlineVoidEnabled && $sendMailEnabled) {
+ $paymentDataObject = $this->dataObjectFactory->create($payment);
+ $response = $this->paymentDetailsProvider->get(['payment' => $paymentDataObject]);
+ $transaction = $this->transactionBuilder->setData($response)->build();
+ if ($transaction->getTransactionSummary()->getRemainingAmountToCapture() > 0) {
+ $this->sendMail->send($order);
+ }
+ }
+ }
+}
diff --git a/Plugin/Sales/Model/Order/Payment.php b/Plugin/Sales/Model/Order/Payment.php
new file mode 100644
index 00000000..5204185e
--- /dev/null
+++ b/Plugin/Sales/Model/Order/Payment.php
@@ -0,0 +1,94 @@
+dataObjectFactory = $dataObjectFactory;
+ $this->paymentDetailsProvider = $paymentDetailsProvider;
+ $this->transactionBuilder = $transactionBuilder;
+ $this->config = $config;
+ }
+
+ /**
+ * Throw exception if offline partial void disabled
+ *
+ * @param MagentoPayment $payment
+ *
+ * @throws LocalizedException
+ * @throws VippsException
+ */
+ public function beforeCancel(MagentoPayment $payment)
+ {
+ if ($payment->getMethod() === 'vipps') {
+ $paymentDataObject = $this->dataObjectFactory->create($payment);
+ $response = $this->paymentDetailsProvider->get(['payment' => $paymentDataObject]);
+ $transaction = $this->transactionBuilder->setData($response)->build();
+ $offlineVoidEnabled = $this->config->isOfflinePartialVoidEnabled();
+ if (!$offlineVoidEnabled && $transaction->getTransactionSummary()->getCapturedAmount() > 0) {
+ throw new LocalizedException(__('Can\'t cancel captured transaction.'));
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index e6c52a8e..b85f8444 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,35 @@ Please see: https://github.com/vippsas/vipps-magento-v1
Please follow the instructions in [INSTALL.md](INSTALL.md)
+
+### Quote Processing Flow
+
+1. When payment was initiated a new record is created on Vipps Quote Monitoring page with status `New`.
+1. Magento polls Vipps for orders to process by cron.
+1. When order was accepted on Vipps side, Magento is trying to place an order and marks a record as `Placed`
+1. When order was cancelled on Vipps side, Magento marks such record as `Cancelled`
+1. If order has not been accepted on Vipps side within some period of time so it marked as expired, Magento marks it as `Expired`
+1. If order has not been yet accepted on Vipps side and has not been expired yet, Magento marks it as `Processing`. Appropriate message added on record details page.
+1. If order accepted on Vipps side but an error occurred during order placement on Magento side, such record marks as `Processing`. Appropriate message added on record details page.
+1. Magento is trying to process the same record `3` times and when it failed after `3` times such record marks as `Place Failed`.
+1. It is possible to specify that Magento has to cancel Vipps order automatically when appropriate Magento quote was failed so that client's money released. See `Store -> Sales -> Payment Methods -> Vipps -> Cancellation`
+1. If it is specified that Magento has to cancel all failed quotes then Magento fetches all records marked as `Place Failed`, cancel them and marks as `Cancelled`
+
+Here is a diagram of the process
+![Screenshot of Quote Processing Flow](docs/images/quote-monitoring-flow.png)
+
+
+# Requests Profiling
+
+Requests Profiling is a page in Magento admin panel that helps you to track a communication between Vipps and Magento.
+You can find the page under `System -> Vipps`
+
+On the page you can see the list of all requests for all orders that Magento sends to Vipps.
+By clicking on a link `Show` in an `Action` column of grid you can find appropriate response from Vipps.
+
+Using built-in Magento grid filter you could easily find all requests per order that you are interested in.
+
+
# Magento
Magento is an open-source e-commerce platform written in PHP: https://magento.com
diff --git a/composer.json b/composer.json
index 07152200..c7e4ad6a 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,7 @@
"type": "magento2-module",
"description": "Vipps Payment Method",
"license": "proprietary",
- "version": "1.2.8",
+ "version": "1.2.9",
"require": {
"magento/framework": "101.0.*",
"magento/module-sales": "101.0.*",
diff --git a/docs/express_vipps_settings.png b/docs/images/express_vipps_settings.png
similarity index 100%
rename from docs/express_vipps_settings.png
rename to docs/images/express_vipps_settings.png
diff --git a/docs/images/quote-monitoring-flow.png b/docs/images/quote-monitoring-flow.png
new file mode 100644
index 00000000..98e61afa
Binary files /dev/null and b/docs/images/quote-monitoring-flow.png differ
diff --git a/docs/vipps_basic.png b/docs/images/vipps_basic.png
similarity index 100%
rename from docs/vipps_basic.png
rename to docs/images/vipps_basic.png
diff --git a/docs/vipps_method.png b/docs/images/vipps_method.png
similarity index 100%
rename from docs/vipps_method.png
rename to docs/images/vipps_method.png
diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml
index 3df9a8fd..6b5c9489 100644
--- a/etc/adminhtml/di.xml
+++ b/etc/adminhtml/di.xml
@@ -1,7 +1,7 @@
+ {{trans "%customer_name," customer_name=$order.getCustomerName()}} ++ {{var email_message|raw}} + ++ {{trans 'You can check the status of your order by logging into your account.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} + ++ {{trans 'If you have questions about your order, you can email us at %store_email' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at %store_phone' store_phone=$store_phone |raw}}{{/depend}}. + {{depend store_hours}} + {{trans 'Our hours are %store_hours.' store_hours=$store_hours |raw}} + {{/depend}} + + |
+ |
+ {{trans 'Your Order #%increment_id' increment_id=$order.increment_id |raw}}+{{trans 'Placed on %created_at' created_at=$order.getCreatedAtFormatted(2) |raw}} + |
+ |
+ {{depend order.getEmailCustomerNote()}}
+
|
+
+ {{/depend}}
+
= $block->escapeHtml($_item->getName()) ?>
+= /* @escapeNotVerified */ __('SKU') ?>: = $block->escapeHtml($block->getSku($_item)) ?>
+ getItemOptions()): ?> + + + getProductAdditionalInformationBlock(); ?> + + = $addInfoBlock->setItem($_item)->toHtml() ?> + + = $block->escapeHtml($_item->getDescription()) ?> +Ordered | += /* @escapeNotVerified */ $_item->getQtyOrdered() * 1 ?> | +
Canceled | += /* @escapeNotVerified */ $_item->getQtyCanceled() * 1 ?> | +